- apps/api: FastAPI backend with Dockerfile - apps/web: Next.js frontend with Dockerfile - apps/sensor: Signal collection agent - packages: shared packages Co-Authored-By: Claude <noreply@anthropic.com>
136 lines
4.0 KiB
TypeScript
136 lines
4.0 KiB
TypeScript
'use client'
|
||
|
||
/**
|
||
* ConnectionStatus - SSE 連線狀態指示器
|
||
* =====================================
|
||
* 顯示與後端 SSE 的連線狀態
|
||
*
|
||
* i18n: 100% 使用 useTranslations,禁止任何寫死字串
|
||
* 符合 AWOOOI 專案開發憲法 v2.0
|
||
*
|
||
* States:
|
||
* - disconnected: 灰色
|
||
* - connecting: 藍色 pulse
|
||
* - connected: 綠色
|
||
* - reconnecting: 橘色 pulse
|
||
* - error: 紅色
|
||
*/
|
||
|
||
import { useTranslations } from 'next-intl'
|
||
import { useLocale } from 'next-intl'
|
||
import { StatusOrb, type StatusType } from '@/components/ui/status-orb'
|
||
import { useConnectionStatus, useMockMode, useLastUpdate } from '@/stores/dashboard.store'
|
||
import { cn } from '@/lib/utils'
|
||
|
||
// =============================================================================
|
||
// Status Mapping
|
||
// =============================================================================
|
||
|
||
type ConnectionStatusType = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error'
|
||
|
||
const connectionToStatus: Record<ConnectionStatusType, StatusType> = {
|
||
disconnected: 'idle',
|
||
connecting: 'syncing',
|
||
connected: 'healthy',
|
||
reconnecting: 'warning',
|
||
error: 'critical',
|
||
}
|
||
|
||
// i18n key mapping (不再使用 hardcoded labels)
|
||
const connectionLabelKeys: Record<ConnectionStatusType, string> = {
|
||
disconnected: 'disconnected',
|
||
connecting: 'connecting',
|
||
connected: 'connected',
|
||
reconnecting: 'reconnecting',
|
||
error: 'error',
|
||
}
|
||
|
||
// =============================================================================
|
||
// Component
|
||
// =============================================================================
|
||
|
||
interface ConnectionStatusProps {
|
||
className?: string
|
||
showLabel?: boolean
|
||
showLastUpdate?: boolean
|
||
}
|
||
|
||
export function ConnectionStatus({
|
||
className,
|
||
showLabel = true,
|
||
showLastUpdate = false,
|
||
}: ConnectionStatusProps) {
|
||
const t = useTranslations('connection')
|
||
const tCommon = useTranslations('common')
|
||
const locale = useLocale()
|
||
const connectionStatus = useConnectionStatus()
|
||
const mockMode = useMockMode()
|
||
const lastUpdate = useLastUpdate()
|
||
|
||
const status = connectionToStatus[connectionStatus]
|
||
const labelKey = connectionLabelKeys[connectionStatus]
|
||
|
||
// Format last update time with dynamic locale
|
||
const lastUpdateStr = lastUpdate
|
||
? lastUpdate.toLocaleTimeString(locale === 'zh-TW' ? 'zh-TW' : 'en-US', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
})
|
||
: '--:--:--'
|
||
|
||
return (
|
||
<div className={cn('flex items-center gap-2', className)}>
|
||
<StatusOrb
|
||
status={status}
|
||
size="sm"
|
||
pulse={connectionStatus === 'connecting' || connectionStatus === 'reconnecting'}
|
||
glow={connectionStatus === 'connected'}
|
||
/>
|
||
|
||
{showLabel && (
|
||
<div className="flex flex-col">
|
||
<span className="text-xs font-mono text-nothing-gray-600">
|
||
{t(labelKey)}
|
||
</span>
|
||
{showLastUpdate && lastUpdate && (
|
||
<span className="text-[10px] font-mono text-nothing-gray-400">
|
||
{lastUpdateStr}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{mockMode && (
|
||
<span className="px-1.5 py-0.5 text-[10px] font-mono bg-status-warning/10 text-status-warning rounded">
|
||
{t('mockMode')}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// =============================================================================
|
||
// Compact Version (for header)
|
||
// =============================================================================
|
||
|
||
export function ConnectionStatusCompact({ className }: { className?: string }) {
|
||
const t = useTranslations('connection')
|
||
const connectionStatus = useConnectionStatus()
|
||
const status = connectionToStatus[connectionStatus]
|
||
|
||
return (
|
||
<div
|
||
className={cn(
|
||
'flex items-center gap-2 px-3 py-1.5 rounded-lg bg-nothing-gray-100',
|
||
className
|
||
)}
|
||
>
|
||
<StatusOrb status={status} size="sm" pulse={connectionStatus !== 'connected'} />
|
||
<span className="font-mono text-xs text-nothing-gray-600 uppercase">
|
||
{t(connectionLabelKeys[connectionStatus])}
|
||
</span>
|
||
</div>
|
||
)
|
||
}
|