Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/publish-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
3 changes: 3 additions & 0 deletions Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
// Runtime environment configuration
// Generated by entrypoint.sh at container startup
window.ENV = {
API_URL: "${API_URL_VALUE}",
WS_URL: "${WS_URL_VALUE}"
WS_URL: "${WS_URL_VALUE}",
APP_VERSION: "${APP_VERSION_VALUE}"
};
EOF

echo "Generated env-config.js with:"
echo " API_URL: ${API_URL_VALUE}"
echo " WS_URL: ${WS_URL_VALUE}"
echo " APP_VERSION: ${APP_VERSION_VALUE}"
echo "Runtime configuration:"
echo " COORDINATOR_URL: ${COORDINATOR_URL:-<not set>}"
echo " COORDINATOR_REALM: ${COORDINATOR_REALM:-realm1}"
Expand Down
56 changes: 49 additions & 7 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -289,19 +289,49 @@
font-weight: 500;
}

.coordinator-status {
.app-version {
font-size: 0.75rem;
letter-spacing: 0.04em;
opacity: 0.72;
white-space: nowrap;
}

/* 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;
}

Expand Down Expand Up @@ -370,4 +400,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;
}
}
28 changes: 11 additions & 17 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useWebSocket } from "./hooks/useWebSocket";
import {
LoadingSpinner,
ErrorMessage,
ConnectionStatus,
ConnectionIndicators,
} from "./components/common";
import { api } from "./services/api";
import type {
Expand All @@ -20,6 +20,8 @@ import "./App.css";
* Main application component
*/
function App() {
const appVersion =
window.ENV?.APP_VERSION ?? import.meta.env.VITE_APP_VERSION ?? "dev";
const {
presetGroups,
loading,
Expand Down Expand Up @@ -209,12 +211,6 @@ function App() {
<div className="header-title">
<h1>Labgrid Dashboard</h1>
</div>
<div className="header-status">
<ConnectionStatus
isConnected={connected}
isReconnecting={isReconnecting}
/>
</div>
</header>

<main className="app-main">
Expand Down Expand Up @@ -261,16 +257,14 @@ function App() {
<span className="target-count">
{totalTargets} target{totalTargets !== 1 ? "s" : ""} found
</span>
{healthInfo && (
<span className="coordinator-status">
Coordinator:{" "}
{healthInfo.coordinator_connected ? (
<span className="status-ok">Connected</span>
) : (
<span className="status-error">Disconnected</span>
)}
</span>
)}
<span className="app-version" aria-label="Application version">
{appVersion}
</span>
<ConnectionIndicators
websocketConnected={connected}
coordinatorConnected={healthInfo?.coordinator_connected ?? false}
isReconnecting={isReconnecting}
/>
</div>
</footer>
</div>
Expand Down
30 changes: 27 additions & 3 deletions frontend/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Tests for the main App component.
*/

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import App from '../App';

Expand Down Expand Up @@ -81,6 +81,11 @@ vi.mock('../hooks/useWebSocket', () => ({
describe('App', () => {
beforeEach(() => {
vi.clearAllMocks();
window.ENV = undefined;
});

afterEach(() => {
window.ENV = undefined;
});

it('renders the app header', async () => {
Expand Down Expand Up @@ -109,11 +114,12 @@ describe('App', () => {
});
});

it('shows connection status when connected', async () => {
it('shows footer connection indicators when connected', async () => {
render(<App />);

await waitFor(() => {
expect(screen.getByText('Connected')).toBeInTheDocument();
expect(screen.getByText('Backend')).toBeInTheDocument();
expect(screen.getByText('Coordinator')).toBeInTheDocument();
});
});

Expand All @@ -126,4 +132,22 @@ describe('App', () => {
expect(footerCount).toHaveTextContent('1 target found');
});
});

it('shows the application version in the footer', async () => {
render(<App />);

await waitFor(() => {
expect(screen.getByText('dev')).toBeInTheDocument();
});
});

it('uses the runtime application version when provided', async () => {
window.ENV = { APP_VERSION: 'v9.9.9-test' };

render(<App />);

await waitFor(() => {
expect(screen.getByText('v9.9.9-test')).toBeInTheDocument();
});
});
});
38 changes: 38 additions & 0 deletions frontend/src/components/common/ConnectionIndicators.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="connection-indicators">
{/* Frontend ↔ Backend WebSocket Connection */}
<div
className={`connection-indicator ${websocketConnected && !isReconnecting ? 'connected' : 'disconnected'} ${isReconnecting ? 'reconnecting' : ''}`}
title={`Frontend ↔ Backend: ${isReconnecting ? 'Reconnecting...' : websocketConnected ? 'Connected' : 'Disconnected'}`}
>
<span className="status-dot" />
<span className="connection-label">Backend</span>
</div>

{/* Backend ↔ Coordinator Connection */}
<div
className={`connection-indicator ${coordinatorConnected ? 'connected' : 'disconnected'}`}
title={`Backend ↔ Coordinator: ${coordinatorConnected ? 'Connected' : 'Disconnected'}`}
>
<span className="status-dot" />
<span className="connection-label">Coordinator</span>
</div>
</div>
);
}

export default ConnectionIndicators;
1 change: 1 addition & 0 deletions frontend/src/components/common/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { LoadingSpinner } from './LoadingSpinner';
export { ErrorMessage } from './ErrorMessage';
export { ConnectionStatus } from './ConnectionStatus';
export { ConnectionIndicators } from './ConnectionIndicators';
2 changes: 1 addition & 1 deletion frontend/src/types/window-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ declare global {
ENV?: {
API_URL?: string;
WS_URL?: string;
APP_VERSION?: string;
};
}
}