ARCH - TASKER DEPLOY: закрепление live baseline и env-driven сборки
This commit is contained in:
parent
c455ce3c34
commit
533f8c6356
|
|
@ -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.
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
node_modules
|
node_modules
|
||||||
.next
|
.next
|
||||||
.yarn
|
.yarn
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
### NextJS ###
|
### NextJS ###
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
@ -20,6 +21,8 @@ dist/
|
||||||
out/
|
out/
|
||||||
build/
|
build/
|
||||||
.react-router/
|
.react-router/
|
||||||
|
**/build/
|
||||||
|
**/.react-router/
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,9 @@ ENV VITE_WEB_BASE_URL=$VITE_WEB_BASE_URL
|
||||||
ARG VITE_WEB_BASE_PATH=""
|
ARG VITE_WEB_BASE_PATH=""
|
||||||
ENV VITE_WEB_BASE_PATH=$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"
|
ARG VITE_WEBSITE_URL="https://plane.so"
|
||||||
ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL
|
ENV VITE_WEBSITE_URL=$VITE_WEBSITE_URL
|
||||||
ARG VITE_SUPPORT_EMAIL="support@plane.so"
|
ARG VITE_SUPPORT_EMAIL="support@plane.so"
|
||||||
|
|
@ -67,8 +70,8 @@ COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
|
|
||||||
# Fetch dependencies to cache store, then install offline with dev deps
|
# 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 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 CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
|
||||||
|
|
||||||
# Build only the admin package
|
# Build only the admin package
|
||||||
RUN pnpm turbo run build --filter=admin
|
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 apps/admin/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/nodedcsudo
|
COPY --from=installer /app/apps/admin/build/client /usr/share/nginx/html/nodedcsudo
|
||||||
RUN chmod -R a+rX /usr/share/nginx/html
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,16 @@
|
||||||
* See the LICENSE file for details.
|
* See the LICENSE file for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Link from "next/link";
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
|
|
||||||
export function AuthHeader() {
|
export function AuthHeader() {
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
|
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
|
||||||
<Link href="/">
|
<a href={logoLinkUrl}>
|
||||||
<span className="tracking-normal text-16 font-semibold text-primary">NODE.DC</span>
|
<span className="tracking-normal text-16 font-semibold text-primary">NODE.DC</span>
|
||||||
</Link>
|
</a>
|
||||||
<span className="rounded-full bg-white/6 px-3 py-1 text-11 font-medium text-secondary">
|
<span className="rounded-full bg-white/6 px-3 py-1 text-11 font-medium text-secondary">
|
||||||
Глобальное администрирование
|
Глобальное администрирование
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import { cn, getFileURL } from "@plane/utils";
|
||||||
import NodeDcLogo from "@/app/assets/logos/nodedc-logo.svg?url";
|
import NodeDcLogo from "@/app/assets/logos/nodedc-logo.svg?url";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "@/hooks/store";
|
import { useUser } from "@/hooks/store";
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
// local imports
|
// local imports
|
||||||
|
|
||||||
const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
|
|
@ -42,6 +43,7 @@ export const AdminHeader = observer(function AdminHeader() {
|
||||||
const { currentUser, signOut } = useUser();
|
const { currentUser, signOut } = useUser();
|
||||||
const { resolvedTheme, setTheme } = useNextTheme();
|
const { resolvedTheme, setTheme } = useNextTheme();
|
||||||
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
const isFeatureRoute = FEATURE_NAVIGATION.some((item) => pathName?.startsWith(item.href));
|
const isFeatureRoute = FEATURE_NAVIGATION.some((item) => pathName?.startsWith(item.href));
|
||||||
const adminName = currentUser?.display_name || currentUser?.email || "Глобальный админ";
|
const adminName = currentUser?.display_name || currentUser?.email || "Глобальный админ";
|
||||||
|
|
@ -60,7 +62,7 @@ export const AdminHeader = observer(function AdminHeader() {
|
||||||
return (
|
return (
|
||||||
<header className="nodedc-admin-header relative z-30 flex w-full flex-shrink-0 flex-col gap-4">
|
<header className="nodedc-admin-header relative z-30 flex w-full flex-shrink-0 flex-col gap-4">
|
||||||
<div className="nodedc-admin-header-top grid w-full items-center gap-4">
|
<div className="nodedc-admin-header-top grid w-full items-center gap-4">
|
||||||
<a href="/" className="nodedc-admin-logo-link inline-flex w-fit items-center" aria-label="NODE.DC">
|
<a href={logoLinkUrl} className="nodedc-admin-logo-link inline-flex w-fit items-center" aria-label="NODE.DC">
|
||||||
<img src={NodeDcLogo} alt="NODE.DC" className="nodedc-admin-logo" />
|
<img src={NodeDcLogo} alt="NODE.DC" className="nodedc-admin-logo" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -40,24 +40,11 @@ COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
|
|
||||||
# Fetch dependencies to cache store, then install offline with dev deps
|
# 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 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 CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
|
||||||
|
|
||||||
ENV TURBO_TELEMETRY_DISABLED=1
|
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
|
RUN pnpm turbo run build --filter=live
|
||||||
|
|
||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,8 @@ COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
|
|
||||||
# Fetch dependencies to cache store, then install offline with dev deps
|
# 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 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 CI=true pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
|
||||||
|
|
||||||
# Build only the space package
|
# Build only the space package
|
||||||
RUN pnpm turbo run build --filter=space
|
RUN pnpm turbo run build --filter=space
|
||||||
|
|
|
||||||
|
|
@ -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;"]
|
||||||
|
|
@ -8,11 +8,10 @@
|
||||||
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { Shapes } from "lucide-react";
|
import { Shapes } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { TopNavPowerK } from "@/components/navigation";
|
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 { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||||
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
import { WorkspaceMenuRoot } from "@/components/workspace/sidebar/workspace-menu-root";
|
||||||
import { useHome } from "@/hooks/store/use-home";
|
import { useHome } from "@/hooks/store/use-home";
|
||||||
|
|
@ -32,26 +31,7 @@ export const ExpandedProjectShellToolbarLayout = ({
|
||||||
}: TProjectShellToolbarLayoutProps) => {
|
}: TProjectShellToolbarLayoutProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { toggleWidgetSettings } = useHome();
|
const { toggleWidgetSettings } = useHome();
|
||||||
const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl);
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("nodedc-expanded-toolbar-shell", { "nodedc-home-top-toolbar": isWorkspaceHome })}>
|
<div className={cn("nodedc-expanded-toolbar-shell", { "nodedc-home-top-toolbar": isWorkspaceHome })}>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { useTranslation } from "@plane/i18n";
|
||||||
import { PlaneLockup } from "@plane/propel/icons";
|
import { PlaneLockup } from "@plane/propel/icons";
|
||||||
import { PageHead } from "@/components/core/page-title";
|
import { PageHead } from "@/components/core/page-title";
|
||||||
import { EAuthModes } from "@/helpers/authentication.helper";
|
import { EAuthModes } from "@/helpers/authentication.helper";
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
import { useInstance } from "@/hooks/store/use-instance";
|
import { useInstance } from "@/hooks/store/use-instance";
|
||||||
|
|
||||||
const authContentMap = {
|
const authContentMap = {
|
||||||
|
|
@ -68,17 +69,19 @@ type TAuthHeaderBase = {
|
||||||
|
|
||||||
export function AuthHeaderBase(props: TAuthHeaderBase) {
|
export function AuthHeaderBase(props: TAuthHeaderBase) {
|
||||||
const { pageTitle, additionalAction } = props;
|
const { pageTitle, additionalAction } = props;
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={pageTitle + " - NODE.DC"} />
|
<PageHead title={pageTitle + " - NODE.DC"} />
|
||||||
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6 px-2 py-1">
|
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6 px-2 py-1">
|
||||||
<Link href="/">
|
<a href={logoLinkUrl}>
|
||||||
<PlaneLockup
|
<PlaneLockup
|
||||||
height={31}
|
height={31}
|
||||||
width={148}
|
width={148}
|
||||||
className="nodedc-auth-logo-lockup text-primary transition-opacity hover:opacity-90"
|
className="nodedc-auth-logo-lockup text-primary transition-opacity hover:opacity-90"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</a>
|
||||||
{additionalAction}
|
{additionalAction}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,23 @@ import GradientBgLogo from "@/app/assets/auth/gradient-bg-logo.webp?url";
|
||||||
import DefaultLayout from "@/layouts/default-layout";
|
import DefaultLayout from "@/layouts/default-layout";
|
||||||
import { PlaneLockup } from "@plane/propel/icons";
|
import { PlaneLockup } from "@plane/propel/icons";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
|
|
||||||
export function InstanceNotReady() {
|
export function InstanceNotReady() {
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<div className="relative z-10 flex h-screen w-screen overflow-hidden">
|
<div className="relative z-10 flex h-screen w-screen overflow-hidden">
|
||||||
<div className="flex h-full w-full flex-col items-center px-8 pt-6 pb-10">
|
<div className="flex h-full w-full flex-col items-center px-8 pt-6 pb-10">
|
||||||
<div className="sticky top-0 flex w-full shrink-0 items-center justify-between gap-6">
|
<div className="sticky top-0 flex w-full shrink-0 items-center justify-between gap-6">
|
||||||
<PlaneLockup height={40} width={190} className="nodedc-auth-logo-lockup text-primary" />
|
<a href={logoLinkUrl} aria-label="NODE.DC">
|
||||||
|
<PlaneLockup
|
||||||
|
height={40}
|
||||||
|
width={190}
|
||||||
|
className="nodedc-auth-logo-lockup text-primary transition-opacity hover:opacity-90"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-full w-full flex-col items-center justify-center gap-7">
|
<div className="flex h-full w-full flex-col items-center justify-center gap-7">
|
||||||
<div className="nodedc-error-shell flex max-w-3xl flex-col items-center gap-11 text-center">
|
<div className="nodedc-error-shell flex max-w-3xl flex-col items-center gap-11 text-center">
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
import { InboxIcon } from "@plane/propel/icons";
|
import { InboxIcon } from "@plane/propel/icons";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
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";
|
import { UserMenuRoot } from "@/components/workspace/sidebar/user-menu-root";
|
||||||
|
|
||||||
type TNodeDCStandaloneShellProps = {
|
type TNodeDCStandaloneShellProps = {
|
||||||
|
|
@ -24,25 +23,7 @@ type TNodeDCStandaloneShellProps = {
|
||||||
export const NodeDCStandaloneShell = (props: TNodeDCStandaloneShellProps) => {
|
export const NodeDCStandaloneShell = (props: TNodeDCStandaloneShellProps) => {
|
||||||
const { children, notificationsCount = 0, onOpenNotifications, showUserControls = false } = props;
|
const { children, notificationsCount = 0, onOpenNotifications, showUserControls = false } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [logoLinkUrl, setLogoLinkUrl] = useState(buildNodeDCLauncherUrl);
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex min-h-screen w-full overflow-hidden bg-[#050507] text-primary">
|
<div className="relative flex min-h-screen w-full overflow-hidden bg-[#050507] text-primary">
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,14 @@ import { observer } from "mobx-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { PlaneLockup } from "@plane/propel/icons";
|
import { PlaneLockup } from "@plane/propel/icons";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useNodeDCBrandLinkUrl } from "@/hooks/use-nodedc-brand-link-url";
|
||||||
import { useUser } from "@/hooks/store/user";
|
import { useUser } from "@/hooks/store/user";
|
||||||
// local imports
|
// local imports
|
||||||
import { SwitchAccountDropdown } from "./switch-account-dropdown";
|
import { SwitchAccountDropdown } from "./switch-account-dropdown";
|
||||||
|
|
||||||
export const OnboardingHeader = observer(function OnboardingHeader() {
|
export const OnboardingHeader = observer(function OnboardingHeader() {
|
||||||
const { data: user } = useUser();
|
const { data: user } = useUser();
|
||||||
|
const logoLinkUrl = useNodeDCBrandLinkUrl();
|
||||||
|
|
||||||
const userName = user?.display_name
|
const userName = user?.display_name
|
||||||
? user.display_name
|
? user.display_name
|
||||||
|
|
@ -27,7 +29,9 @@ export const OnboardingHeader = observer(function OnboardingHeader() {
|
||||||
<div className="h-full w-full bg-accent-primary" />
|
<div className="h-full w-full bg-accent-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full items-center justify-between gap-6 px-6">
|
<div className="flex w-full items-center justify-between gap-6 px-6">
|
||||||
<PlaneLockup height={20} width={95} className="text-primary" />
|
<a href={logoLinkUrl} aria-label="NODE.DC">
|
||||||
|
<PlaneLockup height={20} width={95} className="text-primary transition-opacity hover:opacity-90" />
|
||||||
|
</a>
|
||||||
<SwitchAccountDropdown fullName={userName} />
|
<SwitchAccountDropdown fullName={userName} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue