diff --git a/.env.example b/.env.example index 2fd3396..03e5a09 100644 --- a/.env.example +++ b/.env.example @@ -216,6 +216,11 @@ WEB_PORT=8080 # Supported: en (see src/meshcore_hub/web/static/locales/ for available translations) # WEB_LOCALE=en +# Auto-refresh interval in seconds for list pages (nodes, advertisements, messages) +# Set to 0 to disable auto-refresh +# Default: 30 +# WEB_AUTO_REFRESH_SECONDS=30 + # Enable admin interface at /a/ (requires auth proxy in front) # Default: false # WEB_ADMIN_ENABLED=false diff --git a/AGENTS.md b/AGENTS.md index e19bbd9..cd94a80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -606,6 +606,7 @@ Key variables: - `API_READ_KEY`, `API_ADMIN_KEY` - API authentication keys - `WEB_ADMIN_ENABLED` - Enable admin interface at /a/ (default: `false`, requires auth proxy) - `WEB_THEME` - Default theme for the web dashboard (default: `dark`, options: `dark`, `light`). Users can override via the theme toggle in the navbar, which persists their preference in browser localStorage. +- `WEB_AUTO_REFRESH_SECONDS` - Auto-refresh interval in seconds for list pages (default: `30`, `0` to disable) - `TZ` - Timezone for web dashboard date/time display (default: `UTC`, e.g., `America/New_York`, `Europe/London`) - `FEATURE_DASHBOARD`, `FEATURE_NODES`, `FEATURE_ADVERTISEMENTS`, `FEATURE_MESSAGES`, `FEATURE_MAP`, `FEATURE_MEMBERS`, `FEATURE_PAGES` - Feature flags to enable/disable specific web dashboard pages (default: all `true`). Dependencies: Dashboard auto-disables when all of Nodes/Advertisements/Messages are disabled. Map auto-disables when Nodes is disabled. - `LOG_LEVEL` - Logging verbosity diff --git a/README.md b/README.md index 8a50104..c49b7c5 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,7 @@ The collector automatically cleans up old event data and inactive nodes: | `API_KEY` | *(none)* | API key for web dashboard queries (optional) | | `WEB_THEME` | `dark` | Default theme (`dark` or `light`). Users can override via theme toggle in navbar. | | `WEB_LOCALE` | `en` | Locale/language for the web dashboard (e.g., `en`, `es`, `fr`) | +| `WEB_AUTO_REFRESH_SECONDS` | `30` | Auto-refresh interval in seconds for list pages (0 to disable) | | `WEB_ADMIN_ENABLED` | `false` | Enable admin interface at /a/ (requires auth proxy) | | `TZ` | `UTC` | Timezone for displaying dates/times (e.g., `America/New_York`, `Europe/London`) | | `NETWORK_DOMAIN` | *(none)* | Network domain name (optional) | diff --git a/src/meshcore_hub/common/config.py b/src/meshcore_hub/common/config.py index f21f545..96e732e 100644 --- a/src/meshcore_hub/common/config.py +++ b/src/meshcore_hub/common/config.py @@ -268,6 +268,13 @@ class WebSettings(CommonSettings): description="Locale/language for the web dashboard (e.g. 'en')", ) + # Auto-refresh interval for list pages + web_auto_refresh_seconds: int = Field( + default=30, + description="Auto-refresh interval in seconds for list pages (0 to disable)", + ge=0, + ) + # Admin interface (disabled by default for security) web_admin_enabled: bool = Field( default=False, diff --git a/src/meshcore_hub/web/app.py b/src/meshcore_hub/web/app.py index 26a8555..bb75ab8 100644 --- a/src/meshcore_hub/web/app.py +++ b/src/meshcore_hub/web/app.py @@ -117,6 +117,7 @@ def _build_config_json(app: FastAPI, request: Request) -> str: "is_authenticated": bool(request.headers.get("X-Forwarded-User")), "default_theme": app.state.web_theme, "locale": app.state.web_locale, + "auto_refresh_seconds": app.state.auto_refresh_seconds, } return json.dumps(config) @@ -184,6 +185,9 @@ def create_app( app.state.web_locale = settings.web_locale or "en" load_locale(app.state.web_locale) + # Auto-refresh interval + app.state.auto_refresh_seconds = settings.web_auto_refresh_seconds + # Store configuration in app state (use args if provided, else settings) app.state.web_theme = ( settings.web_theme if settings.web_theme in ("dark", "light") else "dark" diff --git a/src/meshcore_hub/web/static/js/spa/auto-refresh.js b/src/meshcore_hub/web/static/js/spa/auto-refresh.js new file mode 100644 index 0000000..5ef6ba7 --- /dev/null +++ b/src/meshcore_hub/web/static/js/spa/auto-refresh.js @@ -0,0 +1,87 @@ +/** + * Auto-refresh utility for list pages. + * + * Reads `auto_refresh_seconds` from the app config. When the interval is > 0 + * it sets up a periodic timer that calls the provided `fetchAndRender` callback + * and renders a pause/play toggle button into the given container element. + */ + +import { html, litRender, getConfig, t } from './components.js'; + +/** + * Create an auto-refresh controller. + * + * @param {Object} options + * @param {Function} options.fetchAndRender - Async function that fetches data and re-renders the page. + * @param {HTMLElement} options.toggleContainer - Element to render the pause/play toggle into. + * @returns {{ cleanup: Function }} cleanup function to stop the timer. + */ +export function createAutoRefresh({ fetchAndRender, toggleContainer }) { + const config = getConfig(); + const intervalSeconds = config.auto_refresh_seconds || 0; + + if (!intervalSeconds || !toggleContainer) { + return { cleanup() {} }; + } + + let paused = false; + let isPending = false; + let timerId = null; + + function renderToggle() { + const pauseIcon = html``; + const playIcon = html``; + + const tooltip = paused ? t('auto_refresh.resume') : t('auto_refresh.pause'); + const icon = paused ? playIcon : pauseIcon; + + litRender(html` + + `, toggleContainer); + } + + function onToggle() { + paused = !paused; + if (paused) { + clearInterval(timerId); + timerId = null; + } else { + startTimer(); + } + renderToggle(); + } + + async function tick() { + if (isPending || paused) return; + isPending = true; + try { + await fetchAndRender(); + } catch (_e) { + // Errors are handled inside fetchAndRender; don't stop the timer. + } finally { + isPending = false; + } + } + + function startTimer() { + timerId = setInterval(tick, intervalSeconds * 1000); + } + + // Initial render and start + renderToggle(); + startTimer(); + + return { + cleanup() { + if (timerId) { + clearInterval(timerId); + timerId = null; + } + }, + }; +} diff --git a/src/meshcore_hub/web/static/js/spa/pages/advertisements.js b/src/meshcore_hub/web/static/js/spa/pages/advertisements.js index 3ad4841..45abf16 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/advertisements.js +++ b/src/meshcore_hub/web/static/js/spa/pages/advertisements.js @@ -5,6 +5,7 @@ import { truncateKey, errorAlert, pagination, createFilterHandler, autoSubmit, submitOnEnter, copyToClipboard, renderNodeDisplay } from '../components.js'; +import { createAutoRefresh } from '../auto-refresh.js'; export async function render(container, params, router) { const query = params.query || {}; @@ -27,6 +28,7 @@ export async function render(container, params, router) {
copyToClipboard(e, ad.public_key)}
+ title="Click to copy">${ad.public_key}
+ copyToClipboard(e, ad.public_key)}
- title="Click to copy">${ad.public_key}
-