diff --git a/plane-src/apps/web/app/root.tsx b/plane-src/apps/web/app/root.tsx index 7e6ac34..7705c4e 100644 --- a/plane-src/apps/web/app/root.tsx +++ b/plane-src/apps/web/app/root.tsx @@ -10,7 +10,7 @@ import { Links, Meta, Outlet, Scripts } from "react-router"; import type { LinksFunction } from "react-router"; import { ThemeProvider } from "next-themes"; // plane imports -import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants"; +import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, SITE_DESCRIPTION, SITE_NAME, THEMES } from "@plane/constants"; import { cn } from "@plane/utils"; // types // assets @@ -24,9 +24,12 @@ import { NodeDCSessionSync } from "@/components/auth-screens/nodedc-session-sync import { CustomErrorComponent } from "./error"; import { AppProvider } from "./provider"; // fonts +// oxlint-disable-next-line import/no-unassigned-import import "@fontsource-variable/inter"; import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url"; +// oxlint-disable-next-line import/no-unassigned-import import "@fontsource/material-symbols-rounded"; +// oxlint-disable-next-line import/no-unassigned-import import "@fontsource/ibm-plex-mono"; const APP_TITLE = "NODE.DC | Self-hosted task management workspace."; @@ -110,15 +113,20 @@ export function Layout({ children }: { children: ReactNode }) { const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0"); return ( - + - + {/* Meta info for PWA */} - + @@ -128,7 +136,14 @@ export function Layout({ children }: { children: ReactNode }) {
- + {children} diff --git a/plane-src/apps/web/ce/components/preferences/theme-switcher.tsx b/plane-src/apps/web/ce/components/preferences/theme-switcher.tsx index b335b99..ad61016 100644 --- a/plane-src/apps/web/ce/components/preferences/theme-switcher.tsx +++ b/plane-src/apps/web/ce/components/preferences/theme-switcher.tsx @@ -9,7 +9,7 @@ import { observer } from "mobx-react"; import { useTheme } from "next-themes"; // plane imports import type { I_THEME_OPTION } from "@plane/constants"; -import { THEME_OPTIONS } from "@plane/constants"; +import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, THEME_OPTIONS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { setPromiseToast } from "@plane/propel/toast"; import { applyCustomTheme } from "@plane/utils"; @@ -35,18 +35,20 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: { const { t } = useTranslation(); // derived values const currentTheme = useMemo(() => { - const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme); - return userThemeOption || null; + const themeValue = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : userProfile?.theme?.theme; + return THEME_OPTIONS.find((themeOption) => themeOption.value === themeValue) || null; }, [userProfile?.theme?.theme]); const handleThemeChange = useCallback( async (themeOption: I_THEME_OPTION) => { try { - setTheme(themeOption.value); + const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : themeOption.value; + setTheme(nextTheme); // If switching to custom theme and user has saved custom colors, apply them immediately if ( - themeOption.value === "custom" && + !NODEDC_THEME_LOCK_ENABLED && + nextTheme === "custom" && userProfile?.theme?.primary && userProfile?.theme?.background && userProfile?.theme?.darkPalette !== undefined @@ -58,7 +60,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: { ); } - const updatePromise = updateUserTheme({ theme: themeOption.value }); + const updatePromise = updateUserTheme({ theme: nextTheme }); setPromiseToast(updatePromise, { loading: "Updating theme...", success: { @@ -96,7 +98,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: { /> } /> - {userProfile.theme?.theme === "custom" && } + {!NODEDC_THEME_LOCK_ENABLED && userProfile.theme?.theme === "custom" && } ); }); diff --git a/plane-src/apps/web/core/components/appearance/theme-switcher.tsx b/plane-src/apps/web/core/components/appearance/theme-switcher.tsx index 4ec2eba..1b47e2e 100644 --- a/plane-src/apps/web/core/components/appearance/theme-switcher.tsx +++ b/plane-src/apps/web/core/components/appearance/theme-switcher.tsx @@ -9,7 +9,7 @@ import { observer } from "mobx-react"; import { useTheme } from "next-themes"; // plane imports import type { I_THEME_OPTION } from "@plane/constants"; -import { THEME_OPTIONS } from "@plane/constants"; +import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, THEME_OPTIONS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { setPromiseToast } from "@plane/propel/toast"; // components @@ -34,15 +34,16 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: { const { t } = useTranslation(); // derived values const currentTheme = useMemo(() => { - const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme); - return userThemeOption || null; + const themeValue = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : userProfile?.theme?.theme; + return THEME_OPTIONS.find((themeOption) => themeOption.value === themeValue) || null; }, [userProfile?.theme?.theme]); const handleThemeChange = useCallback( (themeOption: I_THEME_OPTION) => { try { - setTheme(themeOption.value); - const updatePromise = updateUserTheme({ theme: themeOption.value }); + const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : themeOption.value; + setTheme(nextTheme); + const updatePromise = updateUserTheme({ theme: nextTheme }); setPromiseToast(updatePromise, { loading: "Updating theme...", success: { @@ -58,7 +59,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: { console.error("Error updating theme:", error); } }, - [updateUserTheme] + [setTheme, updateUserTheme] ); if (!userProfile) return null; @@ -70,7 +71,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: { description={t(props.option.description)} control={} /> - {userProfile.theme?.theme === "custom" && } + {!NODEDC_THEME_LOCK_ENABLED && userProfile.theme?.theme === "custom" && } ); }); diff --git a/plane-src/apps/web/core/components/core/theme/theme-switch.tsx b/plane-src/apps/web/core/components/core/theme/theme-switch.tsx index fc34351..94b1a78 100644 --- a/plane-src/apps/web/core/components/core/theme/theme-switch.tsx +++ b/plane-src/apps/web/core/components/core/theme/theme-switch.tsx @@ -6,7 +6,7 @@ // plane imports import type { I_THEME_OPTION } from "@plane/constants"; -import { THEME_OPTIONS } from "@plane/constants"; +import { NODEDC_ACTIVE_THEME_OPTIONS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // constants import { SelectionDropdown } from "@/components/common/selection-dropdown"; @@ -17,34 +17,34 @@ type Props = { onChange: (value: I_THEME_OPTION) => void; }; +const renderThemeSwatch = (themeOption: I_THEME_OPTION) => ( +
+
+
+
+); + export function ThemeSwitch(props: Props) { const { value, onChange } = props; // translation const { t } = useTranslation(); - const renderThemeSwatch = (themeOption: I_THEME_OPTION) => ( -
-
-
-
- ); - return ( ({ + options={NODEDC_ACTIVE_THEME_OPTIONS.map((themeOption) => ({ key: themeOption.value, isChecked: value?.value === themeOption.value, onClick: () => onChange(themeOption), diff --git a/plane-src/apps/web/core/components/onboarding/switch-account-modal.tsx b/plane-src/apps/web/core/components/onboarding/switch-account-modal.tsx index aa80f8e..3a0942e 100644 --- a/plane-src/apps/web/core/components/onboarding/switch-account-modal.tsx +++ b/plane-src/apps/web/core/components/onboarding/switch-account-modal.tsx @@ -9,6 +9,7 @@ import React, { useState } from "react"; import { useTheme } from "next-themes"; import { ArrowRightLeft } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; +import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants"; // ui import { Button } from "@plane/propel/button"; import { TOAST_TYPE, setToast } from "@plane/propel/toast"; @@ -42,9 +43,10 @@ export function SwitchAccountModal(props: Props) { await signOut() .then(() => { - setTheme("system"); + setTheme(NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : "system"); router.push("/"); handleClose(); + return undefined; }) .catch(() => setToast({ diff --git a/plane-src/apps/web/core/components/power-k/config/preferences-commands.ts b/plane-src/apps/web/core/components/power-k/config/preferences-commands.ts index b1a9e97..e98a5e3 100644 --- a/plane-src/apps/web/core/components/power-k/config/preferences-commands.ts +++ b/plane-src/apps/web/core/components/power-k/config/preferences-commands.ts @@ -8,6 +8,7 @@ import { useCallback } from "react"; import { useTheme } from "next-themes"; import { Calendar, Earth, Languages, Palette } from "lucide-react"; // plane imports +import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { setToast, TOAST_TYPE } from "@plane/propel/toast"; import type { EStartOfTheWeek, TUserProfile } from "@plane/types"; @@ -29,8 +30,9 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => { const handleUpdateTheme = useCallback( async (newTheme: string) => { - setTheme(newTheme); - return updateUserTheme({ theme: newTheme }) + const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : newTheme; + setTheme(nextTheme); + return updateUserTheme({ theme: nextTheme }) .then(() => { setToast({ type: TOAST_TYPE.SUCCESS, diff --git a/plane-src/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx b/plane-src/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx index 6018fe2..95d6a35 100644 --- a/plane-src/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx +++ b/plane-src/apps/web/core/components/power-k/ui/pages/preferences/themes-menu.tsx @@ -8,7 +8,7 @@ import React, { useEffect, useState } from "react"; import { Command } from "cmdk"; import { observer } from "mobx-react"; // plane imports -import { THEME_OPTIONS } from "@plane/constants"; +import { NODEDC_ACTIVE_THEME_OPTIONS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; // local imports import { PowerKModalCommandItem } from "../../modal/command-item"; @@ -33,7 +33,7 @@ export const PowerKPreferencesThemesMenu = observer(function PowerKPreferencesTh return ( - {THEME_OPTIONS.map((theme) => ( + {NODEDC_ACTIVE_THEME_OPTIONS.map((theme) => ( onSelect(theme.value)} label={t(theme.i18n_label)} /> ))} diff --git a/plane-src/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx b/plane-src/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx index 7a191c2..69f2e45 100644 --- a/plane-src/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx +++ b/plane-src/apps/web/core/components/workspace/settings/codex-agent-api-settings.tsx @@ -137,7 +137,10 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti }) ) ); - const workspaceProjectGroups = projectAccessPayload?.workspaces ?? []; + const workspaceProjectGroups = useMemo( + () => projectAccessPayload?.workspaces ?? [], + [projectAccessPayload?.workspaces] + ); const projectOptions = useMemo(() => flattenProjectAccessOptions(workspaceProjectGroups), [workspaceProjectGroups]); const effectiveSelectedProjectKey = selectedProjectKey || projectOptions[0]?.key || ""; const setupCards = useMemo( @@ -735,6 +738,10 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) { В строке Authorization замените только ВАШ_УНИКАЛЬНЫЙ_ТОКЕН на значение из{" "} Agent token. Bearer и ndcag_ оставьте как есть.

+

+ Не используйте bearer_token_env_var, если переменная окружения не добавлена в macOS launchd: + Codex Desktop не увидит обычный export из shell-профиля. +

@@ -743,6 +750,10 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) { Сохраните config.toml и перезапустите локальный Codex. После старта попросите Codex проверить доступные проекты через tasker_list_projects.

+

+ Если токен или endpoint временно недоступны, чат Codex все равно откроется, но MCP tools появятся только + после исправления подключения. +

Правила работы и grants Codex получит сам через MCP по токену.

@@ -775,13 +786,13 @@ function buildCodexConfigSnippet(endpoint: string): string { return `[mcp_servers.${CODEX_MCP_SERVER_NAME}] url = "${endpoint}" enabled = true -required = true +required = false startup_timeout_sec = 20 tool_timeout_sec = 60 [mcp_servers.${CODEX_MCP_SERVER_NAME}.headers] Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН" -Accept = "application/json, text/event-stream" +Accept = "application/json" "MCP-Protocol-Version" = "2025-06-18"`; } @@ -995,6 +1006,7 @@ function mergeTokens(primaryTokens: TCodexAgentToken[], secondaryTokens: TCodexA const tokensById = new Map(); for (const token of primaryTokens) tokensById.set(token.id, token); for (const token of secondaryTokens) tokensById.set(token.id, tokensById.get(token.id) ?? token); + // oxlint-disable-next-line unicorn/no-array-sort return Array.from(tokensById.values()).sort( (leftToken, rightToken) => new Date(rightToken.created_at).getTime() - new Date(leftToken.created_at).getTime() ); @@ -1072,13 +1084,16 @@ function buildProjectGrantPayload(grantKeys: string[]): { workspace_slug: string } function getGrantedProjectKeys(grants: TCodexAgentGrant[]): string[] { - return [ + const grantedProjectKeys = [ ...new Set( grants .filter((grant) => grant.project_id) .map((grant) => buildProjectGrantKey(grant.workspace_slug, String(grant.project_id))) ), - ].sort(); + ]; + + // oxlint-disable-next-line unicorn/no-array-sort + return grantedProjectKeys.sort(); } type TMergeAgentGrantsOptions = { diff --git a/plane-src/apps/web/core/lib/wrappers/store-wrapper.tsx b/plane-src/apps/web/core/lib/wrappers/store-wrapper.tsx index 1b848c4..dbee0d3 100644 --- a/plane-src/apps/web/core/lib/wrappers/store-wrapper.tsx +++ b/plane-src/apps/web/core/lib/wrappers/store-wrapper.tsx @@ -9,6 +9,7 @@ import { useEffect, useRef } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import { useTheme } from "next-themes"; +import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants"; import type { TLanguage } from "@plane/i18n"; import { DEFAULT_LANGUAGE, useTranslation } from "@plane/i18n"; // helpers @@ -57,16 +58,13 @@ function StoreWrapper(props: TStoreWrapper) { */ useEffect(() => { const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed"); - const localBoolValue = localValue ? (localValue === "true" ? true : false) : false; + const localBoolValue = localValue === "true"; if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue); }, [sidebarCollapsed, setTheme, toggleSidebar]); /** - * Effect 1: Initial theme sync from server (one-time only) - * - * This effect runs ONCE per user session to load theme from server. - * After initial load, all theme changes are localStorage-driven (next-themes). - * This prevents a feedback loop where server updates trigger UI updates in a cycle. + * Temporary NODE.DC theme lock: keep the operational dark palette active + * until the light theme gets a full pass. */ useEffect(() => { const userId = userProfile?.id; @@ -79,6 +77,16 @@ function StoreWrapper(props: TStoreWrapper) { currentUserIdRef.current = userId; } + if (NODEDC_THEME_LOCK_ENABLED) { + if (!userProfile?.id || hasInitializedThemeRef.current) { + return; + } + + setTheme(NODEDC_FORCED_THEME); + hasInitializedThemeRef.current = true; + return; + } + // Only initialize theme from server on FIRST load for this user if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) { return; // Skip if already initialized or no profile data @@ -89,7 +97,7 @@ function StoreWrapper(props: TStoreWrapper) { // Mark as initialized - prevents future syncs from server hasInitializedThemeRef.current = true; - }, [userProfile?.theme?.theme, setTheme]); + }, [userProfile?.id, userProfile?.theme?.theme, setTheme]); /** * Effect 2: Custom theme CSS application (runs on every change) @@ -98,12 +106,21 @@ function StoreWrapper(props: TStoreWrapper) { * the theme changes. It runs independently of the initial sync effect. */ useEffect(() => { - if (!userProfile?.theme?.theme) return; + if (NODEDC_THEME_LOCK_ENABLED) { + clearCustomTheme(); + return; + } + + if (!userProfile?.theme?.theme) { + return; + } const currentTheme = userProfile?.theme?.theme; const previousTheme = previousThemeRef.current; const themeData = userProfile?.theme; + if (!currentTheme) return; + // Apply custom theme if current theme is custom if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) { applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light"); @@ -116,7 +133,7 @@ function StoreWrapper(props: TStoreWrapper) { // Update previous theme for next comparison previousThemeRef.current = currentTheme; - }, [userProfile?.theme]); + }, [userProfile?.id, userProfile?.theme]); useEffect(() => { if (!userProfile?.id) return; @@ -138,7 +155,7 @@ function StoreWrapper(props: TStoreWrapper) { useEffect(() => { if (!userProfile?.id) return; changeLanguage((userProfile?.language as TLanguage) || DEFAULT_LANGUAGE); - }, [userProfile?.language, changeLanguage]); + }, [userProfile?.id, userProfile?.language, changeLanguage]); useEffect(() => { if (!params) return; diff --git a/plane-src/packages/constants/src/themes.ts b/plane-src/packages/constants/src/themes.ts index 4c2cf21..028bb0a 100644 --- a/plane-src/packages/constants/src/themes.ts +++ b/plane-src/packages/constants/src/themes.ts @@ -4,7 +4,9 @@ * See the LICENSE file for details. */ -export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"]; +export const NODEDC_FORCED_THEME = "dark"; +export const NODEDC_THEME_LOCK_ENABLED = true; +export const THEMES = ["light", NODEDC_FORCED_THEME, "light-contrast", "dark-contrast", "custom"]; export interface I_THEME_OPTION { key: string; @@ -43,7 +45,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [ }, { key: "dark", - value: "dark", + value: NODEDC_FORCED_THEME, i18n_label: "Dark", type: "dark", icon: { @@ -86,3 +88,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [ }, }, ]; + +export const NODEDC_ACTIVE_THEME_OPTIONS = NODEDC_THEME_LOCK_ENABLED + ? THEME_OPTIONS.filter((themeOption) => themeOption.value === NODEDC_FORCED_THEME) + : THEME_OPTIONS;