Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/dashboard-overflow-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const SYSTEM_ITEMS: Array<OverflowItem> = [
const GATEWAY_ITEMS: Array<OverflowItem> = [
{ icon: MessageMultiple01Icon, label: 'Chat', to: '/chat' },
{ icon: Rocket01Icon, label: 'Conductor', to: '/conductor' },
{ icon: ClipboardIcon, label: 'Operations', to: '/tasks' },
{ icon: ClipboardIcon, label: 'Operations', to: '/operations' },
{ icon: ServerStack01Icon, label: 'Channels', to: '/channels' },
{ icon: ChartLineData02Icon, label: 'Costs', to: '/costs' },
]
Expand Down
160 changes: 5 additions & 155 deletions src/components/gateway-connection-banner.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
'use client'

import { useEffect, useRef, useState } from 'react'

Check failure on line 3 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

'useState' is declared but its value is never read.

Check failure on line 3 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

'useRef' is declared but its value is never read.
import { AnimatePresence, motion } from 'motion/react'

Check failure on line 4 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

All imports in import declaration are unused.
import {
Alert02Icon,

Check failure on line 6 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

'Alert02Icon' is declared but its value is never read.
Cancel01Icon,

Check failure on line 7 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

'Cancel01Icon' is declared but its value is never read.
RefreshIcon,
} from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
pingGateway,

Check failure on line 14 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

'pingGateway' is declared but its value is never read.
useGatewaySetupStore,
} from '@/hooks/use-gateway-setup'
import { getConnectionErrorInfo } from '@/lib/connection-errors'
import { cn } from '@/lib/utils'

const HEALTH_CHECK_INTERVAL_MS = 10_000

Check failure on line 20 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

'HEALTH_CHECK_INTERVAL_MS' is declared but its value is never read.
const HEALTH_CHECK_DELAY_MS = 2_000

Check failure on line 21 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

'HEALTH_CHECK_DELAY_MS' is declared but its value is never read.
const REQUIRED_FAILURES = 2

Check failure on line 22 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

'REQUIRED_FAILURES' is declared but its value is never read.
const DISMISS_STORAGE_KEY = 'clawsuite-gateway-banner-dismissed-until'

Check failure on line 23 in src/components/gateway-connection-banner.tsx

View workflow job for this annotation

GitHub Actions / Build & Lint

'DISMISS_STORAGE_KEY' is declared but its value is never read.
const DISMISS_TTL_MS = 60 * 60 * 1000

type GatewayConnectionSetupFormProps = {
Expand Down Expand Up @@ -152,159 +152,9 @@
}

export function GatewayConnectionBanner() {
const initialize = useGatewaySetupStore((state) => state.initialize)
const loadCurrentConfig = useGatewaySetupStore((state) => state.loadCurrentConfig)
const saveAndTest = useGatewaySetupStore((state) => state.saveAndTest)
const setupConfigured = useGatewaySetupStore((state) => state.setupConfigured)
const testStatus = useGatewaySetupStore((state) => state.testStatus)
const testError = useGatewaySetupStore((state) => state.testError)
const saving = useGatewaySetupStore((state) => state.saving)

const [healthState, setHealthState] = useState<'unknown' | 'healthy' | 'unhealthy'>('unknown')
const [dismissed, setDismissed] = useState(false)
const consecutiveFailuresRef = useRef(0)
const wasUnhealthyRef = useRef(false)
const errorInfo = getConnectionErrorInfo(testError)
const isReconnecting = setupConfigured && (saving || testStatus === 'testing')

useEffect(() => {
void initialize()
}, [initialize])

useEffect(() => {
if (typeof window === 'undefined') return

try {
const dismissedUntil = Number(localStorage.getItem(DISMISS_STORAGE_KEY) ?? '0')
if (dismissedUntil > Date.now()) {
setDismissed(true)
return
}
localStorage.removeItem(DISMISS_STORAGE_KEY)
} catch {
localStorage.removeItem(DISMISS_STORAGE_KEY)
}

setDismissed(false)
}, [])

useEffect(() => {
let mounted = true

async function checkHealth() {
const { ok } = await pingGateway()
if (!mounted) return

if (ok) {
consecutiveFailuresRef.current = 0
setHealthState('healthy')
if (wasUnhealthyRef.current && typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('gateway:health-restored'))
}
wasUnhealthyRef.current = false
setDismissed(false)
if (typeof window !== 'undefined') {
localStorage.removeItem(DISMISS_STORAGE_KEY)
}
return
}

consecutiveFailuresRef.current += 1
if (consecutiveFailuresRef.current >= REQUIRED_FAILURES) {
wasUnhealthyRef.current = true
setHealthState('unhealthy')
}
}

const initialTimer = window.setTimeout(() => {
void checkHealth()
}, HEALTH_CHECK_DELAY_MS)
const interval = window.setInterval(() => {
void checkHealth()
}, HEALTH_CHECK_INTERVAL_MS)

return () => {
mounted = false
window.clearTimeout(initialTimer)
window.clearInterval(interval)
}
}, [setupConfigured])

async function handleReconnect() {
await loadCurrentConfig()
await saveAndTest()
}

function handleDismiss() {
if (typeof window !== 'undefined') {
localStorage.setItem(
DISMISS_STORAGE_KEY,
String(Date.now() + DISMISS_TTL_MS),
)
}
setDismissed(true)
}

const showBanner = healthState === 'unhealthy' && !dismissed

return (
<AnimatePresence initial={false}>
{showBanner ? (
<motion.div
key="gateway-connection-banner"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 12 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="fixed right-4 z-[90] w-[calc(100vw-2rem)] max-w-72 bottom-[calc(var(--tabbar-h,0px)+1rem)] sm:bottom-4"
>
<div className="rounded-xl border border-amber-300 bg-amber-100/95 px-3 py-2.5 text-primary-900 shadow-lg">
<div className="flex items-start gap-2">
<HugeiconsIcon
icon={isReconnecting ? RefreshIcon : Alert02Icon}
size={18}
strokeWidth={1.7}
className={cn(
'mt-0.5 shrink-0 text-amber-700',
isReconnecting ? 'animate-spin' : '',
)}
/>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-amber-950">
{isReconnecting
? 'Gateway reconnecting...'
: '⚠ Gateway offline · Chat unavailable'}
</p>
{!isReconnecting && testStatus === 'error' && testError ? (
<p className="mt-1 text-[11px] text-amber-800">
{errorInfo.title}. {errorInfo.description}
</p>
) : null}
<div className="mt-2 flex items-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={() => void handleReconnect()}
disabled={isReconnecting}
className="h-7 border-amber-300 bg-amber-50 px-2 text-xs text-amber-900 hover:bg-amber-200"
>
<HugeiconsIcon icon={RefreshIcon} size={14} strokeWidth={1.6} />
Reconnect
</Button>
</div>
</div>
<button
type="button"
onClick={handleDismiss}
className="rounded-md p-1 text-amber-800 transition-colors hover:bg-amber-200"
aria-label="Dismiss gateway connection banner"
>
<HugeiconsIcon icon={Cancel01Icon} size={14} strokeWidth={1.8} />
</button>
</div>
</div>
</motion.div>
) : null}
</AnimatePresence>
)
// Temporarily disabled: current health probe is too noisy and produces
// false offline/chat-unavailable banners even while the app is usable.
// Re-enable after the gateway health logic is rewritten to distinguish
// slow usage/session RPCs from actual gateway loss.
return null
}
2 changes: 1 addition & 1 deletion src/components/mobile-tab-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const TABS: TabItem[] = [
label: 'Conductor',
icon: Rocket01Icon,
to: '/conductor',
match: (p) => p.startsWith('/conductor') || p.startsWith('/agent-swarm') || p.startsWith('/agents'),
match: (p) => p.startsWith('/conductor') || p.startsWith('/agent-swarm') || p.startsWith('/agents') || p.startsWith('/operations'),
},
{
id: 'chat',
Expand Down
13 changes: 9 additions & 4 deletions src/components/system-metrics-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,15 @@ async function fetchMetrics(): Promise<SystemMetrics> {
return res.json() as Promise<SystemMetrics>
}

export function SystemMetricsFooter() {
type SystemMetricsFooterProps = {
enabled?: boolean
}

export function SystemMetricsFooter({ enabled = true }: SystemMetricsFooterProps) {
const [metrics, setMetrics] = useState<SystemMetrics | null>(null)

useEffect(() => {
if (!enabled) return undefined
let cancelled = false

async function poll() {
Expand All @@ -64,14 +69,14 @@ export function SystemMetricsFooter() {
}

void poll()
const id = setInterval(() => void poll(), 5_000)
const id = setInterval(() => void poll(), 30_000)
return () => {
cancelled = true
clearInterval(id)
}
}, [])
}, [enabled])

if (!metrics) return null
if (!enabled || !metrics) return null

const { cpu, ramUsed, ramTotal, diskPercent, uptime, gatewayConnected } = metrics

Expand Down
Loading
Loading