diff --git a/docs_prod/NODEDC_TASKER_DEPLOY_MODEL.md b/docs_prod/NODEDC_TASKER_DEPLOY_MODEL.md new file mode 100644 index 0000000..bbf87fd --- /dev/null +++ b/docs_prod/NODEDC_TASKER_DEPLOY_MODEL.md @@ -0,0 +1,67 @@ +# NODE.DC Tasker deploy model + +## Source of truth + +`NODEDC_TASKMANAGER` is the source repository for Tasker code and Docker build inputs. + +The NAS directory `/volume1/docker/nodedc-platform/tasker/plane-src` is a deploy checkout/copy only. It must not become the long-term source of truth. + +Runtime state stays outside the repository: + +- PostgreSQL data: Docker volume `nodedc-tasker_pgdata` +- Redis data: Docker volume `nodedc-tasker_redisdata` +- RabbitMQ data: Docker volume `nodedc-tasker_rabbitmq_data` +- MinIO uploads: Docker volume `nodedc-tasker_uploads` +- NAS environment files: `/volume1/docker/nodedc-platform/tasker/plane-app/.env.synology` + +## Environment contract + +The same codebase supports local and production-like runs. The deployment mode is selected by environment variables and Docker build args, not by branching the code. + +Frontend brand and launcher integration: + +- `VITE_NODEDC_LAUNCHER_URL` points the web/admin bundles to Launcher. +- Production default for NAS builds: `https://hub.nodedc.ru` +- Local default in source helpers: `http://launcher.local.nodedc` + +Backend integration: + +- `PLANE_NODEDC_LAUNCHER_URL` +- `PLANE_NODEDC_LAUNCHER_PUBLIC_URL` +- `PLANE_NODEDC_HANDOFF_URL` +- `PLANE_NODEDC_WORKSPACE_POLICY_URL` +- `PLANE_NODEDC_ACCESS_CHECK_URL` +- `PLANE_NODEDC_ACCESS_TOKEN` + +Production secrets remain in NAS env files or a secret store. They must not be committed. + +## NAS legacy rebuild + +The current NAS-compatible script lives in `plane-src/rebuild-nas-legacy.sh`. + +Default production-style web rebuild: + +```sh +cd /volume1/docker/nodedc-platform/tasker/plane-src +BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 sh rebuild-nas-legacy.sh +``` + +Local/staging-style rebuild can override integration endpoints: + +```sh +VITE_NODEDC_LAUNCHER_URL=http://launcher.local.nodedc \ +SMOKE_BASE_URL=http://task.local.nodedc:18080 \ +SMOKE_RESOLVE= \ +BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 \ +sh rebuild-nas-legacy.sh +``` + +The script recreates only services whose images were rebuilt. It must not remove Docker volumes. + +## Safety rules + +- Do not run `docker compose down -v` on production/staging data. +- Do not delete `nodedc-tasker_*` Docker volumes without a verified backup. +- Run database backups before backend migrations. +- Frontend-only rebuilds do not modify PostgreSQL or MinIO data. +- Backend image rebuilds are safe only when no destructive migration or data command is included. diff --git a/plane-src/.gitignore b/plane-src/.gitignore index e2e6441..3e58a11 100644 --- a/plane-src/.gitignore +++ b/plane-src/.gitignore @@ -1,6 +1,7 @@ node_modules .next .yarn +.pnpm-store/ ### NextJS ### # Dependencies @@ -20,6 +21,8 @@ dist/ out/ build/ .react-router/ +**/build/ +**/.react-router/ # Misc .DS_Store diff --git a/plane-src/apps/admin/Dockerfile.admin b/plane-src/apps/admin/Dockerfile.admin index b28ada8..38d7045 100644 --- a/plane-src/apps/admin/Dockerfile.admin +++ b/plane-src/apps/admin/Dockerfile.admin @@ -53,6 +53,9 @@ ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL ARG VITE_WEB_BASE_PATH="" ENV VITE_WEB_BASE_PATH=$VITE_WEB_BASE_PATH +ARG VITE_NODEDC_LAUNCHER_URL="" +ENV VITE_NODEDC_LAUNCHER_URL=$VITE_NODEDC_LAUNCHER_URL + ARG VITE_WEBSITE_URL="https://plane.so" ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL ARG VITE_SUPPORT_EMAIL="support@plane.so" @@ -67,8 +70,8 @@ COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json # Fetch dependencies to cache store, then install offline with dev deps -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false +RUN pnpm fetch --store-dir=/pnpm/store +RUN CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false # Build only the admin package RUN pnpm turbo run build --filter=admin @@ -79,7 +82,6 @@ FROM nginx:1.29-alpine AS production COPY apps/admin/nginx/nginx.conf /etc/nginx/nginx.conf COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/nodedcsudo -RUN chmod -R a+rX /usr/share/nginx/html EXPOSE 3000 diff --git a/plane-src/apps/admin/app/(all)/(home)/auth-header.tsx b/plane-src/apps/admin/app/(all)/(home)/auth-header.tsx index d27ac93..52cfbb5 100644 --- a/plane-src/apps/admin/app/(all)/(home)/auth-header.tsx +++ b/plane-src/apps/admin/app/(all)/(home)/auth-header.tsx @@ -4,14 +4,16 @@ * See the LICENSE file for details. */ -import Link from "next/link"; +import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; export function AuthHeader() { + const logoLinkUrl = useNodeDCBrandLinkUrl(); + return (
- + NODE.DC - + Глобальное администрирование diff --git a/plane-src/apps/admin/components/common/header/index.tsx b/plane-src/apps/admin/components/common/header/index.tsx index a635873..fcd9a03 100644 --- a/plane-src/apps/admin/components/common/header/index.tsx +++ b/plane-src/apps/admin/components/common/header/index.tsx @@ -21,6 +21,7 @@ import { cn, getFileURL } from "@plane/utils"; import NodeDcLogo from "@/app/assets/logos/nodedc-logo.svg?url"; // hooks import { useUser } from "@/hooks/store"; +import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; // local imports const authService = new AuthService(); @@ -42,6 +43,7 @@ export const AdminHeader = observer(function AdminHeader() { const { currentUser, signOut } = useUser(); const { resolvedTheme, setTheme } = useNextTheme(); const [csrfToken, setCsrfToken] = useState(undefined); + const logoLinkUrl = useNodeDCBrandLinkUrl(); const isFeatureRoute = FEATURE_NAVIGATION.some((item) => pathName?.startsWith(item.href)); const adminName = currentUser?.display_name || currentUser?.email || "Глобальный админ"; @@ -60,7 +62,7 @@ export const AdminHeader = observer(function AdminHeader() { return (
- + NODE.DC diff --git a/plane-src/apps/admin/helpers/nodedc-brand.ts b/plane-src/apps/admin/helpers/nodedc-brand.ts new file mode 100644 index 0000000..7032fea --- /dev/null +++ b/plane-src/apps/admin/helpers/nodedc-brand.ts @@ -0,0 +1,28 @@ +export function buildNodeDCLauncherUrl(): string { + const configuredUrl = process.env.VITE_NODEDC_LAUNCHER_URL; + + if (configuredUrl) { + return configuredUrl; + } + + if (typeof window === "undefined") { + return "http://launcher.local.nodedc/"; + } + + const hostname = window.location.hostname.toLowerCase(); + + if (hostname.endsWith(".nodedc.ru")) { + return "https://hub.nodedc.ru/"; + } + + if (hostname.endsWith(".nas.nodedc")) { + const port = window.location.port ? `:${window.location.port}` : ""; + return `${window.location.protocol}//launcher.nas.nodedc${port}/`; + } + + return "http://launcher.local.nodedc/"; +} + +export function buildNodeDCBrandConfigUrl(): string { + return new URL("/api/public/brand", buildNodeDCLauncherUrl()).toString(); +} diff --git a/plane-src/apps/admin/hooks/use-nodedc-brand-link-url.ts b/plane-src/apps/admin/hooks/use-nodedc-brand-link-url.ts new file mode 100644 index 0000000..9fdff67 --- /dev/null +++ b/plane-src/apps/admin/hooks/use-nodedc-brand-link-url.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from "react"; +import { buildNodeDCBrandConfigUrl, buildNodeDCLauncherUrl } from "@/helpers/nodedc-brand"; + +type TNodeDCBrandPayload = { + logoLinkUrl?: string | null; +}; + +export function useNodeDCBrandLinkUrl() { + const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl); + + useEffect(() => { + let isMounted = true; + + fetch(buildNodeDCBrandConfigUrl(), { cache: "no-store" }) + .then((response) => (response.ok ? response.json() : null)) + .then((payload: TNodeDCBrandPayload | null) => { + const configuredUrl = typeof payload?.logoLinkUrl === "string" ? payload.logoLinkUrl.trim() : ""; + if (isMounted && configuredUrl) setLogoLinkUrl(configuredUrl); + return undefined; + }) + .catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : "Не удалось загрузить brand config NODE.DC"); + }); + + return () => { + isMounted = false; + }; + }, []); + + return logoLinkUrl; +} diff --git a/plane-src/apps/live/Dockerfile.live b/plane-src/apps/live/Dockerfile.live index 51151cf..a50d0d4 100644 --- a/plane-src/apps/live/Dockerfile.live +++ b/plane-src/apps/live/Dockerfile.live @@ -40,24 +40,11 @@ COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json # Fetch dependencies to cache store, then install offline with dev deps -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store +RUN pnpm fetch --store-dir=/pnpm/store +RUN CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store ENV TURBO_TELEMETRY_DISABLED=1 -RUN mkdir -p \ - apps/live/.turbo \ - packages/constants/.turbo \ - packages/decorators/.turbo \ - packages/editor/.turbo \ - packages/hooks/.turbo \ - packages/i18n/.turbo \ - packages/logger/.turbo \ - packages/propel/.turbo \ - packages/types/.turbo \ - packages/ui/.turbo \ - packages/utils/.turbo - RUN pnpm turbo run build --filter=live # ***************************************************************************** diff --git a/plane-src/apps/space/Dockerfile.space b/plane-src/apps/space/Dockerfile.space index f12ff80..c03fe25 100644 --- a/plane-src/apps/space/Dockerfile.space +++ b/plane-src/apps/space/Dockerfile.space @@ -68,8 +68,8 @@ COPY --from=builder /app/out/full/ . COPY turbo.json turbo.json # Fetch dependencies to cache store, then install offline with dev deps -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store -RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false +RUN pnpm fetch --store-dir=/pnpm/store +RUN CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false # Build only the space package RUN pnpm turbo run build --filter=space diff --git a/plane-src/apps/web/Dockerfile.web.nas-legacy b/plane-src/apps/web/Dockerfile.web.nas-legacy new file mode 100644 index 0000000..36a90b5 --- /dev/null +++ b/plane-src/apps/web/Dockerfile.web.nas-legacy @@ -0,0 +1,97 @@ +FROM node:22-alpine AS base + +# Setup pnpm package manager with corepack and configure global bin directory for caching +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH" +RUN corepack enable + +# ***************************************************************************** +# STAGE 1: Build the project +# ***************************************************************************** +FROM base AS builder +RUN apk add --no-cache libc6-compat +# Set working directory +WORKDIR /app + +ARG TURBO_VERSION=2.9.4 +RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION} +COPY . . + +RUN turbo prune --scope=web --docker + +# ***************************************************************************** +# STAGE 2: Install dependencies & build the project +# ***************************************************************************** +# Add lockfile and package.json's of isolated subworkspace +FROM base AS installer + +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# First install the dependencies (as they change less often) +COPY .gitignore .gitignore +COPY --from=builder /app/out/json/ . +COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml +RUN corepack enable pnpm + +# Copy full directory structure before fetch to ensure all package.json files are available +COPY --from=builder /app/out/full/ . +COPY turbo.json turbo.json + +# Fetch dependencies to cache store, then install offline with dev deps +RUN pnpm fetch --store-dir=/pnpm/store +RUN CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store + +ARG VITE_API_BASE_URL="" +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL + +ARG VITE_ADMIN_BASE_URL="" +ENV VITE_ADMIN_BASE_URL=$VITE_ADMIN_BASE_URL + +ARG VITE_ADMIN_BASE_PATH="/nodedcsudo" +ENV VITE_ADMIN_BASE_PATH=$VITE_ADMIN_BASE_PATH + +ARG VITE_LIVE_BASE_URL="" +ENV VITE_LIVE_BASE_URL=$VITE_LIVE_BASE_URL + +ARG VITE_LIVE_BASE_PATH="/live" +ENV VITE_LIVE_BASE_PATH=$VITE_LIVE_BASE_PATH + +ARG VITE_SPACE_BASE_URL="" +ENV VITE_SPACE_BASE_URL=$VITE_SPACE_BASE_URL + +ARG VITE_SPACE_BASE_PATH="/spaces" +ENV VITE_SPACE_BASE_PATH=$VITE_SPACE_BASE_PATH + +ARG VITE_WEB_BASE_URL="" +ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL + +ARG VITE_NODEDC_OIDC_LOGIN_ENABLED="" +ENV VITE_NODEDC_OIDC_LOGIN_ENABLED=$VITE_NODEDC_OIDC_LOGIN_ENABLED + +ARG VITE_NODEDC_OIDC_LOGIN_URL="" +ENV VITE_NODEDC_OIDC_LOGIN_URL=$VITE_NODEDC_OIDC_LOGIN_URL + +ARG VITE_NODEDC_LAUNCHER_URL="" +ENV VITE_NODEDC_LAUNCHER_URL=$VITE_NODEDC_LAUNCHER_URL + +ENV NEXT_TELEMETRY_DISABLED=1 +ENV TURBO_TELEMETRY_DISABLED=1 + +RUN pnpm turbo run build --filter=web + +# ***************************************************************************** +# STAGE 3: Serve with nginx +# ***************************************************************************** +FROM nginx:1.27-alpine AS production + +COPY apps/web/nginx/nginx.conf /etc/nginx/nginx.conf +COPY --from=installer /app/apps/web/build/client /usr/share/nginx/html +RUN chmod -R a+rX /usr/share/nginx/html + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -fsS http://127.0.0.1:3000/ >/dev/null || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/expanded-layout.tsx b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/expanded-layout.tsx index 5df1e25..3825cd4 100644 --- a/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/expanded-layout.tsx +++ b/plane-src/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/expanded-layout.tsx @@ -8,11 +8,10 @@ import { useTranslation } from "@plane/i18n"; import { Shapes } from "lucide-react"; -import { useEffect, useState } from "react"; import { cn } from "@plane/utils"; // components import { TopNavPowerK } from "@/components/navigation"; -import { buildNodeDCBrandConfigUrl, buildNodeDCLauncherUrl } from "@/helpers/nodedc-auth"; +import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root"; import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root"; import { useHome } from "@/hooks/store/use-home"; @@ -32,26 +31,7 @@ export const ExpandedProjectShellToolbarLayout = ({ }: TProjectShellToolbarLayoutProps) => { const { t } = useTranslation(); const { toggleWidgetSettings } = useHome(); - const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl); - - useEffect(() => { - let isMounted = true; - - fetch(buildNodeDCBrandConfigUrl(), { cache: "no-store" }) - .then((response) => (response.ok ? response.json() : null)) - .then((payload: { logoLinkUrl?: string } | null) => { - if (isMounted && payload?.logoLinkUrl) { - setLogoLinkUrl(payload.logoLinkUrl); - } - }) - .catch((error: unknown) => { - console.warn(error instanceof Error ? error.message : "Не удалось загрузить brand config NODE.DC"); - }); - - return () => { - isMounted = false; - }; - }, []); + const logoLinkUrl = useNodeDCBrandLinkUrl(); return (
diff --git a/plane-src/apps/web/core/components/auth-screens/header.tsx b/plane-src/apps/web/core/components/auth-screens/header.tsx index 64186e0..ffa90b4 100644 --- a/plane-src/apps/web/core/components/auth-screens/header.tsx +++ b/plane-src/apps/web/core/components/auth-screens/header.tsx @@ -12,6 +12,7 @@ import { useTranslation } from "@plane/i18n"; import { PlaneLockup } from "@plane/propel/icons"; import { PageHead } from "@/components/core/page-title"; import { EAuthModes } from "@/helpers/authentication.helper"; +import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; import { useInstance } from "@/hooks/store/use-instance"; const authContentMap = { @@ -68,17 +69,19 @@ type TAuthHeaderBase = { export function AuthHeaderBase(props: TAuthHeaderBase) { const { pageTitle, additionalAction } = props; + const logoLinkUrl = useNodeDCBrandLinkUrl(); + return ( <>
- + - + {additionalAction}
diff --git a/plane-src/apps/web/core/components/instance/not-ready-view.tsx b/plane-src/apps/web/core/components/instance/not-ready-view.tsx index 806b978..4d02e6b 100644 --- a/plane-src/apps/web/core/components/instance/not-ready-view.tsx +++ b/plane-src/apps/web/core/components/instance/not-ready-view.tsx @@ -12,14 +12,23 @@ import GradientBgLogo from "@/app/assets/auth/gradient-bg-logo.webp?url"; import DefaultLayout from "@/layouts/default-layout"; import { PlaneLockup } from "@plane/propel/icons"; import { Button } from "@plane/propel/button"; +import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; export function InstanceNotReady() { + const logoLinkUrl = useNodeDCBrandLinkUrl(); + return (
- + + +
diff --git a/plane-src/apps/web/core/components/nodedc/standalone-shell.tsx b/plane-src/apps/web/core/components/nodedc/standalone-shell.tsx index 785b83b..8c61fa0 100644 --- a/plane-src/apps/web/core/components/nodedc/standalone-shell.tsx +++ b/plane-src/apps/web/core/components/nodedc/standalone-shell.tsx @@ -7,11 +7,10 @@ */ import type { ReactNode } from "react"; -import { useEffect, useState } from "react"; import { useTranslation } from "@plane/i18n"; import { InboxIcon } from "@plane/propel/icons"; import { Tooltip } from "@plane/propel/tooltip"; -import { buildNodeDCBrandConfigUrl, buildNodeDCLauncherUrl } from "@/helpers/nodedc-auth"; +import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root"; type TNodeDCStandaloneShellProps = { @@ -24,25 +23,7 @@ type TNodeDCStandaloneShellProps = { export const NodeDCStandaloneShell = (props: TNodeDCStandaloneShellProps) => { const { children, notificationsCount = 0, onOpenNotifications, showUserControls = false } = props; const { t } = useTranslation(); - const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl); - - useEffect(() => { - let isMounted = true; - - fetch(buildNodeDCBrandConfigUrl(), { cache: "no-store" }) - .then((response) => (response.ok ? response.json() : null)) - .then((payload: { logoLinkUrl?: string } | null) => { - if (isMounted && payload?.logoLinkUrl) setLogoLinkUrl(payload.logoLinkUrl); - return undefined; - }) - .catch((error: unknown) => { - console.warn(error instanceof Error ? error.message : "Не удалось загрузить brand config NODE.DC"); - }); - - return () => { - isMounted = false; - }; - }, []); + const logoLinkUrl = useNodeDCBrandLinkUrl(); return (
diff --git a/plane-src/apps/web/core/components/onboarding/header.tsx b/plane-src/apps/web/core/components/onboarding/header.tsx index 21bb081..996f727 100644 --- a/plane-src/apps/web/core/components/onboarding/header.tsx +++ b/plane-src/apps/web/core/components/onboarding/header.tsx @@ -8,12 +8,14 @@ import { observer } from "mobx-react"; // plane imports import { PlaneLockup } from "@plane/propel/icons"; // hooks +import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; import { useUser } from "@/hooks/store/user"; // local imports import { SwitchAccountDropdown } from "./switch-account-dropdown"; export const OnboardingHeader = observer(function OnboardingHeader() { const { data: user } = useUser(); + const logoLinkUrl = useNodeDCBrandLinkUrl(); const userName = user?.display_name ? user.display_name @@ -27,7 +29,9 @@ export const OnboardingHeader = observer(function OnboardingHeader() {
- + + +
diff --git a/plane-src/apps/web/core/hooks/use-nodedc-brand-link-url.ts b/plane-src/apps/web/core/hooks/use-nodedc-brand-link-url.ts new file mode 100644 index 0000000..46acd8d --- /dev/null +++ b/plane-src/apps/web/core/hooks/use-nodedc-brand-link-url.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from "react"; +import { buildNodeDCBrandConfigUrl, buildNodeDCLauncherUrl } from "@/helpers/nodedc-auth"; + +type TNodeDCBrandPayload = { + logoLinkUrl?: string | null; +}; + +export function useNodeDCBrandLinkUrl() { + const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl); + + useEffect(() => { + let isMounted = true; + + fetch(buildNodeDCBrandConfigUrl(), { cache: "no-store" }) + .then((response) => (response.ok ? response.json() : null)) + .then((payload: TNodeDCBrandPayload | null) => { + const configuredUrl = typeof payload?.logoLinkUrl === "string" ? payload.logoLinkUrl.trim() : ""; + if (isMounted && configuredUrl) setLogoLinkUrl(configuredUrl); + return undefined; + }) + .catch((error: unknown) => { + console.warn(error instanceof Error ? error.message : "Не удалось загрузить brand config NODE.DC"); + }); + + return () => { + isMounted = false; + }; + }, []); + + return logoLinkUrl; +} diff --git a/plane-src/rebuild-nas-legacy.sh b/plane-src/rebuild-nas-legacy.sh new file mode 100755 index 0000000..5d4acec --- /dev/null +++ b/plane-src/rebuild-nas-legacy.sh @@ -0,0 +1,150 @@ +#!/bin/sh +set -eu + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) + +DOCKER=${DOCKER:-/usr/local/bin/docker} +SUDO_CMD=${SUDO_CMD:-sudo} +SRC=${SRC:-$SCRIPT_DIR} +APP=${APP:-$(CDPATH= cd -- "$SRC/../plane-app" && pwd)} + +COMPOSE_PROJECT=${COMPOSE_PROJECT:-nodedc-tasker} +COMPOSE_ENV_FILE=${COMPOSE_ENV_FILE:-.env.synology} +COMPOSE_FILE=${COMPOSE_FILE:-docker-compose.yaml} +COMPOSE_OVERRIDE_FILE=${COMPOSE_OVERRIDE_FILE:-docker-compose.synology.override.yml} + +BUILD_BACKEND=${BUILD_BACKEND:-1} +BUILD_WEB=${BUILD_WEB:-1} +BUILD_ADMIN=${BUILD_ADMIN:-0} + +BACKEND_BUILDKIT=${BACKEND_BUILDKIT:-0} +WEB_BUILDKIT=${WEB_BUILDKIT:-0} +ADMIN_BUILDKIT=${ADMIN_BUILDKIT:-1} + +PLANE_BACKEND_IMAGE=${PLANE_BACKEND_IMAGE:-nodedc/plane-backend:local} +PLANE_FRONTEND_IMAGE=${PLANE_FRONTEND_IMAGE:-nodedc/plane-frontend:ru} +PLANE_ADMIN_IMAGE=${PLANE_ADMIN_IMAGE:-nodedc/plane-admin:ru} + +VITE_NODEDC_LAUNCHER_URL=${VITE_NODEDC_LAUNCHER_URL:-https://hub.nodedc.ru} +VITE_NODEDC_OIDC_LOGIN_ENABLED=${VITE_NODEDC_OIDC_LOGIN_ENABLED:-1} + +RUN_SMOKE=${RUN_SMOKE:-1} +SMOKE_BASE_URL=${SMOKE_BASE_URL:-https://ops.nodedc.ru} +SMOKE_RESOLVE=${SMOKE_RESOLVE:-ops.nodedc.ru:443:127.0.0.1} + +RECREATE_SERVICES="" + +run() { + if [ -n "$SUDO_CMD" ]; then + "$SUDO_CMD" "$@" + else + "$@" + fi +} + +run_env() { + if [ -n "$SUDO_CMD" ]; then + "$SUDO_CMD" env "$@" + else + env "$@" + fi +} + +compose_up() { + cd "$APP" + set -- compose -p "$COMPOSE_PROJECT" --env-file "$COMPOSE_ENV_FILE" -f "$COMPOSE_FILE" + + if [ -n "$COMPOSE_OVERRIDE_FILE" ] && [ -f "$COMPOSE_OVERRIDE_FILE" ]; then + set -- "$@" -f "$COMPOSE_OVERRIDE_FILE" + fi + + set -- "$@" up -d --no-build --force-recreate + for service in $RECREATE_SERVICES; do + set -- "$@" "$service" + done + + run "$DOCKER" "$@" +} + +curl_with_optional_resolve() { + if [ -n "$SMOKE_RESOLVE" ]; then + curl -k --resolve "$SMOKE_RESOLVE" "$@" + else + curl -k "$@" + fi +} + +if [ -n "$SUDO_CMD" ]; then + printf "== sudo session ==\n" + "$SUDO_CMD" -v +fi + +if [ "$BUILD_BACKEND" = "1" ]; then + printf "== backend image: %s ==\n" "$PLANE_BACKEND_IMAGE" + cd "$SRC/apps/api" + run_env DOCKER_BUILDKIT="$BACKEND_BUILDKIT" "$DOCKER" build \ + -t "$PLANE_BACKEND_IMAGE" \ + -f Dockerfile.api . + RECREATE_SERVICES="$RECREATE_SERVICES api worker beat-worker" +else + printf "== skip backend image ==\n" +fi + +if [ "$BUILD_WEB" = "1" ]; then + printf "== frontend image: %s ==\n" "$PLANE_FRONTEND_IMAGE" + cd "$SRC" + run_env DOCKER_BUILDKIT="$WEB_BUILDKIT" "$DOCKER" build \ + --build-arg "VITE_NODEDC_LAUNCHER_URL=$VITE_NODEDC_LAUNCHER_URL" \ + --build-arg "VITE_NODEDC_OIDC_LOGIN_ENABLED=$VITE_NODEDC_OIDC_LOGIN_ENABLED" \ + -t "$PLANE_FRONTEND_IMAGE" \ + -f apps/web/Dockerfile.web.nas-legacy . + RECREATE_SERVICES="$RECREATE_SERVICES web" +else + printf "== skip frontend image ==\n" +fi + +if [ "$BUILD_ADMIN" = "1" ]; then + printf "== admin image: %s ==\n" "$PLANE_ADMIN_IMAGE" + cd "$SRC" + run_env DOCKER_BUILDKIT="$ADMIN_BUILDKIT" "$DOCKER" build \ + --build-arg "VITE_NODEDC_LAUNCHER_URL=$VITE_NODEDC_LAUNCHER_URL" \ + -t "$PLANE_ADMIN_IMAGE" \ + -f apps/admin/Dockerfile.admin . + RECREATE_SERVICES="$RECREATE_SERVICES admin" +else + printf "== skip admin image ==\n" +fi + +if [ -z "$RECREATE_SERVICES" ]; then + printf "== nothing to recreate ==\n" + exit 0 +fi + +printf "== recreate tasker services ==\n" +compose_up + +printf "== containers ==\n" +run "$DOCKER" ps \ + --filter "name=$COMPOSE_PROJECT" \ + --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + +if [ "$RUN_SMOKE" = "1" ]; then + printf "== smoke: logo ==\n" + curl_with_optional_resolve -sSI \ + "$SMOKE_BASE_URL/nodedc-logo.svg" \ + | grep -Ei "HTTP/|content-type" || true + + printf "== smoke: websocket ==\n" + curl_with_optional_resolve -i --http1.1 \ + -H "Connection: Upgrade" \ + -H "Upgrade: websocket" \ + -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ + -H "Sec-WebSocket-Version: 13" \ + "$SMOKE_BASE_URL/live/nodedc/stream" \ + --max-time 35 \ + 2>/dev/null | sed -n "1,25p" || true +else + printf "== skip smoke checks ==\n" +fi + +printf "== done ==\n"