diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/README.md b/infra/synology/tasker-overlays/logo-link-brand-hotfix/README.md new file mode 100644 index 0000000..c5ee26a --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/README.md @@ -0,0 +1,30 @@ +# Tasker overlay: editable NODE.DC logo link + +This overlay captures the Synology Tasker source hotfix that routes NODE.DC logo clicks through the Hub public brand config: + +- Hub source of truth: `https://hub.nodedc.ru/api/public/brand` +- Editable admin field: Hub admin → Platform → Misc → Logo link +- Fallback for `*.nodedc.ru`: `https://hub.nodedc.ru/` + +## Apply to Synology source + +From the platform repo root on a machine that has `/Volumes/docker` mounted: + +```bash +rsync -av infra/synology/tasker-overlays/logo-link-brand-hotfix/files/ \ + /Volumes/docker/nodedc-platform/tasker/plane-src/ +``` + +Then deploy web only: + +```bash +cd /volume1/docker/nodedc-platform/tasker/plane-src +BUILD_BACKEND=0 BUILD_WEB=1 BUILD_ADMIN=0 sh rebuild-nas-legacy.sh +``` + +Admin image can be rebuilt separately if needed: + +```bash +cd /volume1/docker/nodedc-platform/tasker/plane-src +BUILD_BACKEND=0 BUILD_WEB=0 BUILD_ADMIN=1 sh rebuild-nas-legacy.sh +``` diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/.gitignore b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/.gitignore new file mode 100755 index 0000000..3e58a11 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/.gitignore @@ -0,0 +1,115 @@ +node_modules +.next +.yarn +.pnpm-store/ + +### NextJS ### +# Dependencies +/node_modules +/.pnp +.pnp.js + +# Testing +/coverage + +# Next.js +/.next/ +/out/ + +# Production +dist/ +out/ +build/ +.react-router/ +**/build/ +**/.react-router/ + +# Misc +.DS_Store +*.pem +.history +tsconfig.tsbuildinfo + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.pnpm-debug.log* + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Vercel +.vercel + +# Turborepo +.turbo + +## Django ## +venv +.venv +*.pyc +staticfiles +mediafiles +.env +.DS_Store +logs/ +htmlcov/ +.coverage + +node_modules/ +assets/dist/ +npm-debug.log +yarn-error.log +pnpm-debug.log + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +package-lock.json +.vscode + +# Sentry +.sentryclirc + +# lock files +package-lock.json + + + +.secrets +tmp/ + +## packages +dist +.temp/ +deploy/selfhost/plane-app/ + +## Storybook +*storybook.log +output.css + +dev-editor +# Redis +*.rdb +*.rdb.gz + +storybook-static + +CLAUDE.md + +build/ +.react-router/ + +build/ +.react-router/ +temp/ +scripts/ diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/Dockerfile.admin b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/Dockerfile.admin new file mode 100755 index 0000000..38d7045 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/Dockerfile.admin @@ -0,0 +1,91 @@ +FROM node:22-alpine AS base + +WORKDIR /app + +ENV TURBO_TELEMETRY_DISABLED=1 +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PNPM_HOME/bin:$PATH" +ENV CI=1 + +RUN corepack enable pnpm + +# =========================================================================== # + +FROM base AS builder + +RUN pnpm add -g turbo@2.9.4 + +COPY . . + +# Create a pruned workspace for just the admin app +RUN turbo prune --scope=admin --docker + +# =========================================================================== # + +FROM base AS installer + +# Build in production mode; we still install dev deps explicitly below +ENV NODE_ENV=production + +# Public envs required at build time (pick up via process.env) +ARG VITE_API_BASE_URL="" +ENV VITE_API_BASE_URL=$VITE_API_BASE_URL +ARG VITE_API_BASE_PATH="/api" +ENV VITE_API_BASE_PATH=$VITE_API_BASE_PATH + +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_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_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_WEB_BASE_URL="" +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" +ENV VITE_SUPPORT_EMAIL=$VITE_SUPPORT_EMAIL + +COPY .gitignore .gitignore +COPY --from=builder /app/out/json/ . +COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml + +# 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 --prod=false + +# Build only the admin package +RUN pnpm turbo run build --filter=admin + +# =========================================================================== # + +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 + +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/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/app/(all)/(home)/auth-header.tsx b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/app/(all)/(home)/auth-header.tsx new file mode 100755 index 0000000..52cfbb5 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/app/(all)/(home)/auth-header.tsx @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; + +export function AuthHeader() { + const logoLinkUrl = useNodeDCBrandLinkUrl(); + + return ( +
+ + NODE.DC + + + Глобальное администрирование + +
+ ); +} diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/components/common/header/index.tsx b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/components/common/header/index.tsx new file mode 100755 index 0000000..fcd9a03 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/components/common/header/index.tsx @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { Fragment, useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { useTheme as useNextTheme } from "next-themes"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { BrainCog, ChevronDown, ExternalLink, Image, LogOut, Mail, Palette, Settings, UserCog2 } from "lucide-react"; +import { Menu, Transition } from "@headlessui/react"; +// plane imports +import { API_BASE_URL } from "@plane/constants"; +import { LockIcon, WorkspaceIcon } from "@plane/propel/icons"; +import { AuthService } from "@plane/services"; +import { Avatar } from "@plane/ui"; +import { cn, getFileURL } from "@plane/utils"; +// assets +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(); + +const PRIMARY_NAVIGATION = [ + { label: "Основное", href: "/general/", Icon: Settings }, + { label: "Почта", href: "/email/", Icon: Mail }, + { label: "Аутентификация", href: "/authentication/", Icon: LockIcon }, + { label: "Воркспейсы", href: "/workspace/", Icon: WorkspaceIcon }, +]; + +const FEATURE_NAVIGATION = [ + { label: "ИИ", href: "/ai/", Icon: BrainCog, description: "OpenAI модель и API-ключ" }, + { label: "Изображения", href: "/image/", Icon: Image, description: "Внешние библиотеки изображений" }, +]; + +export const AdminHeader = observer(function AdminHeader() { + const pathName = usePathname(); + 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 || "Глобальный админ"; + const avatarName = currentUser?.display_name || currentUser?.email || "DC"; + + const handleThemeSwitch = () => { + const newTheme = resolvedTheme === "dark" ? "light" : "dark"; + setTheme(newTheme); + }; + + useEffect(() => { + if (csrfToken === undefined) + void authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token)); + }, [csrfToken]); + + return ( +
+
+ + NODE.DC + + + + + + + + {adminName} + + + {currentUser ? ( + + ) : ( + + )} + + + + +
+ {adminName} + {currentUser?.email} +
+
+ + + {resolvedTheme === "dark" ? "Светлая тема" : "Темная тема"} + +
+
+
+ + + + Выйти + +
+
+
+
+
+
+
+ ); +}); diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/helpers/nodedc-brand.ts b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/helpers/nodedc-brand.ts new file mode 100755 index 0000000..7032fea --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/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/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/hooks/use-nodedc-brand-link-url.ts b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/admin/hooks/use-nodedc-brand-link-url.ts new file mode 100755 index 0000000..9fdff67 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/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/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/expanded-layout.tsx b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/expanded-layout.tsx new file mode 100755 index 0000000..3825cd4 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/app/(all)/[workspaceSlug]/(projects)/top-toolbar/expanded-layout.tsx @@ -0,0 +1,91 @@ +"use client"; + +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import { useTranslation } from "@plane/i18n"; +import { Shapes } from "lucide-react"; +import { cn } from "@plane/utils"; +// components +import { TopNavPowerK } from "@/components/navigation"; +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"; +import { ProjectsToolbarMenu } from "./projects-toolbar-menu"; +import { ExpandedToolbarLink, ExpandedToolbarToolButton, ToolbarNotificationsButton } from "./toolbar-controls"; +// types +import type { TProjectShellToolbarLayoutProps } from "./types"; + +export const ExpandedProjectShellToolbarLayout = ({ + draftsItem, + homeItem, + isWorkspaceHome, + notificationsCount, + profileItem, + stickiesItem, + onOpenNotifications, +}: TProjectShellToolbarLayoutProps) => { + const { t } = useTranslation(); + const { toggleWidgetSettings } = useHome(); + const logoLinkUrl = useNodeDCBrandLinkUrl(); + + return ( +
+
+
+
+ + NODE DC + +
+ +
+ +
+ + + + +
+
+ +
+
+ + + +
+
+
+ +
+
+ {!isWorkspaceHome && ( +
+ +
+
+ )} +
+
+ {isWorkspaceHome && ( + toggleWidgetSettings(true)}> + + + )} +
+
+
+
+
+ ); +}; diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/auth-screens/header.tsx b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/auth-screens/header.tsx new file mode 100755 index 0000000..ffa90b4 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/auth-screens/header.tsx @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import React from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { AUTH_TRACKER_ELEMENTS } from "@plane/constants"; +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 = { + [EAuthModes.SIGN_IN]: { + pageTitle: "auth_actions.sign_up", + text: "auth.common.new_to_plane", + linkText: "auth_actions.sign_up", + linkHref: "/sign-up", + }, + [EAuthModes.SIGN_UP]: { + pageTitle: "auth_actions.sign_in", + text: "auth.common.already_have_an_account", + linkText: "auth_actions.sign_in", + linkHref: "/sign-in", + }, +}; + +type AuthHeaderProps = { + type: EAuthModes; +}; + +export const AuthHeader = observer(function AuthHeader({ type }: AuthHeaderProps) { + const { t } = useTranslation(); + // store + const { config } = useInstance(); + // derived values + const enableSignUpConfig = config?.enable_signup ?? false; + + return ( + + {t(authContentMap[type].text)} + + {t(authContentMap[type].linkText)} + +
+ ) + } + /> + ); +}); + +type TAuthHeaderBase = { + pageTitle: string; + additionalAction?: React.ReactNode; +}; + +export function AuthHeaderBase(props: TAuthHeaderBase) { + const { pageTitle, additionalAction } = props; + const logoLinkUrl = useNodeDCBrandLinkUrl(); + + return ( + <> + +
+ + + + {additionalAction} +
+ + ); +} diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/instance/not-ready-view.tsx b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/instance/not-ready-view.tsx new file mode 100755 index 0000000..4d02e6b --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/instance/not-ready-view.tsx @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import Link from "next/link"; +import { GOD_MODE_URL } from "@plane/constants"; +// assets +import GradientLogo from "@/app/assets/auth/gradient-logo.webp?url"; +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 ( + +
+
+
+ + + +
+
+
+ + + NODE.DC Logo +
+

NODE.DC готов к запуску

+

+ Завершите настройку инстанса и создайте первое рабочее пространство, чтобы начать работу с + проектами и рабочими элементами. +

+
+ + + + + Служба поддержки + +
+
+
+
+
+ ); +} diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/nodedc/standalone-shell.tsx b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/nodedc/standalone-shell.tsx new file mode 100755 index 0000000..8c61fa0 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/nodedc/standalone-shell.tsx @@ -0,0 +1,77 @@ +"use client"; + +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +import type { ReactNode } from "react"; +import { useTranslation } from "@plane/i18n"; +import { InboxIcon } from "@plane/propel/icons"; +import { Tooltip } from "@plane/propel/tooltip"; +import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url"; +import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root"; + +type TNodeDCStandaloneShellProps = { + children: ReactNode; + notificationsCount?: number; + onOpenNotifications?: () => void; + showUserControls?: boolean; +}; + +export const NodeDCStandaloneShell = (props: TNodeDCStandaloneShellProps) => { + const { children, notificationsCount = 0, onOpenNotifications, showUserControls = false } = props; + const { t } = useTranslation(); + const logoLinkUrl = useNodeDCBrandLinkUrl(); + + return ( +
+
+
+
+
+
+ +
+
+
+ + NODE DC + +
+
+
+ {showUserControls && ( +
+ {onOpenNotifications && ( + + + + )} + +
+ )} +
+
+
+ +
+ {children} +
+
+ ); +}; diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/onboarding/header.tsx b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/onboarding/header.tsx new file mode 100755 index 0000000..996f727 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/components/onboarding/header.tsx @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2023-present Plane Software, Inc. and contributors + * SPDX-License-Identifier: AGPL-3.0-only + * See the LICENSE file for details. + */ + +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 + : user?.first_name + ? `${user.first_name} ${user.last_name ?? ""}`.trim() + : user?.email; + + return ( +
+
+
+
+
+ + + + +
+
+ ); +}); diff --git a/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/hooks/use-nodedc-brand-link-url.ts b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/apps/web/core/hooks/use-nodedc-brand-link-url.ts new file mode 100755 index 0000000..46acd8d --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/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/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/rebuild-nas-legacy.sh b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/rebuild-nas-legacy.sh new file mode 100755 index 0000000..713de71 --- /dev/null +++ b/infra/synology/tasker-overlays/logo-link-brand-hotfix/files/rebuild-nas-legacy.sh @@ -0,0 +1,84 @@ +#!/bin/sh +set -eu + +DOCKER=/usr/local/bin/docker +SRC=/volume1/docker/nodedc-platform/tasker/plane-src +APP=/volume1/docker/nodedc-platform/tasker/plane-app +BUILD_BACKEND=${BUILD_BACKEND:-1} +BUILD_WEB=${BUILD_WEB:-1} +BUILD_ADMIN=${BUILD_ADMIN:-0} +RECREATE_SERVICES="" + +printf "== sudo session ==\n" +sudo -v + +if [ "$BUILD_BACKEND" = "1" ]; then + printf "== backend image: nodedc/plane-backend:local ==\n" + cd "$SRC/apps/api" + sudo DOCKER_BUILDKIT=0 "$DOCKER" build \ + -t nodedc/plane-backend:local \ + -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: nodedc/plane-frontend:ru ==\n" + cd "$SRC" + sudo DOCKER_BUILDKIT=0 "$DOCKER" build \ + --build-arg VITE_NODEDC_LAUNCHER_URL=https://hub.nodedc.ru \ + --build-arg VITE_NODEDC_OIDC_LOGIN_ENABLED=1 \ + -t nodedc/plane-frontend:ru \ + -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: nodedc/plane-admin:ru ==\n" + cd "$SRC" + sudo DOCKER_BUILDKIT=1 "$DOCKER" build \ + --build-arg VITE_NODEDC_LAUNCHER_URL=https://hub.nodedc.ru \ + -t nodedc/plane-admin:ru \ + -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" +cd "$APP" +sudo "$DOCKER" compose -p nodedc-tasker \ + --env-file .env.synology \ + -f docker-compose.yaml \ + -f docker-compose.synology.override.yml \ + up -d --no-build --force-recreate $RECREATE_SERVICES + +printf "== containers ==\n" +sudo "$DOCKER" ps \ + --filter "name=nodedc-tasker" \ + --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" + +printf "== smoke: logo ==\n" +curl -k -sSI --resolve ops.nodedc.ru:443:127.0.0.1 \ + https://ops.nodedc.ru/nodedc-logo.svg \ + | grep -Ei "HTTP/|content-type" || true + +printf "== smoke: websocket ==\n" +curl -k -i --http1.1 --resolve ops.nodedc.ru:443:127.0.0.1 \ + -H "Connection: Upgrade" \ + -H "Upgrade: websocket" \ + -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ + -H "Sec-WebSocket-Version: 13" \ + https://ops.nodedc.ru/live/nodedc/stream \ + --max-time 35 \ + 2>/dev/null | sed -n "1,25p" || true + +printf "== done ==\n"