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 (
+
+ );
+}
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 (
+
+
+
+
+
+
+
+
+
+
+
+ );
+});
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 (
+
+
+
+
+
+
+ {!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 (
+
+
+
+ );
+}
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 (
+
+
+
+
+
+
+
+
+ {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"