Initial commit: TaskPool React panel

- React frontend with route-level code splitting
- Backend rebranded from Baihu to TaskPool
- DB brand migration script and local compatibility
This commit is contained in:
2026-07-26 08:43:52 +08:00
commit e6956aa001
397 changed files with 73621 additions and 0 deletions
@@ -0,0 +1,110 @@
import { useCallback, useEffect, useState } from 'react'
import { MonitorDot } from 'lucide-react'
import {
activeInterconnectNodeId,
api,
setActiveInterconnectNodeId,
} from '@/api'
import { getNodes, type InterconnectNode } from '@/api/interconnect'
import { useEventBus } from '@/hooks/useEventBus'
import { SYSTEM_EVENTS } from '@/constants'
import { cn } from '@/lib/utils'
export function NodeSwitcher() {
const [nodes, setNodes] = useState<InterconnectNode[]>([])
const [selected, setSelected] = useState(activeInterconnectNodeId || 'local')
const [isMaster, setIsMaster] = useState(false)
const fetchNodes = useCallback(async () => {
try {
const res = await getNodes()
setNodes(Array.isArray(res) ? res : [])
} catch {
setNodes([])
}
}, [])
const updateRoleState = useCallback(
(role: string) => {
const master = role === 'master'
setIsMaster(master)
if (master) {
setSelected(activeInterconnectNodeId || 'local')
fetchNodes()
} else {
setNodes([])
setSelected('local')
const traveling = !!document.cookie.match(/(?:^| )active_interconnect_node_id=([^;]*)/)
if (activeInterconnectNodeId && !traveling) {
setActiveInterconnectNodeId('')
}
}
},
[fetchNodes],
)
useEffect(() => {
;(async () => {
try {
const role = await api.settings.get('interconnect', 'interconnect_role')
updateRoleState(role || 'none')
} catch {
updateRoleState('none')
}
})()
}, [updateRoleState])
useEventBus(SYSTEM_EVENTS.INTERCONNECT_ROLE_CHANGED, (role) => {
updateRoleState(String(role || 'none'))
})
useEffect(() => {
if (!isMaster) return
const t = window.setInterval(fetchNodes, 120000)
const onVis = () => {
if (document.visibilityState === 'visible') fetchNodes()
}
document.addEventListener('visibilitychange', onVis)
return () => {
clearInterval(t)
document.removeEventListener('visibilitychange', onVis)
}
}, [isMaster, fetchNodes])
if (!isMaster) return null
return (
<div
className={cn(
'inline-flex h-8 max-w-[150px] items-center gap-1 rounded-md border px-2 text-[11px]',
selected !== 'local'
? 'border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400'
: 'border-[var(--border)] bg-[var(--bg-secondary)] text-[var(--text-secondary)]',
)}
>
<MonitorDot size={12} className="shrink-0" />
<select
className="max-w-[120px] truncate border-0 bg-transparent text-[11px] outline-none"
value={selected}
onChange={(e) => {
const val = e.target.value
setSelected(val)
if (val === 'local') {
setActiveInterconnectNodeId('')
} else {
const name = nodes.find((n) => n.id === val)?.name || ''
setActiveInterconnectNodeId(val, name)
}
window.location.reload()
}}
>
<option value="local"></option>
{nodes.map((n) => (
<option key={n.id} value={n.id}>
{n.name}
</option>
))}
</select>
</div>
)
}