From f88b4f1bf37ffdde2e6f0b0cfdf3d31fc22ad7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Dvo=C5=99=C3=A1k?= <150935816+Dvorinka@users.noreply.github.com> Date: Sat, 10 Oct 2026 10:56:33 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20UI/UX=20pass=20=E2=80=94=20connectiv?= =?UTF-8?q?ity=20probe,=20mobile=20overflow,=20real=20metrics,=20deep=20li?= =?UTF-8?q?nks=20(#85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found during a browser-driven audit of the whole app: - connectivity banner showed permanently: pingServer() resolved undefined, which React Query treats as an error; returns boolean now - Vite dev proxy now forwards /health so the probe actually measures the backend, and the API target is env-overridable (VITE_API_PROXY) - auth-session removed from the persisted query cache — stale sessions no longer rehydrate into a false signed-in flash - workspace Observability tab no longer shows fabricated random metrics ("87K Active User", fake performance/speed-test cards); the dashboard now aggregates real per-service metrics (CPU, memory, network, instances) - service detail tabs write ?section= so tab state is shareable/deep-linked - Security page auto-selects the first project instead of rendering blank - People page user-creation form no longer overflows its card (minmax(0,1fr) grid tracks; same fix on env-var rows) - page padding is responsive (px-4 sm:px-8) so mobile keeps content width; Projects header wraps instead of clipping the "+ new project" button - template deploy modal shows a loading state while the session resolves instead of flashing the sign-in prompt - audit log table strips the CIDR suffix from displayed IPs - docs list excerpts no longer repeat the document title - activity filter placeholder "Search search…" → "Search events…" - workspace view tabs get more room: ⌘K button now waits for lg: Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ROADMAP2.md | 2 +- .../features/activity/pages/ActivityPage.tsx | 4 +- .../features/audit/pages/AuditLogsPage.tsx | 4 +- .../src/features/builds/pages/BuildsPage.tsx | 4 +- .../src/features/docs/DocsBrowser.tsx | 3 +- .../src/features/nodes/NodeDetailPage.tsx | 2 +- .../operations/pages/OperationsPage.tsx | 2 +- .../features/projects/pages/ProjectsPage.tsx | 4 +- app/frontend/src/features/secondary/pages.tsx | 68 +-- .../service/pages/ServiceDetailPage.tsx | 23 +- .../templates/pages/TemplatesPage.tsx | 8 +- .../features/webhooks/pages/WebhooksPage.tsx | 2 +- .../workspace/components/MetricsDashboard.tsx | 471 +++++++----------- .../components/SharedVariablesSection.tsx | 2 +- .../workspace/pages/ProjectWorkspacePage.tsx | 10 +- app/frontend/src/lib/api-client.ts | 3 +- app/frontend/src/main.tsx | 1 + app/frontend/vite.config.ts | 6 +- 18 files changed, 270 insertions(+), 349 deletions(-) diff --git a/ROADMAP2.md b/ROADMAP2.md index 2d39cdc..76ac644 100644 --- a/ROADMAP2.md +++ b/ROADMAP2.md @@ -537,7 +537,7 @@ and wired refs, one click or one CLI call. workspace, CLI `environments` + MCP parity. Per-env vars/domains ride the service's own scope (one env per service); promote ships via preview promote. -- [ ] **sqlc port** — ~265 raw `database/sql` sites → `sqlc/queries/`. Done in tranches (dynamic queries stay raw). Tranches merged: metrics storage + service domains (#75), service clone/move ops (#76), preview environments (#77), deployments + rollback (#78). Remaining: runtime, compliance, cron, git, security, auth, sleeper, scanner, gateway, and smaller files. +- [x] **sqlc port** — all static `database/sql` call sites → `sqlc/queries/` + generated `sqlcdb`. Merged: metrics + domains (#75), service clone/move (#76), preview environments (#77), deployments + rollback (#78), runtime ops (#79), cron (#80), sleep/wake sweeper (#81), git providers/repos/webhooks (#82), auth + security (#83), operations/settings/audit/activity/builds/misc + apwhy gateway (#84). Raw SQL remains only for runtime-assembled statements (filtered lists, sparse updates) and Better Auth `auth_users` probes. - [ ] **Tests** — canvas/wizard frontend coverage, deployment/template/ webhook table tests, Playwright e2e golden path. - [x] **First-run wizard** — public `GET /setup/status` + authed diff --git a/app/frontend/src/features/activity/pages/ActivityPage.tsx b/app/frontend/src/features/activity/pages/ActivityPage.tsx index 0606065..1fcae08 100644 --- a/app/frontend/src/features/activity/pages/ActivityPage.tsx +++ b/app/frontend/src/features/activity/pages/ActivityPage.tsx @@ -25,7 +25,7 @@ const SEVERITY_STYLE: Record[] = [ - { key: 'q', label: 'Search', type: 'search', searchFields: ['label', 'action', 'user_email'] }, + { key: 'q', label: 'events', type: 'search', searchFields: ['label', 'action', 'user_email'] }, { key: 'severity', label: 'Severity', type: 'select', options: [ @@ -67,7 +67,7 @@ export function ActivityPage() { -
+
diff --git a/app/frontend/src/features/audit/pages/AuditLogsPage.tsx b/app/frontend/src/features/audit/pages/AuditLogsPage.tsx index e6c5781..c4c6439 100644 --- a/app/frontend/src/features/audit/pages/AuditLogsPage.tsx +++ b/app/frontend/src/features/audit/pages/AuditLogsPage.tsx @@ -85,7 +85,7 @@ export function AuditLogsPage() {
-
+
@@ -140,7 +140,7 @@ export function AuditLogsPage() { {log.resourceId || '—'} - {log.ipAddress || '—'} + {log.ipAddress?.replace(/\/\d+$/, '') || '—'} {log.details && log.details !== '{}' ? log.details : '—'} diff --git a/app/frontend/src/features/builds/pages/BuildsPage.tsx b/app/frontend/src/features/builds/pages/BuildsPage.tsx index 8893721..2bdf1ad 100644 --- a/app/frontend/src/features/builds/pages/BuildsPage.tsx +++ b/app/frontend/src/features/builds/pages/BuildsPage.tsx @@ -192,7 +192,7 @@ export function BuildsPage() { )} {/* Stats Overview */} -
+
@@ -263,7 +263,7 @@ export function BuildsPage() {
{/* Build Table */} -
+
{!isDemoMode && buildsQuery.isLoading ? (
diff --git a/app/frontend/src/features/docs/DocsBrowser.tsx b/app/frontend/src/features/docs/DocsBrowser.tsx index dfb04a5..e5bd562 100644 --- a/app/frontend/src/features/docs/DocsBrowser.tsx +++ b/app/frontend/src/features/docs/DocsBrowser.tsx @@ -15,6 +15,7 @@ function renderMarkdown(markdown: string): string { function excerpt(markdown: string, length = 140): string { const text = markdown + .replace(/^#[^\n]*\n/, ' ') .replace(/```[\s\S]*?```/g, ' ') .replace(/[#>*`\-[\]()]/g, ' ') .replace(/\s+/g, ' ') @@ -119,7 +120,7 @@ export function DocsBrowser() {
{selected ? ( -
+

{selected.section} / {selected.path}

diff --git a/app/frontend/src/features/nodes/NodeDetailPage.tsx b/app/frontend/src/features/nodes/NodeDetailPage.tsx index c95332a..5508cd9 100644 --- a/app/frontend/src/features/nodes/NodeDetailPage.tsx +++ b/app/frontend/src/features/nodes/NodeDetailPage.tsx @@ -330,7 +330,7 @@ export function NodeDetailPage() {
-
+
diff --git a/app/frontend/src/features/operations/pages/OperationsPage.tsx b/app/frontend/src/features/operations/pages/OperationsPage.tsx index b4f00c2..abeb1ab 100644 --- a/app/frontend/src/features/operations/pages/OperationsPage.tsx +++ b/app/frontend/src/features/operations/pages/OperationsPage.tsx @@ -88,7 +88,7 @@ export function OperationsPage() {
) : ( -
+

In flight

diff --git a/app/frontend/src/features/projects/pages/ProjectsPage.tsx b/app/frontend/src/features/projects/pages/ProjectsPage.tsx index 6f399f6..982d68a 100644 --- a/app/frontend/src/features/projects/pages/ProjectsPage.tsx +++ b/app/frontend/src/features/projects/pages/ProjectsPage.tsx @@ -211,9 +211,9 @@ export function ProjectsPage() { return (
-
+
{/* Page head */} -
+

Projects_

diff --git a/app/frontend/src/features/secondary/pages.tsx b/app/frontend/src/features/secondary/pages.tsx index 1bbab92..f3d7b75 100644 --- a/app/frontend/src/features/secondary/pages.tsx +++ b/app/frontend/src/features/secondary/pages.tsx @@ -436,7 +436,7 @@ export function UsagePage() { title="Usage" description="Platform usage metrics and operational summaries" /> -

+
@@ -844,7 +844,7 @@ export function PeoplePage() { title="People" description="Team management and access control" /> -
+
{profileQuery.isLoading ? (
@@ -888,7 +888,7 @@ export function PeoplePage() {
{ event.preventDefault(); createUserMutation.mutate(); @@ -2145,7 +2145,7 @@ export function SettingsPage() { title="Settings" description="Manage account profile and local configuration" /> -
+
{/* Profile Section */}
@@ -2376,7 +2376,7 @@ export function DocsPage() { title="Docs" description="Guides and references — synced from GitHub, cached offline" /> -
+
@@ -2513,7 +2513,7 @@ export function DatabasesPage() { title="Databases" description="Managed database services — connection info, runtime actions and backups" /> -
+

{databases.length} {databases.length === 1 ? 'database' : 'databases'} @@ -3251,7 +3251,7 @@ export function HighAvailabilityPage() { title="High availability" description="Failover manager, policies, and active alerts" /> -

+
HA state feeds the platform security posture. @@ -3615,39 +3615,43 @@ export function SecurityPage() { const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects(), enabled: !isDemoMode }); const projects = isDemoMode ? demoProjects : projectsQuery.data ?? []; + + // Auto-select the first project so the page never renders empty — derived, not + // stored, so clearing the select still shows the empty state explicitly. + const effectiveProjectId = projectId || projects[0]?.id || ''; const servicesQuery = useQuery({ - queryKey: ['services', projectId], - queryFn: () => listServicesByProject(projectId), - enabled: !isDemoMode && Boolean(projectId), + queryKey: ['services', effectiveProjectId], + queryFn: () => listServicesByProject(effectiveProjectId), + enabled: !isDemoMode && Boolean(effectiveProjectId), }); - const services = isDemoMode ? getDemoServicesByProject(projectId) : servicesQuery.data ?? []; + const services = isDemoMode ? getDemoServicesByProject(effectiveProjectId) : servicesQuery.data ?? []; const metricsQuery = useQuery({ - queryKey: ['security-metrics', projectId], - queryFn: () => getSecurityMetrics(projectId), - enabled: !isDemoMode && Boolean(projectId), + queryKey: ['security-metrics', effectiveProjectId], + queryFn: () => getSecurityMetrics(effectiveProjectId), + enabled: !isDemoMode && Boolean(effectiveProjectId), refetchInterval: 15_000, }); const historyQuery = useQuery({ - queryKey: ['security-history', projectId], - queryFn: () => getSecurityHistory(projectId), - enabled: !isDemoMode && Boolean(projectId), + queryKey: ['security-history', effectiveProjectId], + queryFn: () => getSecurityHistory(effectiveProjectId), + enabled: !isDemoMode && Boolean(effectiveProjectId), refetchInterval: 15_000, }); const vulnsQuery = useQuery({ - queryKey: ['security-vulns', projectId], - queryFn: () => listVulnerabilities(projectId), - enabled: !isDemoMode && Boolean(projectId), + queryKey: ['security-vulns', effectiveProjectId], + queryFn: () => listVulnerabilities(effectiveProjectId), + enabled: !isDemoMode && Boolean(effectiveProjectId), }); const invalidate = () => { - queryClient.invalidateQueries({ queryKey: ['security-metrics', projectId] }); - queryClient.invalidateQueries({ queryKey: ['security-history', projectId] }); - queryClient.invalidateQueries({ queryKey: ['security-vulns', projectId] }); + queryClient.invalidateQueries({ queryKey: ['security-metrics', effectiveProjectId] }); + queryClient.invalidateQueries({ queryKey: ['security-history', effectiveProjectId] }); + queryClient.invalidateQueries({ queryKey: ['security-vulns', effectiveProjectId] }); }; const scanMutation = useMutation({ mutationFn: () => startSecurityScan({ - project_id: projectId, + project_id: effectiveProjectId, service_id: scanServiceId || undefined, scan_type: scanType as 'dependency' | 'configuration' | 'comprehensive', }), @@ -3661,10 +3665,10 @@ export function SecurityPage() { const metrics = isDemoMode ? demoSecurityMetrics : metricsQuery.data; const scanHistory = isDemoMode - ? demoSecurityScans.filter((s) => s.project_id === projectId) + ? demoSecurityScans.filter((s) => s.project_id === effectiveProjectId) : historyQuery.data ?? []; const vulns = (isDemoMode - ? demoVulnerabilities.filter((v) => v.project_id === projectId) + ? demoVulnerabilities.filter((v) => v.project_id === effectiveProjectId) : vulnsQuery.data ?? [] ).filter((v) => v.status !== 'resolved'); const pageError = @@ -3677,7 +3681,7 @@ export function SecurityPage() { title="Security" description="Scan findings, security posture, and compliance status" /> -
+
Platform availability and failover live under High availability. @@ -3695,7 +3699,7 @@ export function SecurityPage() {
- {projectId && ( + {effectiveProjectId && ( <>
@@ -3748,7 +3752,7 @@ export function SecurityPage() { )}
- {projectId && metrics && ( + {effectiveProjectId && metrics && (
)} - {projectId && ( + {effectiveProjectId && (
@@ -3835,7 +3839,7 @@ export function SecurityPage() {
)} - {projectId && ( + {effectiveProjectId && (
diff --git a/app/frontend/src/features/service/pages/ServiceDetailPage.tsx b/app/frontend/src/features/service/pages/ServiceDetailPage.tsx index c97a734..7ef1c13 100644 --- a/app/frontend/src/features/service/pages/ServiceDetailPage.tsx +++ b/app/frontend/src/features/service/pages/ServiceDetailPage.tsx @@ -143,7 +143,7 @@ function StatusBadge({ status }: { status: string }) { export function ServiceDetailPage() { const { projectId = '', serviceId = '' } = useParams<{ projectId: string; serviceId: string }>(); const navigate = useNavigate(); - const [searchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const queryClient = useQueryClient(); const isDemoMode = useDemoMode(); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); @@ -791,7 +791,7 @@ export function ServiceDetailPage() { )} {/* Metrics Overview */} -
+
setActiveSection(item.key)} + onClick={() => { + setActiveSection(item.key); + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + if (item.key === 'metrics') next.delete('section'); + else next.set('section', item.key); + return next; + }, + { replace: true }, + ); + }} className={`tab ${active ? 'active' : ''}`} > @@ -1189,7 +1200,7 @@ export function ServiceDetailPage() { {' '} {entry.stream} {' '} - {entry.message} + {entry.message.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z?\s+/, '')}

))}
@@ -1362,7 +1373,7 @@ export function ServiceDetailPage() {
) : (
-
+
Key Value Secret @@ -1370,7 +1381,7 @@ export function ServiceDetailPage() {
{varRows.map((row, i) => (
-
+
updateVarRow(i, { key: e.target.value })} diff --git a/app/frontend/src/features/templates/pages/TemplatesPage.tsx b/app/frontend/src/features/templates/pages/TemplatesPage.tsx index c8fd181..1a2750f 100644 --- a/app/frontend/src/features/templates/pages/TemplatesPage.tsx +++ b/app/frontend/src/features/templates/pages/TemplatesPage.tsx @@ -541,7 +541,7 @@ export function TemplatesPage() { )} {/* Main Content */} -
+
{/* Template List */}
@@ -857,7 +857,11 @@ export function TemplatesPage() {
- {!signedIn ? ( + {sessionQuery.isPending && !isDemoMode ? ( +
+ +
+ ) : !signedIn ? (

Sign in to deploy this template into one of your projects. diff --git a/app/frontend/src/features/webhooks/pages/WebhooksPage.tsx b/app/frontend/src/features/webhooks/pages/WebhooksPage.tsx index 4d35350..044f3c7 100644 --- a/app/frontend/src/features/webhooks/pages/WebhooksPage.tsx +++ b/app/frontend/src/features/webhooks/pages/WebhooksPage.tsx @@ -226,7 +226,7 @@ export function WebhooksPage() {

-
+
{secret && (
diff --git a/app/frontend/src/features/workspace/components/MetricsDashboard.tsx b/app/frontend/src/features/workspace/components/MetricsDashboard.tsx index 0008f68..8deaa74 100644 --- a/app/frontend/src/features/workspace/components/MetricsDashboard.tsx +++ b/app/frontend/src/features/workspace/components/MetricsDashboard.tsx @@ -1,306 +1,205 @@ -import { useEffect, useState } from 'react'; -import { - LineMetricChart, - DualLineChart, - DonutChart, -} from '@/shared/components'; - -interface MetricData { - cpu: number[]; - ram: number; - ramUsed: string; - cache: number; - cacheBreakdown: { cache: number; nonCache: number; total: number }; - users: number[]; - performance: number[]; - performanceAlt: number[]; - upSpeed: number; - downSpeed: number; -} +import { useEffect, useRef, useState } from 'react'; +import { useQueries } from '@tanstack/react-query'; +import { Cpu, HardDrive, ArrowDownUp, Boxes } from 'lucide-react'; +import { LineMetricChart, DonutChart } from '@/shared/components'; +import { getServiceMetrics, type ServiceMetrics } from '@/lib/api-client'; +import { formatBytes } from '@/lib/time'; interface MetricsDashboardProps { - projectId?: string; - isRunning?: boolean; - onStop?: () => void; - onRestart?: () => void; + services: { id: string; name: string; status: string }[]; + isDemoMode?: boolean; } -function getRandom(min: number, max: number) { - return Math.floor(Math.random() * (max - min + 1)) + min; +const cardStyle: React.CSSProperties = { + background: 'var(--surface-card, rgba(255,255,255,0.03))', + border: '1px solid var(--border-subtle, rgba(255,255,255,0.09))', + borderRadius: '14px', + padding: '18px 20px', +}; + +function healthLabel(pct: number): { text: string; color: string } { + if (pct < 50) return { text: 'Good', color: 'var(--success, #5ee6a0)' }; + if (pct < 80) return { text: 'Average', color: 'var(--warning, #f2c94c)' }; + return { text: 'High', color: 'var(--error, #ff6b6b)' }; } -function generateInitialData(): MetricData { - return { - cpu: Array.from({ length: 24 }, () => getRandom(10, 45)), - ram: getRandom(50, 85), - ramUsed: '5.4 GB', - cache: 352, - cacheBreakdown: { cache: 212, nonCache: 85.5, total: 1750 }, - users: Array.from({ length: 15 }, () => getRandom(40, 110)), - performance: Array.from({ length: 12 }, () => getRandom(70, 95)), - performanceAlt: Array.from({ length: 12 }, () => getRandom(60, 85)), - upSpeed: 10.4, - downSpeed: 5.2, - }; -} +export function MetricsDashboard({ services, isDemoMode = false }: MetricsDashboardProps) { + const liveServices = services.filter((s) => s.status === 'running'); -export function MetricsDashboard({ - isRunning = true, -}: MetricsDashboardProps) { - const [data, setData] = useState(generateInitialData); - const [timeRange, setTimeRange] = useState<'day' | 'month' | 'year'>('day'); + const metricsQueries = useQueries({ + queries: liveServices.map((service) => ({ + queryKey: ['service-metrics', service.id], + queryFn: () => getServiceMetrics(service.id), + enabled: !isDemoMode, + refetchInterval: 5000, + retry: false, + })), + }); + const samples = metricsQueries + .map((q) => q.data) + .filter((m): m is ServiceMetrics => Boolean(m) && m!.status === 'ok'); + + const totalCpu = samples.reduce((sum, m) => sum + m.cpu_percent, 0); + const avgCpu = samples.length > 0 ? totalCpu / samples.length : 0; + const memUsed = samples.reduce((sum, m) => sum + m.memory_usage_bytes, 0); + const memLimit = samples.reduce((sum, m) => sum + m.memory_limit_bytes, 0); + const memPct = memLimit > 0 ? (memUsed / memLimit) * 100 : 0; + const netRx = samples.reduce((sum, m) => sum + m.network_rx_bytes, 0); + const netTx = samples.reduce((sum, m) => sum + m.network_tx_bytes, 0); + const instanceCount = samples.reduce((sum, m) => sum + m.instances.length, 0); + + // Rolling history for the sparklines — accumulated from real samples. + const [cpuHistory, setCpuHistory] = useState([]); + const [netHistory, setNetHistory] = useState<{ rx: number[]; tx: number[] }>({ rx: [], tx: [] }); + const lastNetRef = useRef<{ rx: number; tx: number; at: number } | null>(null); + + const samplesKey = samples.map((s) => `${s.service_id}:${s.collected_at}`).join('|'); + const avgCpuRef = avgCpu; + const netRxRef = netRx; + const netTxRef = netTx; useEffect(() => { - if (!isRunning) return; + if (!samplesKey) return; + const now = Date.now(); + const prev = lastNetRef.current; + const elapsed = prev ? Math.max(1, (now - prev.at) / 1000) : 0; + const rxRate = prev && elapsed > 0 ? Math.max(0, (netRxRef - prev.rx) / elapsed) : 0; + const txRate = prev && elapsed > 0 ? Math.max(0, (netTxRef - prev.tx) / elapsed) : 0; + lastNetRef.current = { rx: netRxRef, tx: netTxRef, at: now }; + setCpuHistory((h) => [...h, Math.min(100, avgCpuRef)].slice(-24)); + setNetHistory((h) => ({ rx: [...h.rx, rxRate].slice(-24), tx: [...h.tx, txRate].slice(-24) })); + // eslint-disable-next-line react-hooks/exhaustive-deps -- keyed on the samples' collected_at signature + }, [samplesKey]); - const interval = setInterval(() => { - setData((prev) => { - const newCpu = [...prev.cpu.slice(1), getRandom(10, 45)]; - const newUsers = [...prev.users.slice(1), getRandom(60, 110)]; - const newPerf = [...prev.performance.slice(1), getRandom(82, 99)]; - const newPerfAlt = [...prev.performanceAlt.slice(1), getRandom(60, 85)]; - - return { - ...prev, - cpu: newCpu, - ram: getRandom(50, 85), - ramUsed: `${(8 * (prev.ram / 100)).toFixed(1)} GB`, - users: newUsers, - performance: newPerf, - performanceAlt: newPerfAlt, - upSpeed: parseFloat((Math.random() * 5 + 8).toFixed(1)), - downSpeed: parseFloat((Math.random() * 3 + 4).toFixed(1)), - }; - }); - }, 2000); - - return () => clearInterval(interval); - }, [isRunning]); - - const currentCpu = data.cpu[data.cpu.length - 1]; - const currentUsers = data.users[data.users.length - 1]; - const currentPerf = data.performance[data.performance.length - 1]; + const cpuHealth = healthLabel(avgCpu); + const memHealth = healthLabel(memPct); + const reporting = samples.length; + const noData = !isDemoMode && liveServices.length > 0 && reporting === 0; return (
- {/* Metrics Header - self.html exact match */} -
- Metrics -
- -
- {(['day', 'month', 'year'] as const).map((range) => ( -
setTimeRange(range)} - > - {range.charAt(0).toUpperCase() + range.slice(1)} -
- ))} +
+ Metrics + + {isDemoMode + ? 'sample data' + : reporting > 0 + ? `${reporting} service${reporting === 1 ? '' : 's'} reporting · every 5s` + : 'no running services'} + +
+ +
+ {/* CPU */} +
+
+ + CPU Usage
+
+ {avgCpu.toFixed(1)}% +
+
+ {cpuHealth.text} + {' '}avg across running services +
+
+ {cpuHistory.length > 1 ? ( + + ) : ( +
+ )} +
+
+ + {/* Memory */} +
+
+ + Memory +
+
+ {memPct.toFixed(0)}% +
+
+ {memHealth.text} + {' '}{formatBytes(memUsed)} used +
+
+ +
+
+ {formatBytes(memUsed)} / {memLimit > 0 ? formatBytes(memLimit) : '∞'} +
+
+ + {/* Network */} +
+
+ + Network +
+
+ {formatBytes(netRx)} +
+
+ RX + {' '}total · TX {formatBytes(netTx)} +
+
+ {netHistory.rx.length > 1 ? ( + + ) : ( +
+ )} +
+
+ {netHistory.rx.length > 0 + ? `↓ ${formatBytes(netHistory.rx[netHistory.rx.length - 1])}/s · ↑ ${formatBytes(netHistory.tx[netHistory.tx.length - 1])}/s` + : 'rate accumulating…'} +
+
+ + {/* Instances */} +
+
+ + Instances +
+
+ {isDemoMode ? liveServices.length : instanceCount} +
+
+ Running + {' '}containers across {liveServices.length} service{liveServices.length === 1 ? '' : 's'} +
+
    + {liveServices.slice(0, 4).map((service, i) => { + const m = samples.find((s) => s.service_id === service.id); + return ( +
  • + + + {service.name} + + + {m ? `${m.cpu_percent.toFixed(1)}%` : i < 3 ? '—' : ''} + +
  • + ); + })} + {liveServices.length > 4 && ( +
  • +{liveServices.length - 4} more
  • + )} +
- {/* Row 1: CPU, RAM, Cache - self.html exact: 13px gap */} -
- {/* CPU Card */} -
-
-
- CPU Usage -
-
{currentCpu}%
-
- - {currentCpu < 50 ? 'Good' : currentCpu < 75 ? 'Average' : 'High'} - {' '} - Daily usage -
-
- -
-
- Details -
- -
-
-
- - {/* RAM Card */} -
-
-
- RAM Usage -
-
{data.ram}%
-
- - {data.ram < 60 ? 'Good' : 'Average'} - {' '} - Daily usage -
-
- -
-
Used
-
{data.ramUsed} / 8GB
-
-
-
- Details -
- -
-
-
- - {/* Cache Card */} -
-
-
- Cache -
-
{data.cache} MB
-
- 220MB Average{' '} - cached images and files -
- - {/* Segmented Bar */} -
-
-
-
-
- - {/* Stats row */} -
-
-
-
- Cache -
-
- 212 MB - 12% -
-
-
-
-
-
- Non-Cache -
-
- 85.5 MB - 4% -
-
-
-
-
Total
-
1.75 GB
-
-
- -
- Details -
- -
-
-
-
- - {/* Row 2: Active Users, Performance - self.html exact: 13px gap */} -
- {/* Active Users - horizontal layout */} -
-
-
-
- Active User -
-
{currentUsers} K
-
User active right now
-
- {['🇨🇳', '🇮🇩', '🇲🇲', '🇲🇾', '🇯🇵', '🇮🇳', '🇰🇷', '🇵🇭'].map((flag, i) => ( - {flag} - ))} -
-
- Details -
- -
-
-
-
- -
-
- - {/* Performance Card */} -
-
-
- Performance -
-
-
-
{currentPerf}%
-
- 85 ? '#5ee6a0' : '#f2c94c', fontWeight: 700 }}> - {currentPerf > 85 ? 'Good' : 'Average'} - {' '} - Last scan -
-
-
-
- -
-
-
- - {data.upSpeed} Mbps -
-
- - {data.downSpeed} Mbps -
-
-
-
-
- Check Speed -
- -
-
-
-
+ {noData && ( +

+ Metrics collector is not reporting yet — check that the node agent is online. +

+ )}
); } diff --git a/app/frontend/src/features/workspace/components/SharedVariablesSection.tsx b/app/frontend/src/features/workspace/components/SharedVariablesSection.tsx index cffb65d..ced0c31 100644 --- a/app/frontend/src/features/workspace/components/SharedVariablesSection.tsx +++ b/app/frontend/src/features/workspace/components/SharedVariablesSection.tsx @@ -78,7 +78,7 @@ export function SharedVariablesSection({ projectId }: { projectId: string }) { ) : (
{rows.map((row, i) => ( -
+
updateRow(i, { key: e.target.value })} diff --git a/app/frontend/src/features/workspace/pages/ProjectWorkspacePage.tsx b/app/frontend/src/features/workspace/pages/ProjectWorkspacePage.tsx index 5d6da3c..dea68b1 100644 --- a/app/frontend/src/features/workspace/pages/ProjectWorkspacePage.tsx +++ b/app/frontend/src/features/workspace/pages/ProjectWorkspacePage.tsx @@ -765,7 +765,7 @@ export function ProjectWorkspacePage() {
)} -
+
{activeView === 'observability' && (
@@ -860,11 +860,7 @@ export function ProjectWorkspacePage() {
{/* Metrics Dashboard */} - 0} - onStop={() => toast.showToast('Services stopped', 'warning')} - onRestart={() => toast.showToast('Services restarted', 'success')} - /> + {/* Service List */}
diff --git a/app/frontend/src/lib/api-client.ts b/app/frontend/src/lib/api-client.ts index 22d19dd..2344e7f 100644 --- a/app/frontend/src/lib/api-client.ts +++ b/app/frontend/src/lib/api-client.ts @@ -492,11 +492,12 @@ export function getApiBaseUrl(): string { // pingServer hits the public /health route — any HTTP response proves // reachability, and no auth means no 401 noise in the connectivity probe. -export async function pingServer(): Promise { +export async function pingServer(): Promise { const res = await fetch(`${API_ROOT}/health`); if (!res.ok) { throw new Error(`Server returned ${res.status}`); } + return true; } export type SetupStatus = { diff --git a/app/frontend/src/main.tsx b/app/frontend/src/main.tsx index 55fd0c3..21e125e 100644 --- a/app/frontend/src/main.tsx +++ b/app/frontend/src/main.tsx @@ -23,6 +23,7 @@ const queryClient = new QueryClient({ // High-churn or bulky keys never hit localStorage — a stale log tail is // worse than none. const NEVER_PERSIST = new Set([ + 'auth-session', 'shell-notifications', 'notifications', 'service-logs', diff --git a/app/frontend/vite.config.ts b/app/frontend/vite.config.ts index 60f2eb8..4ce22db 100644 --- a/app/frontend/vite.config.ts +++ b/app/frontend/vite.config.ts @@ -59,8 +59,12 @@ export default defineConfig({ }, proxy: { // Same-origin API path for dev - matches the nginx proxy in the shipped image + '/health': { + target: process.env.VITE_API_PROXY ?? 'http://localhost:8082', + changeOrigin: true, + }, '/api': { - target: 'http://localhost:8082', + target: process.env.VITE_API_PROXY ?? 'http://localhost:8082', changeOrigin: true, ws: true, },