diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..5fee6fc --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,31 @@ +name: Build and Deploy + +on: + push: + tags: + - 'v*' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Deploy to /opt/wled-controller + run: | + DEPLOY_DIR=/opt/wled-controller + + # Ensure deploy directory exists + mkdir -p "$DEPLOY_DIR/data" "$DEPLOY_DIR/logs" "$DEPLOY_DIR/config" + + # Copy server files to deploy directory + rsync -a --delete \ + --exclude 'data/' \ + --exclude 'logs/' \ + server/ "$DEPLOY_DIR/" + + # Build and restart + cd "$DEPLOY_DIR" + docker compose down + docker compose up -d --build diff --git a/server/src/wled_controller/static/css/layout.css b/server/src/wled_controller/static/css/layout.css index c1058d7..e53a7e0 100644 --- a/server/src/wled_controller/static/css/layout.css +++ b/server/src/wled_controller/static/css/layout.css @@ -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; diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index 5fc4414..65f03ae 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -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(); diff --git a/server/src/wled_controller/static/js/core/api.js b/server/src/wled_controller/static/js/core/api.js index 8968fd6..51b7bec 100644 --- a/server/src/wled_controller/static/js/core/api.js +++ b/server/src/wled_controller/static/js/core/api.js @@ -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; } } diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 2006244..56b112d 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -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", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index b157984..b6ae97a 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -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": "Свой", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index bb02f8c..ce86db3 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -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": "自定义", diff --git a/server/src/wled_controller/templates/index.html b/server/src/wled_controller/templates/index.html index df3337e..5193c1a 100644 --- a/server/src/wled_controller/templates/index.html +++ b/server/src/wled_controller/templates/index.html @@ -29,6 +29,13 @@ +