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',