Sync service launch links

This commit is contained in:
DCCONSTRUCTIONS 2026-05-02 13:07:18 +03:00
parent 17e007f49d
commit c6e1de6345
6 changed files with 69 additions and 34 deletions

View File

@ -231,7 +231,7 @@
"subtitle": "Агентная платформа", "subtitle": "Агентная платформа",
"description": "Сборка, запуск и мониторинг агентных workflow.", "description": "Сборка, запуск и мониторинг агентных workflow.",
"fullDescription": "NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.", "fullDescription": "NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.",
"url": "https://dev.handhdc.ru", "url": "https://dev.handhdc.ru/sso/launch",
"launchUrl": "https://dev.handhdc.ru/sso/launch", "launchUrl": "https://dev.handhdc.ru/sso/launch",
"accentColor": "#B5FF5A", "accentColor": "#B5FF5A",
"fallbackGradient": "linear-gradient(128deg, rgba(181, 255, 90, 0.84), rgba(37, 58, 36, 0.86) 42%, #0A0D10 82%)", "fallbackGradient": "linear-gradient(128deg, rgba(181, 255, 90, 0.84), rgba(37, 58, 36, 0.86) 42%, #0A0D10 82%)",
@ -257,7 +257,7 @@
"subtitle": "Операционный слой", "subtitle": "Операционный слой",
"description": "Задачи, контуры предприятия, процессы и AI-функции поверх задачника.", "description": "Задачи, контуры предприятия, процессы и AI-функции поверх задачника.",
"fullDescription": "Task Manager основан на архитектуре Plane и расширен AI-функциями NODE.DC.", "fullDescription": "Task Manager основан на архитектуре Plane и расширен AI-функциями NODE.DC.",
"url": "https://tasks.handhdc.ru", "url": "https://tasks.handhdc.ru/sso/launch",
"launchUrl": "https://tasks.handhdc.ru/sso/launch", "launchUrl": "https://tasks.handhdc.ru/sso/launch",
"accentColor": "#D7C8FF", "accentColor": "#D7C8FF",
"fallbackGradient": "linear-gradient(132deg, rgba(215, 200, 255, 0.82), rgba(51, 41, 79, 0.9) 46%, #0B0D10 84%)", "fallbackGradient": "linear-gradient(132deg, rgba(215, 200, 255, 0.82), rgba(51, 41, 79, 0.9) 46%, #0B0D10 84%)",
@ -279,7 +279,7 @@
"subtitle": "Бухгалтерский ассистент", "subtitle": "Бухгалтерский ассистент",
"description": "Вопросы к 1С, точные выборки и доказательная навигация по данным.", "description": "Вопросы к 1С, точные выборки и доказательная навигация по данным.",
"fullDescription": "Ассистент для бухгалтерских запросов, анализа операций, остатков и документов.", "fullDescription": "Ассистент для бухгалтерских запросов, анализа операций, остатков и документов.",
"url": "https://1c.handhdc.ru", "url": "https://1c.handhdc.ru/sso/launch",
"launchUrl": "https://1c.handhdc.ru/sso/launch", "launchUrl": "https://1c.handhdc.ru/sso/launch",
"accentColor": "#8FD7FF", "accentColor": "#8FD7FF",
"fallbackGradient": "linear-gradient(126deg, rgba(143, 215, 255, 0.8), rgba(32, 61, 80, 0.9) 44%, #080B0F 84%)", "fallbackGradient": "linear-gradient(126deg, rgba(143, 215, 255, 0.8), rgba(32, 61, 80, 0.9) 44%, #080B0F 84%)",
@ -301,7 +301,7 @@
"subtitle": "Госзакупки и тендеры", "subtitle": "Госзакупки и тендеры",
"description": "Поиск, анализ и подготовка тендерных решений.", "description": "Поиск, анализ и подготовка тендерных решений.",
"fullDescription": "Сервис собирает тендерные данные, строит выжимку рисков и помогает подготовить пакет участия.", "fullDescription": "Сервис собирает тендерные данные, строит выжимку рисков и помогает подготовить пакет участия.",
"url": "https://tender.handhdc.ru", "url": "https://tender.handhdc.ru/sso/launch",
"launchUrl": "https://tender.handhdc.ru/sso/launch", "launchUrl": "https://tender.handhdc.ru/sso/launch",
"accentColor": "#FFD166", "accentColor": "#FFD166",
"fallbackGradient": "linear-gradient(135deg, rgba(255, 209, 102, 0.84), rgba(74, 53, 19, 0.92) 42%, #0B0D10 86%)", "fallbackGradient": "linear-gradient(135deg, rgba(255, 209, 102, 0.84), rgba(74, 53, 19, 0.92) 42%, #0B0D10 86%)",
@ -327,7 +327,7 @@
"subtitle": "3D и пространственные данные", "subtitle": "3D и пространственные данные",
"description": "Просмотр цифровых двойников, карт и объектных сцен.", "description": "Просмотр цифровых двойников, карт и объектных сцен.",
"fullDescription": "Витрина геометрии, объектов, слоёв и статусов инфраструктуры.", "fullDescription": "Витрина геометрии, объектов, слоёв и статусов инфраструктуры.",
"url": "https://twin.handhdc.ru", "url": "https://launch.dcserve.ru/",
"launchUrl": "https://launch.dcserve.ru/", "launchUrl": "https://launch.dcserve.ru/",
"accentColor": "#76E4F7", "accentColor": "#76E4F7",
"fallbackGradient": "linear-gradient(140deg, rgba(118, 228, 247, 0.82), rgba(23, 69, 87, 0.92) 47%, #080B0F 86%)", "fallbackGradient": "linear-gradient(140deg, rgba(118, 228, 247, 0.82), rgba(23, 69, 87, 0.92) 47%, #080B0F 86%)",
@ -349,7 +349,7 @@
"subtitle": "Будущие модули", "subtitle": "Будущие модули",
"description": "Скрытый каталог модулей для root-admin preview.", "description": "Скрытый каталог модулей для root-admin preview.",
"fullDescription": "Площадка для будущих цифровых модулей NODE.DC.", "fullDescription": "Площадка для будущих цифровых модулей NODE.DC.",
"url": "https://dm.handhdc.ru", "url": "https://dm.handhdc.ru/sso/launch",
"launchUrl": "https://dm.handhdc.ru/sso/launch", "launchUrl": "https://dm.handhdc.ru/sso/launch",
"accentColor": "#FF9AC2", "accentColor": "#FF9AC2",
"fallbackGradient": "linear-gradient(135deg, rgba(255, 154, 194, 0.78), rgba(76, 41, 64, 0.9) 44%, #090B0F 86%)", "fallbackGradient": "linear-gradient(135deg, rgba(255, 154, 194, 0.78), rgba(76, 41, 64, 0.9) 44%, #090B0F 86%)",

View File

@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import type { Client } from "../entities/client/types"; import type { Client } from "../entities/client/types";
import type { Invite } from "../entities/invite/types"; import type { Invite } from "../entities/invite/types";
import { syncServiceLaunchLink } from "../entities/service/links";
import type { LauncherServiceView, Service } from "../entities/service/types"; import type { LauncherServiceView, Service } from "../entities/service/types";
import type { SyncStatus } from "../entities/sync/types"; import type { SyncStatus } from "../entities/sync/types";
import type { ClientGroup, ClientMembership, LauncherUser } from "../entities/user/types"; import type { ClientGroup, ClientMembership, LauncherUser } from "../entities/user/types";
@ -18,7 +19,7 @@ import { ServiceStage } from "../widgets/service-stage/ServiceStage";
import { TopBar } from "../widgets/top-bar/TopBar"; import { TopBar } from "../widgets/top-bar/TopBar";
export function LauncherApp() { export function LauncherApp() {
const [data, setData] = useState<LauncherData>(initialLauncherData); const [data, setData] = useState<LauncherData>(() => syncLauncherServiceLinks(initialLauncherData));
const [activeProfileId, setActiveProfileId] = useState(profileOptions[0].userId); const [activeProfileId, setActiveProfileId] = useState(profileOptions[0].userId);
const [activeClientId, setActiveClientId] = useState(profileOptions[0].defaultClientId); const [activeClientId, setActiveClientId] = useState(profileOptions[0].defaultClientId);
const [selectedServiceId, setSelectedServiceId] = useState<string | undefined>(); const [selectedServiceId, setSelectedServiceId] = useState<string | undefined>();
@ -51,7 +52,7 @@ export function LauncherApp() {
loadPersistedLauncherData() loadPersistedLauncherData()
.then((persistedData) => { .then((persistedData) => {
if (isMounted && persistedData) { if (isMounted && persistedData) {
setData(persistedData); setData(syncLauncherServiceLinks(persistedData));
} }
}) })
.finally(() => { .finally(() => {
@ -232,11 +233,11 @@ export function LauncherApp() {
...current, ...current,
services: current.services.map((service) => services: current.services.map((service) =>
service.id === serviceId service.id === serviceId
? { ? syncServiceLaunchLink({
...service, ...service,
...patch, ...patch,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
} })
: service : service
), ),
})); }));
@ -444,7 +445,7 @@ export function LauncherApp() {
subtitle: "Новый сервис", subtitle: "Новый сервис",
description: "Описание сервиса для витрины.", description: "Описание сервиса для витрины.",
fullDescription: "Заполните описание, медиа и ссылку запуска в редакторе контента.", fullDescription: "Заполните описание, медиа и ссылку запуска в редакторе контента.",
url: "https://service.handhdc.ru", url: "https://service.handhdc.ru/sso/launch",
launchUrl: "https://service.handhdc.ru/sso/launch", launchUrl: "https://service.handhdc.ru/sso/launch",
accentColor: "#F7F8F4", accentColor: "#F7F8F4",
fallbackGradient: "linear-gradient(135deg, rgba(247, 248, 244, 0.72), rgba(36, 37, 42, 0.9) 52%, #090B0F 88%)", fallbackGradient: "linear-gradient(135deg, rgba(247, 248, 244, 0.72), rgba(36, 37, 42, 0.9) 52%, #090B0F 88%)",
@ -529,3 +530,10 @@ export function LauncherApp() {
</div> </div>
); );
} }
function syncLauncherServiceLinks(data: LauncherData): LauncherData {
return {
...data,
services: data.services.map(syncServiceLaunchLink),
};
}

View File

@ -0,0 +1,21 @@
import type { Service } from "./types";
export function getServiceLaunchLink(service: Pick<Service, "url" | "launchUrl">): string {
return service.launchUrl?.trim() || service.url.trim();
}
export function createServiceLaunchLinkPatch(value: string): Pick<Service, "url" | "launchUrl"> {
const launchLink = value.trim();
return {
url: launchLink,
launchUrl: launchLink || null,
};
}
export function syncServiceLaunchLink(service: Service): Service {
return {
...service,
...createServiceLaunchLinkPatch(getServiceLaunchLink(service)),
};
}

View File

@ -2,6 +2,7 @@ import { computeEffectiveAccess } from "../../entities/access/computeEffectiveAc
import type { EffectiveAccessResult, ServiceAccessException, ServiceGrant } from "../../entities/access/types"; import type { EffectiveAccessResult, ServiceAccessException, ServiceGrant } from "../../entities/access/types";
import type { Client } from "../../entities/client/types"; import type { Client } from "../../entities/client/types";
import type { Invite } from "../../entities/invite/types"; import type { Invite } from "../../entities/invite/types";
import { getServiceLaunchLink } from "../../entities/service/links";
import type { LauncherServiceView, Service } from "../../entities/service/types"; import type { LauncherServiceView, Service } from "../../entities/service/types";
import type { SyncStatus } from "../../entities/sync/types"; import type { SyncStatus } from "../../entities/sync/types";
import type { import type {
@ -217,7 +218,7 @@ export function buildLauncherServices(data: LauncherData, userId: string, active
status: service.status, status: service.status,
userAccess: effectiveAccess.allowed ? ("allowed" as const) : ("denied" as const), userAccess: effectiveAccess.allowed ? ("allowed" as const) : ("denied" as const),
appRole: effectiveAccess.appRole, appRole: effectiveAccess.appRole,
openUrl: effectiveAccess.openEnabled ? service.launchUrl ?? service.url : null, openUrl: effectiveAccess.openEnabled ? getServiceLaunchLink(service) || null : null,
accentColor: service.accentColor, accentColor: service.accentColor,
media: { media: {
icon: service.iconUrl, icon: service.iconUrl,

View File

@ -150,7 +150,7 @@ export const mockServices: Service[] = [
description: "Сборка, запуск и мониторинг агентных workflow.", description: "Сборка, запуск и мониторинг агентных workflow.",
fullDescription: fullDescription:
"NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.", "NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.",
url: "https://dev.handhdc.ru", url: "https://dev.handhdc.ru/sso/launch",
launchUrl: "https://dev.handhdc.ru/sso/launch", launchUrl: "https://dev.handhdc.ru/sso/launch",
accentColor: "#B5FF5A", accentColor: "#B5FF5A",
fallbackGradient: "linear-gradient(128deg, rgba(181, 255, 90, 0.84), rgba(37, 58, 36, 0.86) 42%, #0A0D10 82%)", fallbackGradient: "linear-gradient(128deg, rgba(181, 255, 90, 0.84), rgba(37, 58, 36, 0.86) 42%, #0A0D10 82%)",
@ -168,7 +168,7 @@ export const mockServices: Service[] = [
subtitle: "Операционный слой", subtitle: "Операционный слой",
description: "Задачи, контуры предприятия, процессы и AI-функции поверх задачника.", description: "Задачи, контуры предприятия, процессы и AI-функции поверх задачника.",
fullDescription: "Task Manager основан на архитектуре Plane и расширен AI-функциями NODE.DC.", fullDescription: "Task Manager основан на архитектуре Plane и расширен AI-функциями NODE.DC.",
url: "https://tasks.handhdc.ru", url: "https://tasks.handhdc.ru/sso/launch",
launchUrl: "https://tasks.handhdc.ru/sso/launch", launchUrl: "https://tasks.handhdc.ru/sso/launch",
accentColor: "#D7C8FF", accentColor: "#D7C8FF",
fallbackGradient: "linear-gradient(132deg, rgba(215, 200, 255, 0.82), rgba(51, 41, 79, 0.9) 46%, #0B0D10 84%)", fallbackGradient: "linear-gradient(132deg, rgba(215, 200, 255, 0.82), rgba(51, 41, 79, 0.9) 46%, #0B0D10 84%)",
@ -186,7 +186,7 @@ export const mockServices: Service[] = [
subtitle: "Бухгалтерский ассистент", subtitle: "Бухгалтерский ассистент",
description: "Вопросы к 1С, точные выборки и доказательная навигация по данным.", description: "Вопросы к 1С, точные выборки и доказательная навигация по данным.",
fullDescription: "Ассистент для бухгалтерских запросов, анализа операций, остатков и документов.", fullDescription: "Ассистент для бухгалтерских запросов, анализа операций, остатков и документов.",
url: "https://1c.handhdc.ru", url: "https://1c.handhdc.ru/sso/launch",
launchUrl: "https://1c.handhdc.ru/sso/launch", launchUrl: "https://1c.handhdc.ru/sso/launch",
accentColor: "#8FD7FF", accentColor: "#8FD7FF",
fallbackGradient: "linear-gradient(126deg, rgba(143, 215, 255, 0.8), rgba(32, 61, 80, 0.9) 44%, #080B0F 84%)", fallbackGradient: "linear-gradient(126deg, rgba(143, 215, 255, 0.8), rgba(32, 61, 80, 0.9) 44%, #080B0F 84%)",
@ -204,7 +204,7 @@ export const mockServices: Service[] = [
subtitle: "Госзакупки и тендеры", subtitle: "Госзакупки и тендеры",
description: "Поиск, анализ и подготовка тендерных решений.", description: "Поиск, анализ и подготовка тендерных решений.",
fullDescription: "Сервис собирает тендерные данные, строит выжимку рисков и помогает подготовить пакет участия.", fullDescription: "Сервис собирает тендерные данные, строит выжимку рисков и помогает подготовить пакет участия.",
url: "https://tender.handhdc.ru", url: "https://tender.handhdc.ru/sso/launch",
launchUrl: "https://tender.handhdc.ru/sso/launch", launchUrl: "https://tender.handhdc.ru/sso/launch",
accentColor: "#FFD166", accentColor: "#FFD166",
fallbackGradient: "linear-gradient(135deg, rgba(255, 209, 102, 0.84), rgba(74, 53, 19, 0.92) 42%, #0B0D10 86%)", fallbackGradient: "linear-gradient(135deg, rgba(255, 209, 102, 0.84), rgba(74, 53, 19, 0.92) 42%, #0B0D10 86%)",
@ -222,7 +222,7 @@ export const mockServices: Service[] = [
subtitle: "3D и пространственные данные", subtitle: "3D и пространственные данные",
description: "Просмотр цифровых двойников, карт и объектных сцен.", description: "Просмотр цифровых двойников, карт и объектных сцен.",
fullDescription: "Витрина геометрии, объектов, слоёв и статусов инфраструктуры.", fullDescription: "Витрина геометрии, объектов, слоёв и статусов инфраструктуры.",
url: "https://twin.handhdc.ru", url: "https://twin.handhdc.ru/sso/launch",
launchUrl: "https://twin.handhdc.ru/sso/launch", launchUrl: "https://twin.handhdc.ru/sso/launch",
accentColor: "#76E4F7", accentColor: "#76E4F7",
fallbackGradient: "linear-gradient(140deg, rgba(118, 228, 247, 0.82), rgba(23, 69, 87, 0.92) 47%, #080B0F 86%)", fallbackGradient: "linear-gradient(140deg, rgba(118, 228, 247, 0.82), rgba(23, 69, 87, 0.92) 47%, #080B0F 86%)",
@ -240,7 +240,7 @@ export const mockServices: Service[] = [
subtitle: "Будущие модули", subtitle: "Будущие модули",
description: "Скрытый каталог модулей для root-admin preview.", description: "Скрытый каталог модулей для root-admin preview.",
fullDescription: "Площадка для будущих цифровых модулей NODE.DC.", fullDescription: "Площадка для будущих цифровых модулей NODE.DC.",
url: "https://dm.handhdc.ru", url: "https://dm.handhdc.ru/sso/launch",
launchUrl: "https://dm.handhdc.ru/sso/launch", launchUrl: "https://dm.handhdc.ru/sso/launch",
accentColor: "#FF9AC2", accentColor: "#FF9AC2",
fallbackGradient: "linear-gradient(135deg, rgba(255, 154, 194, 0.78), rgba(76, 41, 64, 0.9) 44%, #090B0F 86%)", fallbackGradient: "linear-gradient(135deg, rgba(255, 154, 194, 0.78), rgba(76, 41, 64, 0.9) 44%, #090B0F 86%)",
@ -259,7 +259,7 @@ export const mockServices: Service[] = [
description: "Отключённый сервис для проверки диагностики root-admin.", description: "Отключённый сервис для проверки диагностики root-admin.",
fullDescription: "Не показывается обычным пользователям, виден root-admin в каталоге.", fullDescription: "Не показывается обычным пользователям, виден root-admin в каталоге.",
url: "https://internal.handhdc.ru", url: "https://internal.handhdc.ru",
launchUrl: null, launchUrl: "https://internal.handhdc.ru",
accentColor: "#F97373", accentColor: "#F97373",
fallbackGradient: "linear-gradient(135deg, rgba(249, 115, 115, 0.78), rgba(73, 32, 32, 0.92) 43%, #090B0F 86%)", fallbackGradient: "linear-gradient(135deg, rgba(249, 115, 115, 0.78), rgba(73, 32, 32, 0.92) 43%, #090B0F 86%)",
status: "disabled", status: "disabled",

View File

@ -40,6 +40,7 @@ import {
import type { ServiceAppRole } from "../../entities/access/types"; import type { ServiceAppRole } from "../../entities/access/types";
import type { Client, ClientStatus, ClientType } from "../../entities/client/types"; import type { Client, ClientStatus, ClientType } from "../../entities/client/types";
import type { Invite, InviteStatus } from "../../entities/invite/types"; import type { Invite, InviteStatus } from "../../entities/invite/types";
import { createServiceLaunchLinkPatch, getServiceLaunchLink } from "../../entities/service/links";
import type { MediaKind, Service, ServiceMediaSource, ServiceStatus } from "../../entities/service/types"; import type { MediaKind, Service, ServiceMediaSource, ServiceStatus } from "../../entities/service/types";
import type { SyncState, SyncStatus } from "../../entities/sync/types"; import type { SyncState, SyncStatus } from "../../entities/sync/types";
import type { import type {
@ -875,7 +876,7 @@ function ServicesSection({
<th>Сервис</th> <th>Сервис</th>
<th>Slug</th> <th>Slug</th>
<th>Статус</th> <th>Статус</th>
<th>URL</th> <th>Ссылка запуска</th>
<th>Authentik</th> <th>Authentik</th>
<th aria-label="Редактирование" /> <th aria-label="Редактирование" />
<th aria-label="Порядок" /> <th aria-label="Порядок" />
@ -1007,9 +1008,9 @@ function ServiceTableCells({
<td> <td>
<input <input
className="admin-table-input" className="admin-table-input"
value={service.url} value={getServiceLaunchLink(service)}
onChange={(event) => onUpdateService(service.id, { url: event.target.value })} onChange={(event) => onUpdateService(service.id, createServiceLaunchLinkPatch(event.target.value))}
aria-label={`URL сервиса ${service.title}`} aria-label={`Ссылка запуска сервиса ${service.title}`}
/> />
</td> </td>
<td> <td>
@ -1262,7 +1263,10 @@ function ServiceContentModal({
<span> <span>
<Link2 size={14} /> Ссылка запуска <Link2 size={14} /> Ссылка запуска
</span> </span>
<input value={draft.launchUrl ?? ""} onChange={(event) => update("launchUrl", event.target.value || null)} /> <input
value={getServiceLaunchLink(draft)}
onChange={(event) => setDraft((current) => ({ ...current, ...createServiceLaunchLinkPatch(event.target.value) }))}
/>
</label> </label>
<MediaSourceField <MediaSourceField
@ -1331,6 +1335,7 @@ function ServiceContentModal({
subtitle: draft.subtitle, subtitle: draft.subtitle,
description: draft.description, description: draft.description,
fullDescription: draft.fullDescription, fullDescription: draft.fullDescription,
url: draft.url,
launchUrl: draft.launchUrl, launchUrl: draft.launchUrl,
coverImageUrl: draft.coverImageUrl, coverImageUrl: draft.coverImageUrl,
coverMediaKind: draft.coverMediaKind, coverMediaKind: draft.coverMediaKind,