diff --git a/.env.development b/.env.development index 3c2d5cb..585c88b 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,4 @@ -VITE_API_BASE_URL=https://admin-api.dev.eventhub.local -VITE_WS_URL=wss://admin-ws.dev.eventhub.local -VITE_APP_TITLE=EventHub Admin \ No newline at end of file +# Same-origin через Vite proxy (см. vite.config.ts) — иначе CORS с localhost → admin-api. +VITE_API_BASE_URL= +VITE_WS_URL= +VITE_APP_TITLE=EventHub Admin diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 381ec66..b87e270 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -30,6 +30,19 @@ jobs: with: node-version: '22' + - name: Bake build identity + id: build_meta + run: | + set -euo pipefail + APP_VERSION="$(tr -d '[:space:]' < VERSION 2>/dev/null || echo 0.0)" + GIT_SHA="${GITHUB_SHA:0:12}" + BUILT_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "VITE_APP_VERSION=${APP_VERSION}" >> "$GITHUB_ENV" + echo "VITE_GIT_SHA=${GIT_SHA}" >> "$GITHUB_ENV" + echo "VITE_BUILT_AT=${BUILT_AT}" >> "$GITHUB_ENV" + echo "version=${APP_VERSION}" >> "$GITHUB_OUTPUT" + echo "git_sha=${GIT_SHA}" >> "$GITHUB_OUTPUT" + - run: npm ci - run: npm run lint - run: npm run build diff --git a/.gitea/workflows/deploy-stage.yml b/.gitea/workflows/deploy-stage.yml index 977406e..e85ccad 100644 --- a/.gitea/workflows/deploy-stage.yml +++ b/.gitea/workflows/deploy-stage.yml @@ -1,40 +1,46 @@ name: Deploy stage (admin) +# Auto после успешного CI на master: тот же sha-*, плюс floating :stage. +# Product version (VERSION) не бампается — только sha в рантайме/registry. on: - push: - tags: - - 'v*' + workflow_run: + workflows: [CI] + types: [completed] + branches: [master, main] workflow_dispatch: - inputs: - release_tag: - description: 'Release tag (например v0.1.1)' - required: true - default: 'v0.1.1' - sha_tag: - description: 'Образ sha-* (например sha-3f5dde498652)' - required: true - default: 'sha-3f5dde498652' + +concurrency: + group: eventhub-frontadmin-deploy-stage + cancel-in-progress: false env: REGISTRY: git.sabilin.com/eventhub jobs: deploy-stage-admin: + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') runs-on: ubuntu-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }} - - name: Compute image tags + - name: Compute image tag id: meta run: | set -euo pipefail - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "sha_tag=${{ github.event.inputs.sha_tag }}" >> "$GITHUB_OUTPUT" - echo "release_tag=${{ github.event.inputs.release_tag }}" >> "$GITHUB_OUTPUT" + if [ "${{ github.event_name }}" = "workflow_run" ]; then + SHA="${{ github.event.workflow_run.head_sha }}" else - echo "sha_tag=sha-${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT" - echo "release_tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" + SHA="${GITHUB_SHA}" fi + echo "tag=sha-${SHA:0:12}" >> "$GITHUB_OUTPUT" + + - name: Wait for Docker daemon + run: bash scripts/ensure-docker-daemon.sh 240 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -46,14 +52,24 @@ jobs: username: ${{ secrets.REGISTRY_USER }} password: ${{ secrets.REGISTRY_PASSWORD }} - - name: Promote admin-ui image (no rebuild) + - name: Verify admin-ui image from CI + run: | + set -euo pipefail + TAG="${{ steps.meta.outputs.tag }}" + if ! docker buildx imagetools inspect "${REGISTRY}/eventhub-admin-ui:${TAG}" >/dev/null 2>&1; then + echo "Образ eventhub-admin-ui:${TAG} не найден — CI push не завершился." + exit 1 + fi + echo "OK: eventhub-admin-ui:${TAG}" + + - name: Promote sha → :stage (no rebuild) env: REGISTRY_USER: ${{ secrets.REGISTRY_USER }} REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} run: | bash scripts/clone-devops-run.sh scripts/promote-images.sh \ - "${{ steps.meta.outputs.sha_tag }}" \ - "${{ steps.meta.outputs.release_tag }}" \ + "${{ steps.meta.outputs.tag }}" \ + "${{ steps.meta.outputs.tag }}" \ stage \ eventhub-admin-ui @@ -66,7 +82,7 @@ jobs: script: | export REGISTRY_USER='${{ secrets.REGISTRY_USER }}' export REGISTRY_PASSWORD='${{ secrets.REGISTRY_PASSWORD }}' - bash /opt/eventhub-stage/devops/scripts/deploy-service.sh stage admin ${{ steps.meta.outputs.release_tag }} + bash /opt/eventhub-stage/devops/scripts/deploy-service.sh stage admin ${{ steps.meta.outputs.tag }} - name: Prune old registry images continue-on-error: true diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..49d5957 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.1 diff --git a/src/api/healthApi.ts b/src/api/healthApi.ts new file mode 100644 index 0000000..0236861 --- /dev/null +++ b/src/api/healthApi.ts @@ -0,0 +1,16 @@ +import apiClient from './client'; + +export type AdminHealthInfo = { + status: string; + service?: string; + version?: string; + git_sha?: string; + built_at?: string; +}; + +export const healthApi = { + getAdminHealth: async (): Promise => { + const { data } = await apiClient.get('/v1/admin/health'); + return data; + }, +}; diff --git a/src/layouts/ControlCenterLayout.tsx b/src/layouts/ControlCenterLayout.tsx index c238ea3..d0be9ce 100644 --- a/src/layouts/ControlCenterLayout.tsx +++ b/src/layouts/ControlCenterLayout.tsx @@ -52,6 +52,12 @@ import { ErrorBoundary } from '@/components/ErrorBoundary'; import { CommandPalette } from '@/components/CommandPalette'; import { useCommandPaletteHotkey } from '@/hooks/useCommandPaletteHotkey'; import { HeaderNodeMetrics } from '@/components/HeaderNodeMetrics'; +import { healthApi } from '@/api/healthApi'; + +function formatBuildLabel(version: string, sha: string): string { + const shortSha = sha && sha !== 'unknown' && sha !== 'dev' ? sha.slice(0, 12) : sha; + return `${version} (${shortSha})`; +} const navIconByKey: Record> = { '/dashboard': LayoutDashboard, @@ -102,10 +108,34 @@ const ControlCenterLayout: React.FC = () => { const [collapsed, setCollapsed] = useState(false); const [paletteOpen, setPaletteOpen] = useState(false); const [density, setDensity] = useState(() => getTableDensity()); + const [apiVersionLabel, setApiVersionLabel] = useState(null); const openPalette = useCallback(() => setPaletteOpen(true), []); useCommandPaletteHotkey(openPalette); + const adminVersionLabel = formatBuildLabel( + import.meta.env.VITE_APP_VERSION || '0.0', + import.meta.env.VITE_GIT_SHA || 'dev' + ); + + useEffect(() => { + let cancelled = false; + healthApi + .getAdminHealth() + .then((info) => { + if (cancelled) return; + setApiVersionLabel( + formatBuildLabel(info.version || '0.0', info.git_sha || 'unknown') + ); + }) + .catch(() => { + if (!cancelled) setApiVersionLabel(null); + }); + return () => { + cancelled = true; + }; + }, []); + const selectedKey = getMenuSelectedKey(location.pathname); const groups = useMemo(() => filterNavGroupsByRole(NAV_GROUPS, user?.role), [user?.role]); @@ -317,6 +347,15 @@ const ControlCenterLayout: React.FC = () => { {t('common.langEn')} + +
{t('layout.adminVersion', { version: adminVersionLabel })}
+
+ {apiVersionLabel + ? t('layout.apiVersion', { version: apiVersionLabel }) + : t('layout.apiVersionUnavailable')} +
+
+ {t('layout.logout')} diff --git a/src/locales/en.json b/src/locales/en.json index 6910b68..5de36a3 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -192,7 +192,10 @@ "nodeCpu": "CPU: {{value}}%", "nodeMemory": "Memory: {{used}} / {{total}} MB ({{percent}}%)", "nodeWas": "was {{value}}%", - "nodeAria": "{{node}}: CPU {{cpu}}%, memory {{memory}}%" + "nodeAria": "{{node}}: CPU {{cpu}}%, memory {{memory}}%", + "adminVersion": "Admin {{version}}", + "apiVersion": "API {{version}}", + "apiVersionUnavailable": "API — unavailable" }, "explore": { "viewAria": "List view", diff --git a/src/locales/ru.json b/src/locales/ru.json index 51bad78..380bef3 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -192,7 +192,10 @@ "nodeCpu": "CPU: {{value}}%", "nodeMemory": "Память: {{used}} / {{total}} МБ ({{percent}}%)", "nodeWas": "было {{value}}%", - "nodeAria": "{{node}}: CPU {{cpu}}%, память {{memory}}%" + "nodeAria": "{{node}}: CPU {{cpu}}%, память {{memory}}%", + "adminVersion": "Admin {{version}}", + "apiVersion": "API {{version}}", + "apiVersionUnavailable": "API — нет данных" }, "explore": { "viewAria": "Вид списка", diff --git a/vite.config.ts b/vite.config.ts index 79d972b..8790350 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,8 +6,17 @@ import tailwindcss from '@tailwindcss/vite'; const apiBaseUrl = process.env.VITE_API_BASE_URL ?? 'https://admin-api.dev.eventhub.local'; const wsUrl = process.env.VITE_WS_URL ?? 'wss://admin-ws.dev.eventhub.local'; +const appVersion = process.env.VITE_APP_VERSION ?? '0.0'; +const gitSha = process.env.VITE_GIT_SHA ?? 'dev'; +const builtAt = process.env.VITE_BUILT_AT ?? ''; + export default defineConfig({ plugins: [react(), tailwindcss()], + define: { + 'import.meta.env.VITE_APP_VERSION': JSON.stringify(appVersion), + 'import.meta.env.VITE_GIT_SHA': JSON.stringify(gitSha), + 'import.meta.env.VITE_BUILT_AT': JSON.stringify(builtAt), + }, resolve: { alias: { '@': path.resolve(__dirname, './src'),