Add ping endpoint, update-refresh prompt, and dark-mode polish
All checks were successful
CI/CD Pipeline / deploy (push) Successful in 26s
CI/CD Pipeline / promote_stable (push) Successful in 2s

This commit is contained in:
2026-02-24 18:48:54 +01:00
parent 2a69ee8258
commit 0b4e0df305
9 changed files with 218 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { BrowserRouter, Link, Route, Routes, useLocation } from 'react-router-dom'
import { Container, NavDropdown } from 'react-bootstrap'
import { Button, Container, Modal, NavDropdown } from 'react-bootstrap'
import { AuthProvider, useAuth } from './context/AuthContext'
import Home from './pages/Home'
import ForumView from './pages/ForumView'
@@ -13,7 +13,7 @@ import BoardIndex from './pages/BoardIndex'
import Ucp from './pages/Ucp'
import Profile from './pages/Profile'
import { useTranslation } from 'react-i18next'
import { fetchSettings, fetchVersion, getForum, getThread } from './api/client'
import { fetchPing, fetchSettings, fetchVersion, getForum, getThread } from './api/client'
function PortalHeader({
userMenu,
@@ -241,9 +241,16 @@ function PortalHeader({
}
function AppShell() {
const PING_INTERVAL_MS = 15000
const PING_INTERVAL_HIDDEN_MS = 60000
const { t } = useTranslation()
const { token, email, userId, logout, isAdmin, isModerator } = useAuth()
const [versionInfo, setVersionInfo] = useState(null)
const [availableBuild, setAvailableBuild] = useState(null)
const [pingBuild, setPingBuild] = useState(null)
const [showUpdateModal, setShowUpdateModal] = useState(false)
const currentBuildRef = useRef(null)
const promptedBuildRef = useRef(null)
const [theme, setTheme] = useState('auto')
const [resolvedTheme, setResolvedTheme] = useState('light')
const [accentOverride, setAccentOverride] = useState(
@@ -272,6 +279,73 @@ function AppShell() {
.catch(() => setVersionInfo(null))
}, [])
useEffect(() => {
currentBuildRef.current =
typeof versionInfo?.build === 'number' ? versionInfo.build : null
}, [versionInfo?.build])
useEffect(() => {
const currentBuild =
typeof versionInfo?.build === 'number' ? versionInfo.build : null
if (currentBuild !== null && pingBuild !== null && pingBuild > currentBuild) {
setAvailableBuild(pingBuild)
return
}
setAvailableBuild(null)
}, [versionInfo?.build, pingBuild])
useEffect(() => {
if (availableBuild === null) return
if (promptedBuildRef.current === availableBuild) return
promptedBuildRef.current = availableBuild
setShowUpdateModal(true)
}, [availableBuild])
useEffect(() => {
let active = true
let timeoutId = null
const scheduleNext = () => {
const delay = document.hidden ? PING_INTERVAL_HIDDEN_MS : PING_INTERVAL_MS
timeoutId = window.setTimeout(runPing, delay)
}
const runPing = async () => {
try {
const data = await fetchPing()
const currentBuild = currentBuildRef.current
const remoteBuild =
typeof data?.version_status?.build === 'number'
? data.version_status.build
: null
console.log('speedBB ping', {
...data,
current_version: currentBuild,
})
if (!active) return
if (remoteBuild !== null) {
setPingBuild(remoteBuild)
}
window.dispatchEvent(new CustomEvent('speedbb-ping', { detail: data }))
} catch {
// ignore transient ping failures
} finally {
if (active) {
scheduleNext()
}
}
}
runPing()
return () => {
active = false
if (timeoutId) {
window.clearTimeout(timeoutId)
}
}
}, [])
useEffect(() => {
let active = true
const loadSettings = async () => {
@@ -495,8 +569,35 @@ function AppShell() {
<span className="bb-version-label">)</span>
</span>
)}
{availableBuild !== null && (
<Button
type="button"
size="sm"
className="bb-accent-button"
onClick={() => window.location.reload()}
>
{t('version.update_available_short')} (build {availableBuild}) ·{' '}
{t('version.update_now')}
</Button>
)}
</div>
</footer>
<Modal show={showUpdateModal} onHide={() => setShowUpdateModal(false)} centered>
<Modal.Header closeButton>
<Modal.Title>{t('version.refresh_prompt_title')}</Modal.Title>
</Modal.Header>
<Modal.Body>
{t('version.refresh_prompt_body', { build: availableBuild ?? '-' })}
</Modal.Body>
<Modal.Footer className="justify-content-between">
<Button variant="outline-secondary" onClick={() => setShowUpdateModal(false)}>
{t('version.remind_later')}
</Button>
<Button className="bb-accent-button" onClick={() => window.location.reload()}>
{t('version.update_now')}
</Button>
</Modal.Footer>
</Modal>
</div>
)
}