1 Commits

Author SHA1 Message Date
Георгий
b3d902ab74 feat: deploy admin panel to stage
Some checks failed
Build White Nights admin stage / build-and-deploy (push) Failing after 14s
2026-10-27 22:19:45 +03:00
7 changed files with 148 additions and 17 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.git
node_modules
dist
dist-ssr
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# Backend content/auth API and media endpoint.
VITE_API_URL="https://server.white-nights-stage.makkah.verstack.ru"
VITE_KRBL_MEDIA="https://server.white-nights-stage.makkah.verstack.ru/media/"
# Device APIs used by the embedded route demo at /demo/:id.
VITE_GEO_API_URL="https://device.white-nights-stage.makkah.verstack.ru"
VITE_WEATHER_API_URL="https://device.white-nights-stage.makkah.verstack.ru"
VITE_NEED_AUTH="true"

View File

@@ -0,0 +1,57 @@
name: Build White Nights admin stage
on:
workflow_dispatch:
push:
branches:
- feature/stage-service-endpoints
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set image metadata
id: meta
run: |
IMAGE_PREFIX="${{ secrets.REGISTRY_URL }}/krbl/whitenightsadminpanel"
echo "IMAGE_PREFIX=${IMAGE_PREFIX}" >> "$GITEA_OUTPUT"
echo "IMAGE_TAG=$(echo \"${{ gitea.sha }}\" | cut -c1-7)" >> "$GITEA_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the container registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push admin panel
uses: docker/build-push-action@v5
with:
context: .
push: true
build-args: |
VITE_API_URL=https://server.white-nights-stage.makkah.verstack.ru
VITE_KRBL_MEDIA=https://server.white-nights-stage.makkah.verstack.ru/media/
VITE_GEO_API_URL=https://device.white-nights-stage.makkah.verstack.ru
VITE_WEATHER_API_URL=https://device.white-nights-stage.makkah.verstack.ru
tags: |
${{ steps.meta.outputs.IMAGE_PREFIX }}/service-admin-panel:${{ steps.meta.outputs.IMAGE_TAG }}
${{ steps.meta.outputs.IMAGE_PREFIX }}/service-admin-panel:stage
- name: Restart backend pod to pick up the stage frontend
env:
KUBE_CONFIG_DATA: ${{ secrets.WNS_KUBE_CONFIG }}
run: |
set -eu
KUBECONFIG_PATH="$RUNNER_TEMP/wn-kubeconfig"
printf '%s' "$KUBE_CONFIG_DATA" > "$KUBECONFIG_PATH"
chmod 600 "$KUBECONFIG_PATH"
export KUBECONFIG="$KUBECONFIG_PATH"
kubectl -n white-nights-stage rollout restart deployment/wn-backend
kubectl -n white-nights-stage rollout status deployment/wn-backend --timeout=5m

View File

@@ -1,5 +1,5 @@
# Stage 1: Build the application
FROM node:20-alpine AS build
FROM node:24-alpine AS build
# Set working directory
WORKDIR /app
@@ -7,6 +7,13 @@ WORKDIR /app
# Copy package.json and yarn.lock
COPY package.json yarn.lock ./
# Vite embeds these values into the static bundle at build time. When an
# argument is omitted, the checked-in .env remains the fallback.
ARG VITE_API_URL
ARG VITE_GEO_API_URL
ARG VITE_WEATHER_API_URL
ARG VITE_KRBL_MEDIA
# Install dependencies
RUN yarn install --frozen-lockfile
@@ -14,19 +21,23 @@ RUN yarn install --frozen-lockfile
COPY . .
# Build the application
RUN yarn build
RUN if [ -n "${VITE_API_URL:-}" ]; then export VITE_API_URL; fi && \
if [ -n "${VITE_GEO_API_URL:-}" ]; then export VITE_GEO_API_URL; fi && \
if [ -n "${VITE_WEATHER_API_URL:-}" ]; then export VITE_WEATHER_API_URL; fi && \
if [ -n "${VITE_KRBL_MEDIA:-}" ]; then export VITE_KRBL_MEDIA; fi && \
yarn build
# Stage 2: Serve the application with Nginx
FROM nginx:alpine
FROM nginxinc/nginx-unprivileged:alpine
# Copy the built application from the build stage
COPY --from=build /app/dist /usr/share/nginx/html
# Copy nginx configuration (optional, can be added later if needed)
# COPY nginx.conf /etc/nginx/conf.d/default.conf
# Keep client-side routes working on direct navigation and page refresh.
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Expose port 80
EXPOSE 80
# The unprivileged image cannot bind to port 80.
EXPOSE 8080
# Start Nginx server
CMD ["nginx", "-g", "daemon off;"]
CMD ["nginx", "-g", "daemon off;"]

22
STAGE_DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,22 @@
# Stage deployment
The admin panel is a static Vite bundle. The backend and device remain
separate services.
Build a stage image with the service URLs embedded into the bundle:
```bash
docker build \
--build-arg VITE_API_URL=https://server.white-nights-stage.makkah.verstack.ru \
--build-arg VITE_KRBL_MEDIA=https://server.white-nights-stage.makkah.verstack.ru/media/ \
--build-arg VITE_GEO_API_URL=https://device.white-nights-stage.makkah.verstack.ru \
--build-arg VITE_WEATHER_API_URL=https://device.white-nights-stage.makkah.verstack.ru \
-t white-nights-admin-panel:stage .
```
The backend URL serves content, authentication, media and admin API requests.
The device URL serves geolocation and weather requests used by `/demo/:id`.
The device ingress must publish the geolocation and weather paths to the
corresponding device ports. The browser must also be authorized by Authelia
and the backend must allow the frontend origin through CORS.

11
nginx.conf Normal file
View File

@@ -0,0 +1,11 @@
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}

View File

@@ -1,11 +1,24 @@
import axios from "axios";
const envUrl = import.meta.env.VITE_API_URL || "localhost";
const baseURL = envUrl.includes("://") ? envUrl : `http://${envUrl}`;
const isFullUrl =
baseURL.includes("://") &&
!envUrl.includes("localhost") &&
!envUrl.includes("127.0.0.1");
const trimTrailingSlash = (value) => value.replace(/\/+$/, "");
const serviceURL = (rawValue, defaultPort) => {
const value = String(rawValue || "").trim() || "localhost";
const hasScheme = value.includes("://");
const url = new URL(hasScheme ? value : `http://${value}`);
// Bare local hosts use service-specific ports. Full URLs keep their
// configured scheme and port, which is required behind ingress.
if (!hasScheme && !url.port) {
url.port = String(defaultPort);
}
return trimTrailingSlash(url.toString());
};
const apiURL = import.meta.env.VITE_API_URL || "localhost";
const geoURL = import.meta.env.VITE_GEO_API_URL || apiURL;
const weatherURL = import.meta.env.VITE_WEATHER_API_URL || apiURL;
const addAuthInterceptor = (instance) => {
instance.interceptors.request.use((config) => {
@@ -17,9 +30,9 @@ const addAuthInterceptor = (instance) => {
});
};
export const apiBaseURL = isFullUrl ? baseURL : `${baseURL}:8080`;
export const geoBaseURL = isFullUrl ? baseURL : `${baseURL}:6001`;
export const weatherBaseURL = isFullUrl ? baseURL : `${baseURL}:6002`;
export const apiBaseURL = serviceURL(apiURL, 8080);
export const geoBaseURL = serviceURL(geoURL, 6001);
export const weatherBaseURL = serviceURL(weatherURL, 6002);
export const getMediaUrl = (id) => {
const token = localStorage.getItem("token");