From b44b04c47e5a617449a89533eff9fbefd020644b Mon Sep 17 00:00:00 2001 From: Gerrri <28703799+Gerrri@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:45:28 +0100 Subject: [PATCH 1/4] feat: consolidate connection status indicators in footer Move connection status displays from header to footer and improve clarity: - Remove ConnectionStatus component from header - Add new ConnectionIndicators component in footer - Display both Backend (WebSocket) and Coordinator connection status - Use colored status dots (green/red/orange) instead of text - Show reconnecting state with animated pulse effect This provides a clearer overview of both connection states in one place. --- frontend/src/App.css | 49 ++++++++++++++++--- frontend/src/App.tsx | 23 +++------ .../common/ConnectionIndicators.tsx | 38 ++++++++++++++ frontend/src/components/common/index.ts | 1 + 4 files changed, 87 insertions(+), 24 deletions(-) create mode 100644 frontend/src/components/common/ConnectionIndicators.tsx diff --git a/frontend/src/App.css b/frontend/src/App.css index b14c8e0..fb93584 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -289,19 +289,42 @@ font-weight: 500; } -.coordinator-status { +/* Connection Indicators */ +.connection-indicators { display: flex; align-items: center; - gap: 0.25rem; + gap: 1.5rem; } -.status-ok { - color: #22c55e; - font-weight: 500; +.connection-indicator { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.875rem; + transition: opacity 0.2s ease; } -.status-error { - color: #ef4444; +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + transition: background-color 0.2s ease; +} + +.connection-indicator.connected .status-dot { + background-color: #22c55e; +} + +.connection-indicator.disconnected .status-dot { + background-color: #ef4444; +} + +.connection-indicator.reconnecting .status-dot { + background-color: #f59e0b; + animation: pulse 1s ease-in-out infinite; +} + +.connection-label { font-weight: 500; } @@ -370,4 +393,16 @@ .btn-retry:hover { background: #ef4444; } + + .connection-indicator.connected .status-dot { + background-color: #4ade80; + } + + .connection-indicator.disconnected .status-dot { + background-color: #f87171; + } + + .connection-indicator.reconnecting .status-dot { + background-color: #fbbf24; + } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 25b2321..dbc79e5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,7 +5,7 @@ import { useWebSocket } from "./hooks/useWebSocket"; import { LoadingSpinner, ErrorMessage, - ConnectionStatus, + ConnectionIndicators, } from "./components/common"; import { api } from "./services/api"; import type { @@ -209,12 +209,6 @@ function App() {

Labgrid Dashboard

-
- -
@@ -261,16 +255,11 @@ function App() { {totalTargets} target{totalTargets !== 1 ? "s" : ""} found - {healthInfo && ( - - Coordinator:{" "} - {healthInfo.coordinator_connected ? ( - Connected - ) : ( - Disconnected - )} - - )} + diff --git a/frontend/src/components/common/ConnectionIndicators.tsx b/frontend/src/components/common/ConnectionIndicators.tsx new file mode 100644 index 0000000..f40d48e --- /dev/null +++ b/frontend/src/components/common/ConnectionIndicators.tsx @@ -0,0 +1,38 @@ +interface ConnectionIndicatorsProps { + websocketConnected: boolean; + coordinatorConnected: boolean; + isReconnecting?: boolean; +} + +/** + * Connection status indicators with pictograms for footer + */ +export function ConnectionIndicators({ + websocketConnected, + coordinatorConnected, + isReconnecting = false, +}: ConnectionIndicatorsProps) { + return ( +
+ {/* Frontend ↔ Backend WebSocket Connection */} +
+ + Backend +
+ + {/* Backend ↔ Coordinator Connection */} +
+ + Coordinator +
+
+ ); +} + +export default ConnectionIndicators; diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts index bc7ea26..69ca8f7 100644 --- a/frontend/src/components/common/index.ts +++ b/frontend/src/components/common/index.ts @@ -1,3 +1,4 @@ export { LoadingSpinner } from './LoadingSpinner'; export { ErrorMessage } from './ErrorMessage'; export { ConnectionStatus } from './ConnectionStatus'; +export { ConnectionIndicators } from './ConnectionIndicators'; From a1d17646dab0bb02e9a3bc288f635e58460afb3e Mon Sep 17 00:00:00 2001 From: Gerrri <28703799+Gerrri@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:14:56 +0100 Subject: [PATCH 2/4] test: update footer connection indicator checks --- frontend/src/__tests__/App.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/__tests__/App.test.tsx b/frontend/src/__tests__/App.test.tsx index 3b0c956..a1de9bf 100644 --- a/frontend/src/__tests__/App.test.tsx +++ b/frontend/src/__tests__/App.test.tsx @@ -109,11 +109,12 @@ describe('App', () => { }); }); - it('shows connection status when connected', async () => { + it('shows footer connection indicators when connected', async () => { render(); await waitFor(() => { - expect(screen.getByText('Connected')).toBeInTheDocument(); + expect(screen.getByText('Backend')).toBeInTheDocument(); + expect(screen.getByText('Coordinator')).toBeInTheDocument(); }); }); From 4c26a6c78c8c8dbdc79bb8580610f05125fdecc6 Mon Sep 17 00:00:00 2001 From: Gerrri <28703799+Gerrri@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:18:04 +0100 Subject: [PATCH 3/4] feat: show dashboard version in footer --- frontend/src/App.css | 7 +++++++ frontend/src/App.tsx | 6 ++++++ frontend/src/__tests__/App.test.tsx | 8 ++++++++ frontend/src/env.d.ts | 1 + frontend/vite.config.ts | 8 ++++++++ 5 files changed, 30 insertions(+) create mode 100644 frontend/src/env.d.ts diff --git a/frontend/src/App.css b/frontend/src/App.css index fb93584..790c72d 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -289,6 +289,13 @@ font-weight: 500; } +.app-version { + font-size: 0.75rem; + letter-spacing: 0.04em; + opacity: 0.72; + white-space: nowrap; +} + /* Connection Indicators */ .connection-indicators { display: flex; diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dbc79e5..8a30ae9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,6 +16,9 @@ import type { } from "./types"; import "./App.css"; +const APP_VERSION = + typeof __APP_VERSION__ === "string" ? __APP_VERSION__ : "0.1.0"; + /** * Main application component */ @@ -255,6 +258,9 @@ function App() { {totalTargets} target{totalTargets !== 1 ? "s" : ""} found + + v{APP_VERSION} + { expect(footerCount).toHaveTextContent('1 target found'); }); }); + + it('shows the application version in the footer', async () => { + render(); + + await waitFor(() => { + expect(screen.getByText('v0.1.0')).toBeInTheDocument(); + }); + }); }); diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts new file mode 100644 index 0000000..41fad5b --- /dev/null +++ b/frontend/src/env.d.ts @@ -0,0 +1 @@ +declare const __APP_VERSION__: string; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index daec086..91a884c 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,9 +1,17 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; +import { readFileSync } from 'node:fs'; + +const packageJson = JSON.parse( + readFileSync(new URL('./package.json', import.meta.url), 'utf-8') +) as { version: string }; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + define: { + __APP_VERSION__: JSON.stringify(packageJson.version), + }, server: { port: 3000, host: '0.0.0.0', From 7af7c26609cacf68bcd254cf254979e139c94fd2 Mon Sep 17 00:00:00 2001 From: Gerrri <28703799+Gerrri@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:30:06 +0100 Subject: [PATCH 4/4] feat: derive footer version from runtime build --- .github/workflows/publish-image.yml | 14 ++++++++++++++ Dockerfile.prod | 3 +++ entrypoint.sh | 5 ++++- frontend/src/App.tsx | 7 +++---- frontend/src/__tests__/App.test.tsx | 19 +++++++++++++++++-- frontend/src/env.d.ts | 1 - frontend/src/types/window-env.d.ts | 2 +- frontend/vite.config.ts | 8 -------- 8 files changed, 42 insertions(+), 17 deletions(-) delete mode 100644 frontend/src/env.d.ts diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 61aa178..bf5e5d9 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -51,6 +51,18 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} type=raw,value=${{ github.event.inputs.image_name }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.image_name != '' }} + - name: Determine app version + id: app_version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.image_name }}" ]; then + version="${{ github.event.inputs.image_name }}" + elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then + version="${GITHUB_REF_NAME}" + else + version="sha-${GITHUB_SHA::7}" + fi + echo "value=${version}" >> "${GITHUB_OUTPUT}" + - name: Build and push Docker image uses: docker/build-push-action@v5 with: @@ -60,5 +72,7 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + build-args: | + APP_VERSION=${{ steps.app_version.outputs.value }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/Dockerfile.prod b/Dockerfile.prod index 35cf893..e4b69aa 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -44,6 +44,8 @@ RUN pip install --no-cache-dir -r requirements.txt # ============================================================================= FROM python:3.11-slim +ARG APP_VERSION=dev + WORKDIR /app # Install runtime dependencies @@ -62,6 +64,7 @@ ENV PATH="/opt/venv/bin:$PATH" ENV PYTHONUNBUFFERED=1 ENV UVICORN_WORKERS=1 ENV UVICORN_LOG_LEVEL=info +ENV APP_VERSION=${APP_VERSION} # Copy frontend build artifacts to nginx web root COPY --from=frontend-builder /app/frontend/dist /var/www/html diff --git a/entrypoint.sh b/entrypoint.sh index 52f06d6..6cbc289 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,19 +5,22 @@ set -e # This file is loaded by the browser before the main application API_URL_VALUE="${API_URL_EXTERNAL-/api}" WS_URL_VALUE="${WS_URL_EXTERNAL-/api/ws}" +APP_VERSION_VALUE="${APP_VERSION-dev}" cat > /var/www/html/env-config.js <}" echo " COORDINATOR_REALM: ${COORDINATOR_REALM:-realm1}" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a30ae9..f3e2f92 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -16,13 +16,12 @@ import type { } from "./types"; import "./App.css"; -const APP_VERSION = - typeof __APP_VERSION__ === "string" ? __APP_VERSION__ : "0.1.0"; - /** * Main application component */ function App() { + const appVersion = + window.ENV?.APP_VERSION ?? import.meta.env.VITE_APP_VERSION ?? "dev"; const { presetGroups, loading, @@ -259,7 +258,7 @@ function App() { {totalTargets} target{totalTargets !== 1 ? "s" : ""} found - v{APP_VERSION} + {appVersion} ({ describe('App', () => { beforeEach(() => { vi.clearAllMocks(); + window.ENV = undefined; + }); + + afterEach(() => { + window.ENV = undefined; }); it('renders the app header', async () => { @@ -132,7 +137,17 @@ describe('App', () => { render(); await waitFor(() => { - expect(screen.getByText('v0.1.0')).toBeInTheDocument(); + expect(screen.getByText('dev')).toBeInTheDocument(); + }); + }); + + it('uses the runtime application version when provided', async () => { + window.ENV = { APP_VERSION: 'v9.9.9-test' }; + + render(); + + await waitFor(() => { + expect(screen.getByText('v9.9.9-test')).toBeInTheDocument(); }); }); }); diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts deleted file mode 100644 index 41fad5b..0000000 --- a/frontend/src/env.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare const __APP_VERSION__: string; diff --git a/frontend/src/types/window-env.d.ts b/frontend/src/types/window-env.d.ts index 251a5a7..1e0bead 100644 --- a/frontend/src/types/window-env.d.ts +++ b/frontend/src/types/window-env.d.ts @@ -5,7 +5,7 @@ declare global { ENV?: { API_URL?: string; WS_URL?: string; + APP_VERSION?: string; }; } } - diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 91a884c..daec086 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,17 +1,9 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -import { readFileSync } from 'node:fs'; - -const packageJson = JSON.parse( - readFileSync(new URL('./package.json', import.meta.url), 'utf-8') -) as { version: string }; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], - define: { - __APP_VERSION__: JSON.stringify(packageJson.version), - }, server: { port: 3000, host: '0.0.0.0',