Add connection overlay and Gitea CI/CD workflow

Show full-screen overlay with spinner when server is unreachable,
with periodic health checks that auto-hide on reconnect.
Add Gitea Actions workflow for auto-deploy on release tags.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 11:07:13 +03:00
parent bf212c86ec
commit 99d8c4b8fb
8 changed files with 134 additions and 5 deletions

View File

@@ -105,6 +105,48 @@ h2 {
50% { opacity: 0.5; }
}
/* Connection lost overlay */
.connection-overlay {
position: fixed;
inset: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
}
.connection-overlay-content {
text-align: center;
color: #fff;
}
.connection-overlay-content h2 {
margin: 16px 0 8px;
font-size: 1.4rem;
}
.connection-overlay-content p {
margin: 0;
opacity: 0.7;
font-size: 0.95rem;
}
.connection-spinner-lg {
width: 40px;
height: 40px;
margin: 0 auto;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: conn-spin 0.8s linear infinite;
}
@keyframes conn-spin {
to { transform: rotate(360deg); }
}
/* WLED device health indicator */
.health-dot {
display: inline-block;

View File

@@ -7,7 +7,7 @@ import { apiKey, setApiKey, refreshInterval } from './core/state.js';
import { Modal } from './core/modal.js';
// Layer 1: api, i18n
import { loadServerInfo, loadDisplays, configureApiKey } from './core/api.js';
import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor } from './core/api.js';
import { t, initLocale, changeLocale } from './core/i18n.js';
// Layer 2: ui
@@ -506,6 +506,7 @@ window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
stopConnectionMonitor();
stopEventsWS();
disconnectAllKCWebSockets();
disconnectAllLedPreviewWS();
@@ -552,6 +553,10 @@ document.addEventListener('DOMContentLoaded', async () => {
// Setup form handler
document.getElementById('add-device-form').addEventListener('submit', handleAddDevice);
// Always monitor server connection (even before login)
loadServerInfo();
startConnectionMonitor();
// Show modal if no API key is stored
if (!apiKey) {
setTimeout(() => {
@@ -563,7 +568,6 @@ document.addEventListener('DOMContentLoaded', async () => {
}
// User is logged in, load data
loadServerInfo();
loadDisplays();
loadTargetsTab();

View File

@@ -120,17 +120,56 @@ export function handle401Error() {
}
}
let _connCheckTimer = null;
let _serverOnline = null; // null = unknown, true/false
function _setConnectionState(online) {
const changed = _serverOnline !== online;
_serverOnline = online;
const banner = document.getElementById('connection-overlay');
const badge = document.getElementById('server-status');
if (online) {
if (banner) banner.style.display = 'none';
if (badge) badge.className = 'status-badge online';
} else {
if (banner) banner.style.display = 'flex';
if (badge) badge.className = 'status-badge offline';
}
return changed;
}
export async function loadServerInfo() {
try {
const response = await fetch('/health');
const response = await fetch('/health', { signal: AbortSignal.timeout(5000) });
const data = await response.json();
document.getElementById('version-number').textContent = `v${data.version}`;
document.getElementById('server-status').textContent = '●';
document.getElementById('server-status').className = 'status-badge online';
const wasOffline = _serverOnline === false;
_setConnectionState(true);
if (wasOffline) {
// Server came back — reload data
window.dispatchEvent(new CustomEvent('server:reconnected'));
}
} catch (error) {
console.error('Failed to load server info:', error);
document.getElementById('server-status').className = 'status-badge offline';
_setConnectionState(false);
}
}
/**
* Start periodic health checks. Shows/hides the connection banner.
* @param {number} interval - Check interval in ms (default 10s)
*/
export function startConnectionMonitor(interval = 10000) {
stopConnectionMonitor();
_connCheckTimer = setInterval(loadServerInfo, interval);
}
export function stopConnectionMonitor() {
if (_connCheckTimer) {
clearInterval(_connCheckTimer);
_connCheckTimer = null;
}
}

View File

@@ -2,6 +2,8 @@
"app.title": "LED Grab",
"app.version": "Version:",
"app.api_docs": "API Documentation",
"app.connection_lost": "Server unreachable",
"app.connection_retrying": "Attempting to reconnect…",
"theme.toggle": "Toggle theme",
"accent.title": "Accent color",
"accent.custom": "Custom",

View File

@@ -2,6 +2,8 @@
"app.title": "LED Grab",
"app.version": "Версия:",
"app.api_docs": "Документация API",
"app.connection_lost": "Сервер недоступен",
"app.connection_retrying": "Попытка переподключения…",
"theme.toggle": "Переключить тему",
"accent.title": "Цвет акцента",
"accent.custom": "Свой",

View File

@@ -2,6 +2,8 @@
"app.title": "LED Grab",
"app.version": "版本:",
"app.api_docs": "API 文档",
"app.connection_lost": "服务器不可达",
"app.connection_retrying": "正在尝试重新连接…",
"theme.toggle": "切换主题",
"accent.title": "主题色",
"accent.custom": "自定义",