diff --git a/src/App.css b/src/App.css
index 69ff659..42bd416 100644
--- a/src/App.css
+++ b/src/App.css
@@ -59,6 +59,11 @@ h2 {
color: #cbd5e1;
}
+.refreshing-indicator {
+ color: #93c5fd;
+ font-size: 0.9rem;
+}
+
.banner {
padding: 0.9rem 1rem;
margin-bottom: 1rem;
diff --git a/src/App.jsx b/src/App.jsx
index 9125536..e3945ef 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,10 +1,8 @@
-import { useCallback, useEffect, useMemo, useState } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import './App.css'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://app-01:8080'
-const WS_URL = import.meta.env.VITE_WS_URL || ''
const POLL_INTERVAL_MS = 10000
-const RECONNECT_DELAY_MS = 5000
const statusColors = {
queued: 'status-queued',
@@ -68,95 +66,68 @@ function DetailRow({ label, value, mono = false, preserve = false }) {
function App() {
const [items, setItems] = useState([])
const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
const [error, setError] = useState('')
const [sort, setSort] = useState({ key: 'created_at', direction: 'desc' })
const [filters, setFilters] = useState(defaultFilters)
const [selectedId, setSelectedId] = useState('')
- const [transport, setTransport] = useState(WS_URL ? 'connecting websocket…' : 'polling')
+ const [transport] = useState('incremental polling')
+ const lastSuccessRef = useRef('')
+
+ const mergeItems = useCallback((incomingItems) => {
+ setItems((current) => {
+ if (!incomingItems.length) return current
+ const next = new Map(current.map((item) => [item.id, item]))
+ incomingItems.forEach((item) => next.set(item.id, item))
+ return Array.from(next.values())
+ })
+ }, [])
+
+ const refreshTimestamp = useCallback((sourceItems) => {
+ const timestamps = sourceItems.map((item) => item.updated_at).filter(Boolean).sort()
+ if (timestamps.length) {
+ lastSuccessRef.current = timestamps[timestamps.length - 1]
+ }
+ }, [])
+
+ const load = useCallback(async (incremental = false) => {
+ if (incremental) setRefreshing(true)
- const load = useCallback(async () => {
try {
- const response = await fetch(`${API_BASE_URL}/work`)
+ const url = new URL(`${API_BASE_URL}/work`)
+ if (incremental && lastSuccessRef.current) {
+ url.searchParams.set('since', lastSuccessRef.current)
+ }
+
+ const response = await fetch(url)
if (!response.ok) throw new Error(`Request failed with ${response.status}`)
const data = await response.json()
- setItems(data)
+
+ if (incremental && lastSuccessRef.current) {
+ mergeItems(data)
+ } else {
+ setItems(data)
+ }
+
+ refreshTimestamp(data.length ? data : items)
setError('')
setSelectedId((current) => {
- if (current && data.some((item) => item.id === current)) return current
- return data[0]?.id ?? ''
+ const pool = data.length && incremental ? [...items, ...data] : data.length ? data : items
+ if (current && pool.some((item) => item.id === current)) return current
+ return pool[0]?.id ?? ''
})
} catch (err) {
setError(err.message || 'Failed to load queue')
} finally {
setLoading(false)
+ setRefreshing(false)
}
- }, [])
+ }, [items, mergeItems, refreshTimestamp])
useEffect(() => {
- let active = true
- let socket
- let pollTimer
- let reconnectTimer
-
- const startPolling = () => {
- setTransport('polling')
- load()
- pollTimer = window.setInterval(load, POLL_INTERVAL_MS)
- }
-
- const stopPolling = () => {
- if (pollTimer) window.clearInterval(pollTimer)
- }
-
- const connectWebSocket = () => {
- if (!WS_URL || !active) {
- startPolling()
- return
- }
-
- try {
- socket = new WebSocket(WS_URL)
- } catch {
- startPolling()
- return
- }
-
- socket.onopen = () => {
- if (!active) return
- stopPolling()
- setTransport('websocket')
- load()
- }
-
- socket.onmessage = () => {
- if (!active) return
- load()
- }
-
- socket.onerror = () => {
- if (!active) return
- setTransport('polling')
- }
-
- socket.onclose = () => {
- if (!active) return
- startPolling()
- reconnectTimer = window.setTimeout(() => {
- stopPolling()
- connectWebSocket()
- }, RECONNECT_DELAY_MS)
- }
- }
-
- load()
- connectWebSocket()
-
- return () => {
- active = false
- stopPolling()
- if (reconnectTimer) window.clearTimeout(reconnectTimer)
- if (socket && socket.readyState < 2) socket.close()
- }
+ load(false)
+ const timer = window.setInterval(() => load(true), POLL_INTERVAL_MS)
+ return () => window.clearInterval(timer)
}, [load])
const filterOptions = useMemo(
@@ -214,6 +185,7 @@ function App() {
{filteredItems.length} visible items
{items.length} total items
Updates via {transport}
+ {refreshing && Refreshing…}