From 0b4e0df305f9b5b953db51b2d3c8f83b7ad4d44e Mon Sep 17 00:00:00 2001 From: tracer Date: Tue, 24 Feb 2026 18:48:54 +0100 Subject: [PATCH] Add ping endpoint, update-refresh prompt, and dark-mode polish --- CHANGELOG.md | 6 ++ app/Http/Controllers/PingController.php | 23 +++++ resources/js/App.jsx | 107 +++++++++++++++++++++++- resources/js/api/client.js | 9 ++ resources/js/index.css | 63 ++++++++++---- resources/lang/de.json | 3 + resources/lang/en.json | 3 + routes/web.php | 3 + tests/Feature/PingControllerTest.php | 18 ++++ 9 files changed, 218 insertions(+), 17 deletions(-) create mode 100644 app/Http/Controllers/PingController.php create mode 100644 tests/Feature/PingControllerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0b460..9b17d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ - Implemented token-based new-password submission (`?token=...&email=...`) wired to `POST /api/reset-password`. - Updated reset flow UX to return to `/login` after successful reset-link request and after successful password update. - Added English and German translations for password reset screens/messages. +- Added new `/ping` endpoint returning connection status, build status, and notification state. +- Added frontend ping polling with active/hidden intervals and console diagnostics. +- Added update-available detection comparing loaded build vs ping build, with footer refresh CTA. +- Added update info modal prompting users to refresh when a newer build is detected. +- Tuned global dark mode palette to reduce overly bright text/surfaces in dark theme. +- Refined accent button state styling (hover/active/focus) to avoid Bootstrap blue fallback and preserve contrast. ## 2026-02-18 - Added CLI default PHP version detection to system status (`php_default_version`) using the CLI `php` binary. diff --git a/app/Http/Controllers/PingController.php b/app/Http/Controllers/PingController.php new file mode 100644 index 0000000..993322e --- /dev/null +++ b/app/Http/Controllers/PingController.php @@ -0,0 +1,23 @@ +where('key', 'build')->value('value'); + $reportedBuild = $build !== null ? ((int) $build) + 1 : 1; + + return response()->json([ + 'connect' => 'ok', + 'version_status' => [ + 'build' => $reportedBuild, + ], + 'notification_state' => false, + ]); + } +} diff --git a/resources/js/App.jsx b/resources/js/App.jsx index 9fe833b..423ad0b 100644 --- a/resources/js/App.jsx +++ b/resources/js/App.jsx @@ -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() { ) )} + {availableBuild !== null && ( + + )} + setShowUpdateModal(false)} centered> + + {t('version.refresh_prompt_title')} + + + {t('version.refresh_prompt_body', { build: availableBuild ?? '-' })} + + + + + + ) } diff --git a/resources/js/api/client.js b/resources/js/api/client.js index d3ace48..d3f89c1 100644 --- a/resources/js/api/client.js +++ b/resources/js/api/client.js @@ -129,6 +129,15 @@ export async function fetchVersion() { return apiFetch('/version') } +export async function fetchPing() { + const response = await fetch('/ping', { + headers: { + Accept: 'application/json', + }, + }) + return parseResponse(response) +} + export async function fetchVersionCheck() { return apiFetch('/version/check') } diff --git a/resources/js/index.css b/resources/js/index.css index 213a76a..cdcd0ae 100644 --- a/resources/js/index.css +++ b/resources/js/index.css @@ -862,10 +862,17 @@ a { } [data-bs-theme="dark"] { - --bb-ink: #aaaeb4; - --bb-ink-muted: #6b7483; - --bb-border: #2a2f3a; - --bb-page-bg: radial-gradient(circle at 10% 20%, #141823 0%, #10131a 45%, #0b0e14 100%); + --bb-ink: #a3acb9; + --bb-ink-muted: #626d7e; + --bb-border: #242a35; + --bb-page-bg: radial-gradient(circle at 10% 20%, #10151f 0%, #0b1018 45%, #080b11 100%); + --bs-body-bg: #080b11; + --bs-body-color: #b3bdca; + --bs-secondary-bg: #121822; + --bs-tertiary-bg: #0f141d; + --bs-border-color: #242a35; + --bs-modal-bg: #121822; + --bs-modal-color: #b3bdca; } [data-bs-theme="dark"] .bb-hero { @@ -875,11 +882,30 @@ a { [data-bs-theme="dark"] .bb-card, [data-bs-theme="dark"] .bb-form { - background: #171b22; - border-color: #2a2f3a; + background: #121822; + border-color: #242a35; box-shadow: 0 12px 24px rgba(0, 0, 0, 0.35); } +[data-bs-theme="dark"] .modal-content { + background: #121822; + border-color: #242a35; + color: #b3bdca; +} + +[data-bs-theme="dark"] .modal-content .modal-body, +[data-bs-theme="dark"] .modal-content .modal-footer { + background: #121822; +} + +[data-bs-theme="dark"] .modal-content .modal-header { + color: #b7c0cc; +} + +[data-bs-theme="dark"] .modal-content .modal-title { + color: #b7c0cc; +} + [data-bs-theme="light"] .bb-forum-row { background: #fff; } @@ -2172,16 +2198,25 @@ a { } .bb-accent-button { - background: var(--bb-accent, #f29b3f); - border-color: var(--bb-accent, #f29b3f); - color: #0e121b; + --bs-btn-bg: var(--bb-accent, #f29b3f); + --bs-btn-border-color: var(--bb-accent, #f29b3f); + --bs-btn-color: #0e121b; + --bs-btn-hover-bg: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000); + --bs-btn-hover-border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000); + --bs-btn-hover-color: #fff; + --bs-btn-active-bg: color-mix(in srgb, var(--bb-accent, #f29b3f) 80%, #000); + --bs-btn-active-border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 80%, #000); + --bs-btn-active-color: #fff; + --bs-btn-disabled-bg: var(--bb-accent, #f29b3f); + --bs-btn-disabled-border-color: var(--bb-accent, #f29b3f); + --bs-btn-disabled-color: #0e121b; + background: var(--bs-btn-bg); + border-color: var(--bs-btn-border-color); + color: var(--bs-btn-color); } -.bb-accent-button:hover, -.bb-accent-button:focus { - background: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000); - border-color: color-mix(in srgb, var(--bb-accent, #f29b3f) 85%, #000); - color: #0e121b; +.bb-accent-button:focus-visible { + box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--bb-accent, #f29b3f) 35%, transparent); } .bb-accent-button:disabled, diff --git a/resources/lang/de.json b/resources/lang/de.json index 96fdf73..13687d3 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -186,6 +186,9 @@ "version.up_to_date": "Aktuell", "version.update_available": "Update verfügbar (v{{version}})", "version.update_available_short": "Update verfügbar", + "version.refresh_prompt_title": "Update verfuegbar", + "version.refresh_prompt_body": "Ein neuerer Build ({{build}}) ist verfuegbar. Jetzt neu laden, um die aktuelle Version zu verwenden?", + "version.remind_later": "Spaeter", "version.unknown": "Version unbekannt", "version.update_now": "Jetzt aktualisieren", "version.update_title": "System aktualisieren", diff --git a/resources/lang/en.json b/resources/lang/en.json index df87488..2cf8e89 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -176,6 +176,9 @@ "version.up_to_date": "Up to date", "version.update_available": "Update available (v{{version}})", "version.update_available_short": "Update available", + "version.refresh_prompt_title": "Update available", + "version.refresh_prompt_body": "A newer build ({{build}}) is available. Refresh now to load the latest version?", + "version.remind_later": "Later", "version.unknown": "Version unknown", "version.update_now": "Update now", "version.update_title": "Update system", diff --git a/routes/web.php b/routes/web.php index 0d9bc31..2f99a3a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ name('password.reset'); +Route::get('/ping', PingController::class); + Route::get('/{any}', function () { if (!file_exists(base_path('.env'))) { return redirect('/install'); diff --git a/tests/Feature/PingControllerTest.php b/tests/Feature/PingControllerTest.php new file mode 100644 index 0000000..566b256 --- /dev/null +++ b/tests/Feature/PingControllerTest.php @@ -0,0 +1,18 @@ + 'build'], ['value' => '1337']); + + $response = $this->getJson('/ping'); + + $response->assertOk(); + $response->assertJson([ + 'connect' => 'ok', + 'version_status' => [ + 'build' => 1337, + ], + 'notification_state' => false, + ]); +});