From ab9d03be8abfb98b32510ba6153d65908ca38868 Mon Sep 17 00:00:00 2001 From: nessi Date: Fri, 13 Feb 2026 09:55:08 +0100 Subject: [PATCH 1/4] Add GitHub Actions workflow for Docker image release This workflow automates building and publishing Docker images upon a release or manual trigger. It includes steps for version resolution, Docker Hub login, and caching to optimize builds for both backend and frontend images. --- .github/workflows/docker-release.yml | 91 ++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .github/workflows/docker-release.yml diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..e38c925 --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,91 @@ +name: Docker Publish (Release) + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: "Version tag to publish (e.g. 0.1.2 or v0.1.2)" + required: false + type: string + +jobs: + publish: + name: Build and Push Docker Images + runs-on: ubuntu-latest + permissions: + contents: read + + env: + # Optional repo variable. If unset, DOCKERHUB_USERNAME is used. + IMAGE_NAMESPACE: ${{ vars.DOCKERHUB_NAMESPACE }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve version/tag + id: ver + shell: bash + run: | + RAW_TAG="${{ github.event.release.tag_name }}" + if [ -z "$RAW_TAG" ]; then + RAW_TAG="${{ inputs.version }}" + fi + if [ -z "$RAW_TAG" ]; then + RAW_TAG="${GITHUB_REF_NAME}" + fi + + CLEAN_TAG="${RAW_TAG#v}" + echo "raw=$RAW_TAG" >> "$GITHUB_OUTPUT" + echo "clean=$CLEAN_TAG" >> "$GITHUB_OUTPUT" + + - name: Set image namespace + id: ns + shell: bash + run: | + NS="${IMAGE_NAMESPACE}" + if [ -z "$NS" ]; then + NS="${{ secrets.DOCKERHUB_USERNAME }}" + fi + if [ -z "$NS" ]; then + echo "Missing Docker Hub namespace. Set repo var DOCKERHUB_NAMESPACE or secret DOCKERHUB_USERNAME." + exit 1 + fi + echo "value=$NS" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push backend image + uses: docker/build-push-action@v6 + with: + context: ./backend + file: ./backend/Dockerfile + push: true + tags: | + ${{ steps.ns.outputs.value }}/nexapg-backend:${{ steps.ver.outputs.clean }} + ${{ steps.ns.outputs.value }}/nexapg-backend:latest + cache-from: type=registry,ref=${{ steps.ns.outputs.value }}/nexapg-backend:buildcache + cache-to: type=registry,ref=${{ steps.ns.outputs.value }}/nexapg-backend:buildcache,mode=max + + - name: Build and push frontend image + uses: docker/build-push-action@v6 + with: + context: ./frontend + file: ./frontend/Dockerfile + push: true + build-args: | + VITE_API_URL=/api/v1 + tags: | + ${{ steps.ns.outputs.value }}/nexapg-frontend:${{ steps.ver.outputs.clean }} + ${{ steps.ns.outputs.value }}/nexapg-frontend:latest + cache-from: type=registry,ref=${{ steps.ns.outputs.value }}/nexapg-frontend:buildcache + cache-to: type=registry,ref=${{ steps.ns.outputs.value }}/nexapg-frontend:buildcache,mode=max From ba1559e79029b5e650cefc109817176c0e999638 Mon Sep 17 00:00:00 2001 From: nessi Date: Fri, 13 Feb 2026 10:01:24 +0100 Subject: [PATCH 2/4] Improve agentless mode messaging for host-level metrics Updated the messaging and UI to clarify unavailability of host-level metrics, such as CPU, RAM, and disk space, in agentless mode. Added clear formatting and new functions to handle missing metrics gracefully in the frontend. --- backend/app/services/infra_probe.py | 4 ++-- frontend/src/pages/TargetDetailPage.jsx | 26 +++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/backend/app/services/infra_probe.py b/backend/app/services/infra_probe.py index a5910b0..d331ec8 100644 --- a/backend/app/services/infra_probe.py +++ b/backend/app/services/infra_probe.py @@ -17,10 +17,10 @@ class DiskSpaceProvider: class NullDiskSpaceProvider(DiskSpaceProvider): async def get_free_bytes(self, target_host: str) -> DiskSpaceProbeResult: return DiskSpaceProbeResult( - source="none", + source="agentless", status="unavailable", free_bytes=None, - message=f"No infra probe configured for host {target_host}. Add SSH/Agent provider later.", + message=f"Agentless mode: host-level free disk is not available for {target_host}.", ) diff --git a/frontend/src/pages/TargetDetailPage.jsx b/frontend/src/pages/TargetDetailPage.jsx index a24b9ed..21c26e1 100644 --- a/frontend/src/pages/TargetDetailPage.jsx +++ b/frontend/src/pages/TargetDetailPage.jsx @@ -41,6 +41,19 @@ function formatNumber(value, digits = 2) { return Number(value).toFixed(digits); } +function formatHostMetricUnavailable() { + return "N/A (agentless)"; +} + +function formatDiskSpaceAgentless(diskSpace) { + if (!diskSpace) return formatHostMetricUnavailable(); + if (diskSpace.free_bytes !== null && diskSpace.free_bytes !== undefined) { + return formatBytes(diskSpace.free_bytes); + } + if (diskSpace.status === "unavailable") return formatHostMetricUnavailable(); + return "-"; +} + function MetricsTooltip({ active, payload, label }) { if (!active || !payload || payload.length === 0) return null; const row = payload[0]?.payload || {}; @@ -346,6 +359,9 @@ export function TargetDetailPage() { {uiMode === "dba" && overview && (

Database Overview

+

+ Agentless mode: host-level CPU, RAM, and free-disk metrics are not available. +

PostgreSQL Version{overview.instance.server_version || "-"}
@@ -366,8 +382,8 @@ export function TargetDetailPage() {
WAL Size{formatBytes(overview.storage.wal_directory_size_bytes)}
-
- Free Disk{formatBytes(overview.storage.disk_space.free_bytes)} +
+ Free Disk{formatDiskSpaceAgentless(overview.storage.disk_space)}
Replay Lag @@ -378,6 +394,12 @@ export function TargetDetailPage() {
Replication Slots{overview.replication.replication_slots_count ?? "-"}
Repl Clients{overview.replication.active_replication_clients ?? "-"}
Autovacuum Workers{overview.performance.autovacuum_workers ?? "-"}
+
+ Host CPU{formatHostMetricUnavailable()} +
+
+ Host RAM{formatHostMetricUnavailable()} +
From 5c5d51350fb23983a4768c67ba45219dd792527c Mon Sep 17 00:00:00 2001 From: nessi Date: Fri, 13 Feb 2026 10:06:56 +0100 Subject: [PATCH 3/4] Fix stale `refresh` usage in QueryInsightsPage effect hooks Replaced `refresh` with `useRef` to ensure the latest value is always used in async operations. Added cleanup logic to handle component unmounts during API calls, preventing state updates on unmounted components. --- frontend/src/pages/QueryInsightsPage.jsx | 26 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/frontend/src/pages/QueryInsightsPage.jsx b/frontend/src/pages/QueryInsightsPage.jsx index ac2c04b..76ebe46 100644 --- a/frontend/src/pages/QueryInsightsPage.jsx +++ b/frontend/src/pages/QueryInsightsPage.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { apiFetch } from "../api"; import { useAuth } from "../state"; @@ -62,6 +62,7 @@ function buildQueryTips(row) { export function QueryInsightsPage() { const { tokens, refresh } = useAuth(); + const refreshRef = useRef(refresh); const [targets, setTargets] = useState([]); const [targetId, setTargetId] = useState(""); const [rows, setRows] = useState([]); @@ -71,6 +72,10 @@ export function QueryInsightsPage() { const [error, setError] = useState(""); const [loading, setLoading] = useState(true); + useEffect(() => { + refreshRef.current = refresh; + }, [refresh]); + useEffect(() => { (async () => { try { @@ -89,17 +94,26 @@ export function QueryInsightsPage() { useEffect(() => { if (!targetId) return; + let active = true; (async () => { try { - const data = await apiFetch(`/targets/${targetId}/top-queries`, {}, tokens, refresh); + const data = await apiFetch(`/targets/${targetId}/top-queries`, {}, tokens, refreshRef.current); + if (!active) return; setRows(data); - setSelectedQuery(data[0] || null); - setPage(1); + setSelectedQuery((prev) => { + if (!prev) return data[0] || null; + const keep = data.find((row) => row.queryid === prev.queryid); + return keep || data[0] || null; + }); + setPage((prev) => (prev === 1 ? prev : 1)); } catch (e) { - setError(String(e.message || e)); + if (active) setError(String(e.message || e)); } })(); - }, [targetId, tokens, refresh]); + return () => { + active = false; + }; + }, [targetId, tokens?.accessToken, tokens?.refreshToken]); const dedupedByQueryId = [...rows].reduce((acc, row) => { if (!row?.queryid) return acc; From 4e4f8ad5d478675eaa8156f81382f15e112b83ef Mon Sep 17 00:00:00 2001 From: nessi Date: Fri, 13 Feb 2026 10:11:00 +0100 Subject: [PATCH 4/4] Update NEXAPG version to 0.1.3 This increments the application version from 0.1.2 to 0.1.3. It likely reflects bug fixes, improvements, or minor feature additions. --- backend/app/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index f04aa50..c526e70 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -2,7 +2,7 @@ from functools import lru_cache from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict -NEXAPG_VERSION = "0.1.2" +NEXAPG_VERSION = "0.1.3" class Settings(BaseSettings):