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": "Агентная платформа",
"description": "Сборка, запуск и мониторинг агентных workflow.",
"fullDescription": "NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.",
"url": "https://dev.handhdc.ru",
"url": "https://dev.handhdc.ru/sso/launch",
"launchUrl": "https://dev.handhdc.ru/sso/launch",
"accentColor": "#B5FF5A",
"fallbackGradient": "linear-gradient(128deg, rgba(181, 255, 90, 0.84), rgba(37, 58, 36, 0.86) 42%, #0A0D10 82%)",
@ -257,7 +257,7 @@
"subtitle": "Операционный слой",
"description": "Задачи, контуры предприятия, процессы и AI-функции поверх задачника.",
"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",
"accentColor": "#D7C8FF",
"fallbackGradient": "linear-gradient(132deg, rgba(215, 200, 255, 0.82), rgba(51, 41, 79, 0.9) 46%, #0B0D10 84%)",
@ -279,7 +279,7 @@
"subtitle": "Бухгалтерский ассистент",
"description": "Вопросы к 1С, точные выборки и доказательная навигация по данным.",
"fullDescription": "Ассистент для бухгалтерских запросов, анализа операций, остатков и документов.",
"url": "https://1c.handhdc.ru",
"url": "https://1c.handhdc.ru/sso/launch",
"launchUrl": "https://1c.handhdc.ru/sso/launch",
"accentColor": "#8FD7FF",
"fallbackGradient": "linear-gradient(126deg, rgba(143, 215, 255, 0.8), rgba(32, 61, 80, 0.9) 44%, #080B0F 84%)",
@ -301,7 +301,7 @@
"subtitle": "Госзакупки и тендеры",
"description": "Поиск, анализ и подготовка тендерных решений.",
"fullDescription": "Сервис собирает тендерные данные, строит выжимку рисков и помогает подготовить пакет участия.",
"url": "https://tender.handhdc.ru",
"url": "https://tender.handhdc.ru/sso/launch",
"launchUrl": "https://tender.handhdc.ru/sso/launch",
"accentColor": "#FFD166",
"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 и пространственные данные",
"description": "Просмотр цифровых двойников, карт и объектных сцен.",
"fullDescription": "Витрина геометрии, объектов, слоёв и статусов инфраструктуры.",
"url": "https://twin.handhdc.ru",
"url": "https://launch.dcserve.ru/",
"launchUrl": "https://launch.dcserve.ru/",
"accentColor": "#76E4F7",
"fallbackGradient": "linear-gradient(140deg, rgba(118, 228, 247, 0.82), rgba(23, 69, 87, 0.92) 47%, #080B0F 86%)",
@ -349,7 +349,7 @@
"subtitle": "Будущие модули",
"description": "Скрытый каталог модулей для root-admin preview.",
"fullDescription": "Площадка для будущих цифровых модулей NODE.DC.",
"url": "https://dm.handhdc.ru",
"url": "https://dm.handhdc.ru/sso/launch",
"launchUrl": "https://dm.handhdc.ru/sso/launch",
"accentColor": "#FF9AC2",
"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 type { Client } from "../entities/client/types";
import type { Invite } from "../entities/invite/types";
import { syncServiceLaunchLink } from "../entities/service/links";
import type { LauncherServiceView, Service } from "../entities/service/types";
import type { SyncStatus } from "../entities/sync/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";
export function LauncherApp() {
const [data, setData] = useState<LauncherData>(initialLauncherData);
const [data, setData] = useState<LauncherData>(() => syncLauncherServiceLinks(initialLauncherData));
const [activeProfileId, setActiveProfileId] = useState(profileOptions[0].userId);
const [activeClientId, setActiveClientId] = useState(profileOptions[0].defaultClientId);
const [selectedServiceId, setSelectedServiceId] = useState<string | undefined>();
@ -51,7 +52,7 @@ export function LauncherApp() {
loadPersistedLauncherData()
.then((persistedData) => {
if (isMounted && persistedData) {
setData(persistedData);
setData(syncLauncherServiceLinks(persistedData));
}
})
.finally(() => {
@ -232,11 +233,11 @@ export function LauncherApp() {
...current,
services: current.services.map((service) =>
service.id === serviceId
? {
? syncServiceLaunchLink({
...service,
...patch,
updatedAt: new Date().toISOString(),
}
})
: service
),
}));
@ -444,7 +445,7 @@ export function LauncherApp() {
subtitle: "Новый сервис",
description: "Описание сервиса для витрины.",
fullDescription: "Заполните описание, медиа и ссылку запуска в редакторе контента.",
url: "https://service.handhdc.ru",
url: "https://service.handhdc.ru/sso/launch",
launchUrl: "https://service.handhdc.ru/sso/launch",
accentColor: "#F7F8F4",
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>
);
}
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 { Client } from "../../entities/client/types";
import type { Invite } from "../../entities/invite/types";
import { getServiceLaunchLink } from "../../entities/service/links";
import type { LauncherServiceView, Service } from "../../entities/service/types";
import type { SyncStatus } from "../../entities/sync/types";
import type {
@ -217,7 +218,7 @@ export function buildLauncherServices(data: LauncherData, userId: string, active
status: service.status,
userAccess: effectiveAccess.allowed ? ("allowed" as const) : ("denied" as const),
appRole: effectiveAccess.appRole,
openUrl: effectiveAccess.openEnabled ? service.launchUrl ?? service.url : null,
openUrl: effectiveAccess.openEnabled ? getServiceLaunchLink(service) || null : null,
accentColor: service.accentColor,
media: {
icon: service.iconUrl,

View File

@ -150,7 +150,7 @@ export const mockServices: Service[] = [
description: "Сборка, запуск и мониторинг агентных workflow.",
fullDescription:
"NodeDC используется для настройки агентных процессов, визуальной оркестрации, интеграций и runtime-мониторинга.",
url: "https://dev.handhdc.ru",
url: "https://dev.handhdc.ru/sso/launch",
launchUrl: "https://dev.handhdc.ru/sso/launch",
accentColor: "#B5FF5A",
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: "Операционный слой",
description: "Задачи, контуры предприятия, процессы и AI-функции поверх задачника.",
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",
accentColor: "#D7C8FF",
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: "Бухгалтерский ассистент",
description: "Вопросы к 1С, точные выборки и доказательная навигация по данным.",
fullDescription: "Ассистент для бухгалтерских запросов, анализа операций, остатков и документов.",
url: "https://1c.handhdc.ru",
url: "https://1c.handhdc.ru/sso/launch",
launchUrl: "https://1c.handhdc.ru/sso/launch",
accentColor: "#8FD7FF",
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: "Госзакупки и тендеры",
description: "Поиск, анализ и подготовка тендерных решений.",
fullDescription: "Сервис собирает тендерные данные, строит выжимку рисков и помогает подготовить пакет участия.",
url: "https://tender.handhdc.ru",
url: "https://tender.handhdc.ru/sso/launch",
launchUrl: "https://tender.handhdc.ru/sso/launch",
accentColor: "#FFD166",
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 и пространственные данные",
description: "Просмотр цифровых двойников, карт и объектных сцен.",
fullDescription: "Витрина геометрии, объектов, слоёв и статусов инфраструктуры.",
url: "https://twin.handhdc.ru",
url: "https://twin.handhdc.ru/sso/launch",
launchUrl: "https://twin.handhdc.ru/sso/launch",
accentColor: "#76E4F7",
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: "Будущие модули",
description: "Скрытый каталог модулей для root-admin preview.",
fullDescription: "Площадка для будущих цифровых модулей NODE.DC.",
url: "https://dm.handhdc.ru",
url: "https://dm.handhdc.ru/sso/launch",
launchUrl: "https://dm.handhdc.ru/sso/launch",
accentColor: "#FF9AC2",
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.",
fullDescription: "Не показывается обычным пользователям, виден root-admin в каталоге.",
url: "https://internal.handhdc.ru",
launchUrl: null,
launchUrl: "https://internal.handhdc.ru",
accentColor: "#F97373",
fallbackGradient: "linear-gradient(135deg, rgba(249, 115, 115, 0.78), rgba(73, 32, 32, 0.92) 43%, #090B0F 86%)",
status: "disabled",

View File

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