Compare commits
No commits in common. "3094464f62361700316d804f3f061f7b329fe060" and "39843b7737694687799c398ae74fdd91ff5ca627" have entirely different histories.
3094464f62
...
39843b7737
|
|
@ -1543,19 +1543,7 @@ if (process.env.NODE_ENV === "production") {
|
||||||
throw new Error("Launcher production build is missing. Run npm run build before starting the server.");
|
throw new Error("Launcher production build is missing. Run npm run build before starting the server.");
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.static(distRoot, {
|
app.use(express.static(distRoot, { index: false }));
|
||||||
index: false,
|
|
||||||
immutable: true,
|
|
||||||
maxAge: "1y",
|
|
||||||
setHeaders(res, assetPath) {
|
|
||||||
if (assetPath === indexHtmlPath) {
|
|
||||||
res.setHeader("Cache-Control", noStoreCacheControl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (req.method !== "GET" && req.method !== "HEAD") {
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
||||||
next();
|
next();
|
||||||
|
|
@ -2557,16 +2545,12 @@ function isPublicPoolUser(data, user) {
|
||||||
|
|
||||||
function resolveUserServiceModules(data, user, serviceSlug, clientId) {
|
function resolveUserServiceModules(data, user, serviceSlug, clientId) {
|
||||||
if (!user?.id) return {};
|
if (!user?.id) return {};
|
||||||
|
const normalizedClientId = normalizeOptionalText(clientId);
|
||||||
|
if (!normalizedClientId) return {};
|
||||||
const service = data.services.find(
|
const service = data.services.find(
|
||||||
(candidate) => candidate.slug === serviceSlug || candidate.authentikApplicationSlug === serviceSlug
|
(candidate) => candidate.slug === serviceSlug || candidate.authentikApplicationSlug === serviceSlug
|
||||||
);
|
);
|
||||||
if (!service?.id) return {};
|
if (!service?.id) return {};
|
||||||
if (hasSystemServiceModuleEntitlement(user, service, "codex_agents")) {
|
|
||||||
return { codex_agents: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedClientId = normalizeOptionalText(clientId);
|
|
||||||
if (!normalizedClientId) return {};
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
(data.serviceModuleEntitlements ?? [])
|
(data.serviceModuleEntitlements ?? [])
|
||||||
|
|
@ -2581,10 +2565,6 @@ function resolveUserServiceModules(data, user, serviceSlug, clientId) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSystemServiceModuleEntitlement(user, service, moduleId) {
|
|
||||||
return user?.id === "user_root" && moduleId === "codex_agents" && service?.slug === "task-manager";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFrontchannelLogoutUrls() {
|
function getFrontchannelLogoutUrls() {
|
||||||
const urls = [config.taskLogoutUrl];
|
const urls = [config.taskLogoutUrl];
|
||||||
const launcherData = readLauncherData();
|
const launcherData = readLauncherData();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, 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 { syncServiceLaunchLink } from "../entities/service/links";
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
ensureAdminTaskManagerProjectMembership,
|
ensureAdminTaskManagerProjectMembership,
|
||||||
ensureAdminTaskManagerWorkspaceMembership,
|
ensureAdminTaskManagerWorkspaceMembership,
|
||||||
fetchAdminTaskManagerWorkspaces,
|
fetchAdminTaskManagerWorkspaces,
|
||||||
|
fetchControlPlaneSnapshot,
|
||||||
reorderAdminServices,
|
reorderAdminServices,
|
||||||
retryAdminSync,
|
retryAdminSync,
|
||||||
rejectAdminAccessRequest,
|
rejectAdminAccessRequest,
|
||||||
|
|
@ -63,24 +64,21 @@ import { acceptInvite, fetchPublicInvite, registerInvite, type PublicInviteRespo
|
||||||
import type { AccessRequest, CreateAccessRequestCommand } from "../entities/access-request/types";
|
import type { AccessRequest, CreateAccessRequestCommand } from "../entities/access-request/types";
|
||||||
import { subscribeToNodeDCLogoutEvents } from "../shared/session/sessionSync";
|
import { subscribeToNodeDCLogoutEvents } from "../shared/session/sessionSync";
|
||||||
import { loadPersistedLauncherData } from "../shared/api/storageApi";
|
import { loadPersistedLauncherData } from "../shared/api/storageApi";
|
||||||
import type {
|
import {
|
||||||
AccessAssignmentValue,
|
AdminOverlay,
|
||||||
CreateUserCommand,
|
type AccessAssignmentValue,
|
||||||
EnsureTaskManagerProjectMemberCommand,
|
type CreateUserCommand,
|
||||||
SetServiceModuleEntitlementCommand,
|
type EnsureTaskManagerProjectMemberCommand,
|
||||||
SetUserServiceAccessCommand,
|
type SetServiceModuleEntitlementCommand,
|
||||||
|
type SetUserServiceAccessCommand,
|
||||||
} from "../widgets/admin-overlay/AdminOverlay";
|
} from "../widgets/admin-overlay/AdminOverlay";
|
||||||
|
import { ProfileSettingsPanel } from "../widgets/profile-settings-panel/ProfileSettingsPanel";
|
||||||
import { ServiceRail } from "../widgets/service-rail/ServiceRail";
|
import { ServiceRail } from "../widgets/service-rail/ServiceRail";
|
||||||
import { ServiceStage } from "../widgets/service-stage/ServiceStage";
|
import { ServiceStage } from "../widgets/service-stage/ServiceStage";
|
||||||
import { TopBar, type LauncherAdminMode } from "../widgets/top-bar/TopBar";
|
import { TopBar, type LauncherAdminMode } from "../widgets/top-bar/TopBar";
|
||||||
|
|
||||||
let lastAuthRedirect: { url: string; startedAt: number } | null = null;
|
let lastAuthRedirect: { url: string; startedAt: number } | null = null;
|
||||||
|
|
||||||
const AdminOverlay = lazy(() => import("../widgets/admin-overlay/AdminOverlay").then((module) => ({ default: module.AdminOverlay })));
|
|
||||||
const ProfileSettingsPanel = lazy(() =>
|
|
||||||
import("../widgets/profile-settings-panel/ProfileSettingsPanel").then((module) => ({ default: module.ProfileSettingsPanel }))
|
|
||||||
);
|
|
||||||
|
|
||||||
type InviteFlowState =
|
type InviteFlowState =
|
||||||
| { status: "loading" }
|
| { status: "loading" }
|
||||||
| { status: "ready"; payload: PublicInviteResponse }
|
| { status: "ready"; payload: PublicInviteResponse }
|
||||||
|
|
@ -103,7 +101,6 @@ export function LauncherApp() {
|
||||||
const [adminMode, setAdminMode] = useState<LauncherAdminMode>("admin");
|
const [adminMode, setAdminMode] = useState<LauncherAdminMode>("admin");
|
||||||
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
|
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
|
||||||
const [authApps, setAuthApps] = useState<LauncherAuthApp[] | null>(null);
|
const [authApps, setAuthApps] = useState<LauncherAuthApp[] | null>(null);
|
||||||
const [runtimeReady, setRuntimeReady] = useState(false);
|
|
||||||
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
|
||||||
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
|
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
|
||||||
const [pendingServiceModuleEntitlements, setPendingServiceModuleEntitlements] = useState<Record<string, boolean>>({});
|
const [pendingServiceModuleEntitlements, setPendingServiceModuleEntitlements] = useState<Record<string, boolean>>({});
|
||||||
|
|
@ -116,7 +113,6 @@ export function LauncherApp() {
|
||||||
const runtimeDataRef = useRef(data);
|
const runtimeDataRef = useRef(data);
|
||||||
const runtimeProfileIdRef = useRef(activeProfileId);
|
const runtimeProfileIdRef = useRef(activeProfileId);
|
||||||
const runtimeClientIdRef = useRef(activeClientId);
|
const runtimeClientIdRef = useRef(activeClientId);
|
||||||
const runtimeRefreshInFlightRef = useRef<Promise<void> | null>(null);
|
|
||||||
const resolvedProfileId = useMemo(
|
const resolvedProfileId = useMemo(
|
||||||
() => resolveRuntimeProfileId(data, authSession, activeProfileId),
|
() => resolveRuntimeProfileId(data, authSession, activeProfileId),
|
||||||
[activeProfileId, authSession, data]
|
[activeProfileId, authSession, data]
|
||||||
|
|
@ -223,63 +219,38 @@ export function LauncherApp() {
|
||||||
|
|
||||||
const selectedService = launcherServices.find((service) => service.id === selectedServiceId);
|
const selectedService = launcherServices.find((service) => service.id === selectedServiceId);
|
||||||
|
|
||||||
const refreshRuntimeState = useCallback(async () => {
|
|
||||||
if (runtimeRefreshInFlightRef.current) {
|
|
||||||
return runtimeRefreshInFlightRef.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
const request = (async () => {
|
|
||||||
try {
|
|
||||||
const nextSession = await fetchAuthSession();
|
|
||||||
|
|
||||||
setAuthSession(nextSession);
|
|
||||||
|
|
||||||
if (!nextSession.authenticated) {
|
|
||||||
setAuthApps([]);
|
|
||||||
setRuntimeReady(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [persistedData, apps] = await Promise.all([
|
|
||||||
loadPersistedLauncherData(),
|
|
||||||
fetchAvailableApps(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (persistedData) {
|
|
||||||
setData(syncLauncherServiceLinks(persistedData));
|
|
||||||
}
|
|
||||||
|
|
||||||
setAuthApps(apps);
|
|
||||||
setRuntimeReady(true);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
setRuntimeReady(true);
|
|
||||||
console.warn(error instanceof Error ? error.message : "Не удалось обновить runtime состояние Launcher");
|
|
||||||
}
|
|
||||||
})().finally(() => {
|
|
||||||
runtimeRefreshInFlightRef.current = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
runtimeRefreshInFlightRef.current = request;
|
|
||||||
return request;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
refreshRuntimeState()
|
fetchAuthSession()
|
||||||
|
.then(async (session) => {
|
||||||
|
if (!isMounted) return;
|
||||||
|
|
||||||
|
setAuthSession(session);
|
||||||
|
|
||||||
|
if (!session.authenticated) {
|
||||||
|
setAuthApps([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apps = await fetchAvailableApps();
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setAuthApps(apps);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
if (!isMounted) return;
|
if (!isMounted) return;
|
||||||
|
|
||||||
setAuthSession({ authenticated: false, loginUrl: "/auth/login" });
|
setAuthSession({ authenticated: false, loginUrl: "/auth/login" });
|
||||||
setAuthApps([]);
|
setAuthApps([]);
|
||||||
setRuntimeReady(true);
|
|
||||||
console.warn(error instanceof Error ? error.message : "Не удалось проверить сессию платформы");
|
console.warn(error instanceof Error ? error.message : "Не удалось проверить сессию платформы");
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
};
|
};
|
||||||
}, [refreshRuntimeState]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authSession || authSession.authenticated) return;
|
if (!authSession || authSession.authenticated) return;
|
||||||
|
|
@ -367,6 +338,41 @@ export function LauncherApp() {
|
||||||
}
|
}
|
||||||
}, [activeClientId, activeProfileId, authSession, data]);
|
}, [activeClientId, activeProfileId, authSession, data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
loadPersistedLauncherData()
|
||||||
|
.then((persistedData) => {
|
||||||
|
if (isMounted && persistedData) {
|
||||||
|
setData(syncLauncherServiceLinks(persistedData));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canOpenAdminApi) return;
|
||||||
|
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
fetchControlPlaneSnapshot()
|
||||||
|
.then((snapshot) => {
|
||||||
|
if (isMounted) {
|
||||||
|
setData(syncLauncherServiceLinks(snapshot.data));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.warn(error instanceof Error ? error.message : "Не удалось загрузить control-plane snapshot");
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [canOpenAdminApi]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!adminOpen || !canOpenAdminApi) return;
|
if (!adminOpen || !canOpenAdminApi) return;
|
||||||
void refreshTaskManagerWorkspaces();
|
void refreshTaskManagerWorkspaces();
|
||||||
|
|
@ -379,6 +385,32 @@ export function LauncherApp() {
|
||||||
setAdminMode("admin");
|
setAdminMode("admin");
|
||||||
}, [runtimeMe.permissions.canOpenAdmin]);
|
}, [runtimeMe.permissions.canOpenAdmin]);
|
||||||
|
|
||||||
|
const refreshRuntimeState = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const nextSession = await fetchAuthSession();
|
||||||
|
|
||||||
|
setAuthSession(nextSession);
|
||||||
|
|
||||||
|
if (!nextSession.authenticated) {
|
||||||
|
setAuthApps([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [persistedData, apps] = await Promise.all([
|
||||||
|
loadPersistedLauncherData(),
|
||||||
|
fetchAvailableApps(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (persistedData) {
|
||||||
|
setData(syncLauncherServiceLinks(persistedData));
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthApps(apps);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.warn(error instanceof Error ? error.message : "Не удалось обновить runtime состояние Launcher");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authSession?.authenticated) return;
|
if (!authSession?.authenticated) return;
|
||||||
|
|
||||||
|
|
@ -391,6 +423,10 @@ export function LauncherApp() {
|
||||||
|
|
||||||
const eventSource = new EventSource("/api/events");
|
const eventSource = new EventSource("/api/events");
|
||||||
|
|
||||||
|
eventSource.addEventListener("nodedc-ready", () => {
|
||||||
|
void refreshMountedRuntimeState();
|
||||||
|
});
|
||||||
|
|
||||||
eventSource.addEventListener("nodedc-runtime", () => {
|
eventSource.addEventListener("nodedc-runtime", () => {
|
||||||
void refreshMountedRuntimeState();
|
void refreshMountedRuntimeState();
|
||||||
});
|
});
|
||||||
|
|
@ -430,7 +466,7 @@ export function LauncherApp() {
|
||||||
if (document.visibilityState === "visible") {
|
if (document.visibilityState === "visible") {
|
||||||
void refreshRuntimeState();
|
void refreshRuntimeState();
|
||||||
}
|
}
|
||||||
}, 60000);
|
}, 5000);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.clearInterval(intervalId);
|
window.clearInterval(intervalId);
|
||||||
|
|
@ -835,7 +871,7 @@ export function LauncherApp() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!authSession || (authSession.authenticated && !runtimeReady)) {
|
if (!authSession) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -887,7 +923,6 @@ export function LauncherApp() {
|
||||||
onSelectNext={() => handleStageStep("next")}
|
onSelectNext={() => handleStageStep("next")}
|
||||||
/>
|
/>
|
||||||
{adminOpen && me.permissions.canOpenAdmin ? (
|
{adminOpen && me.permissions.canOpenAdmin ? (
|
||||||
<Suspense fallback={null}>
|
|
||||||
<AdminOverlay
|
<AdminOverlay
|
||||||
data={data}
|
data={data}
|
||||||
me={runtimeMe}
|
me={runtimeMe}
|
||||||
|
|
@ -932,17 +967,14 @@ export function LauncherApp() {
|
||||||
onSetTaskManagerProjectMemberRole={handleSetTaskManagerProjectMemberRole}
|
onSetTaskManagerProjectMemberRole={handleSetTaskManagerProjectMemberRole}
|
||||||
onSetServiceModuleEntitlement={handleSetServiceModuleEntitlement}
|
onSetServiceModuleEntitlement={handleSetServiceModuleEntitlement}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
) : null}
|
) : null}
|
||||||
{profileSettingsOpen && activeProfileUser ? (
|
{profileSettingsOpen && activeProfileUser ? (
|
||||||
<Suspense fallback={null}>
|
|
||||||
<ProfileSettingsPanel
|
<ProfileSettingsPanel
|
||||||
user={activeProfileUser}
|
user={activeProfileUser}
|
||||||
onClose={() => setProfileSettingsOpen(false)}
|
onClose={() => setProfileSettingsOpen(false)}
|
||||||
onSaveProfile={handleUpdateOwnProfile}
|
onSaveProfile={handleUpdateOwnProfile}
|
||||||
onChangePassword={handleUpdateOwnPassword}
|
onChangePassword={handleUpdateOwnPassword}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
|
||||||
) : null}
|
) : null}
|
||||||
<ServiceRail services={launcherServices} selectedServiceId={selectedServiceId} onSelect={handleServiceSelect} />
|
<ServiceRail services={launcherServices} selectedServiceId={selectedServiceId} onSelect={handleServiceSelect} />
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -754,6 +754,7 @@ code {
|
||||||
content: "";
|
content: "";
|
||||||
opacity: 0.34;
|
opacity: 0.34;
|
||||||
transform: rotate(-2deg);
|
transform: rotate(-2deg);
|
||||||
|
animation: stageVideoDrift 16s linear infinite;
|
||||||
mix-blend-mode: screen;
|
mix-blend-mode: screen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1044,6 +1045,15 @@ code {
|
||||||
background: color-mix(in srgb, var(--service-accent) 45%, rgba(255, 255, 255, 0.16));
|
background: color-mix(in srgb, var(--service-accent) 45%, rgba(255, 255, 255, 0.16));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes stageVideoDrift {
|
||||||
|
from {
|
||||||
|
transform: translate3d(0, 0, 0) rotate(-2deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translate3d(-7rem, -7rem, 0) rotate(-2deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes stageFigureFloat {
|
@keyframes stageFigureFloat {
|
||||||
from {
|
from {
|
||||||
transform: translate3d(-0.8rem, 0, 0) rotate(-9deg) scale(1);
|
transform: translate3d(-0.8rem, 0, 0) rotate(-9deg) scale(1);
|
||||||
|
|
@ -1191,7 +1201,7 @@ code {
|
||||||
flex: 0 0 var(--service-rail-card-size);
|
flex: 0 0 var(--service-rail-card-size);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
background: #050506;
|
background: color-mix(in srgb, var(--tile-accent) 36%, rgba(255, 255, 255, 0.12));
|
||||||
}
|
}
|
||||||
|
|
||||||
.service-tile__media-asset {
|
.service-tile__media-asset {
|
||||||
|
|
|
||||||
|
|
@ -3425,8 +3425,7 @@ function OperationalCoreAccessModal({
|
||||||
</div>
|
</div>
|
||||||
<div className="task-module-access-list">
|
<div className="task-module-access-list">
|
||||||
{operationalCoreModules.map((module) => {
|
{operationalCoreModules.map((module) => {
|
||||||
const systemEnabled = hasSystemServiceModuleEntitlement(user, module.id);
|
const enabled = hasServiceModuleEntitlement(data, client.id, user.id, service.id, module.id);
|
||||||
const enabled = systemEnabled || hasServiceModuleEntitlement(data, client.id, user.id, service.id, module.id);
|
|
||||||
const pendingKey = serviceModuleEntitlementKey(client.id, user.id, service.id, module.id);
|
const pendingKey = serviceModuleEntitlementKey(client.id, user.id, service.id, module.id);
|
||||||
const pending = Boolean(pendingServiceModuleEntitlements[pendingKey]);
|
const pending = Boolean(pendingServiceModuleEntitlements[pendingKey]);
|
||||||
|
|
||||||
|
|
@ -3436,7 +3435,7 @@ function OperationalCoreAccessModal({
|
||||||
className={cn("task-module-access-row", enabled && "task-module-access-row--enabled", pending && "task-module-access-row--pending")}
|
className={cn("task-module-access-row", enabled && "task-module-access-row--enabled", pending && "task-module-access-row--pending")}
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={enabled}
|
aria-pressed={enabled}
|
||||||
disabled={pending || systemEnabled}
|
disabled={pending}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onSetServiceModuleEntitlement({
|
onSetServiceModuleEntitlement({
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
|
|
@ -3455,7 +3454,7 @@ function OperationalCoreAccessModal({
|
||||||
<span className="task-module-checker" aria-hidden="true">
|
<span className="task-module-checker" aria-hidden="true">
|
||||||
{enabled ? <span /> : null}
|
{enabled ? <span /> : null}
|
||||||
</span>
|
</span>
|
||||||
<span>{pending ? "Сохраняем..." : systemEnabled ? "Системно включён" : enabled ? module.enabledLabel : module.disabledLabel}</span>
|
<span>{pending ? "Сохраняем..." : enabled ? module.enabledLabel : module.disabledLabel}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
@ -4814,10 +4813,6 @@ function hasServiceModuleEntitlement(data: LauncherData, clientId: string, userI
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasSystemServiceModuleEntitlement(user: LauncherUser, moduleId: ServiceModuleId): boolean {
|
|
||||||
return user.id === "user_root" && moduleId === "codex_agents";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOperationalCoreService(service: Service): boolean {
|
function isOperationalCoreService(service: Service): boolean {
|
||||||
return service.slug === "task-manager" || service.authentikApplicationSlug === "task-manager";
|
return service.slug === "task-manager" || service.authentikApplicationSlug === "task-manager";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import { resolveAmbientMedia } from "../../entities/service/media";
|
import { DEFAULT_AMBIENT_MEDIA, resolveAmbientMedia } from "../../entities/service/media";
|
||||||
import type { LauncherServiceView } from "../../entities/service/types";
|
import type { LauncherServiceView } from "../../entities/service/types";
|
||||||
import { cn } from "../../shared/lib/cn";
|
import { cn } from "../../shared/lib/cn";
|
||||||
|
|
||||||
|
|
@ -34,8 +34,8 @@ export function ServiceRail({
|
||||||
>
|
>
|
||||||
<RailMedia
|
<RailMedia
|
||||||
className="service-tile__media-asset"
|
className="service-tile__media-asset"
|
||||||
src={service.media.coverImage ?? service.media.thumbnail ?? null}
|
src={service.media.coverImage ?? service.media.ambientVideo ?? DEFAULT_AMBIENT_MEDIA}
|
||||||
kind={service.media.coverKind ?? "image"}
|
kind={service.media.coverKind ?? service.media.ambientKind}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span className="service-tile__content">
|
<span className="service-tile__content">
|
||||||
|
|
@ -57,17 +57,15 @@ function RailMedia({
|
||||||
kind,
|
kind,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
src?: string | null;
|
src: string;
|
||||||
kind?: LauncherServiceView["media"]["coverKind"];
|
kind?: LauncherServiceView["media"]["coverKind"];
|
||||||
className: string;
|
className: string;
|
||||||
}) {
|
}) {
|
||||||
if (!src) return null;
|
|
||||||
|
|
||||||
if (kind === "video" || isVideoSource(src)) {
|
if (kind === "video" || isVideoSource(src)) {
|
||||||
return <video className={className} src={src} autoPlay loop muted playsInline preload="metadata" />;
|
return <video className={className} src={src} autoPlay loop muted playsInline />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <img className={className} src={src} alt="" loading="lazy" decoding="async" />;
|
return <img className={className} src={src} alt="" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVideoSource(src: string) {
|
function isVideoSource(src: string) {
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export function ServiceStage({
|
||||||
<section className="service-stage" style={style}>
|
<section className="service-stage" style={style}>
|
||||||
<div className="stage-video-shell">
|
<div className="stage-video-shell">
|
||||||
<div className="stage-video-stream" aria-hidden="true">
|
<div className="stage-video-stream" aria-hidden="true">
|
||||||
<StageMedia className="stage-video-gif" src={ambientMedia.src} kind={ambientMedia.kind} priority={!service} />
|
<StageMedia className="stage-video-gif" src={ambientMedia.src} kind={ambientMedia.kind} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{service ? (
|
{service ? (
|
||||||
|
|
@ -155,22 +155,12 @@ function ServiceIcon({ slug }: { slug: string }) {
|
||||||
return <Bot size={18} />;
|
return <Bot size={18} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StageMedia({
|
function StageMedia({ src, kind, className }: { src: string; kind?: LauncherServiceView["media"]["coverKind"]; className: string }) {
|
||||||
src,
|
|
||||||
kind,
|
|
||||||
className,
|
|
||||||
priority = false,
|
|
||||||
}: {
|
|
||||||
src: string;
|
|
||||||
kind?: LauncherServiceView["media"]["coverKind"];
|
|
||||||
className: string;
|
|
||||||
priority?: boolean;
|
|
||||||
}) {
|
|
||||||
if (kind === "video" || isVideoSource(src)) {
|
if (kind === "video" || isVideoSource(src)) {
|
||||||
return <video className={className} src={src} autoPlay loop muted playsInline preload="metadata" />;
|
return <video className={className} src={src} autoPlay loop muted playsInline />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <img className={className} src={src} alt="" loading={priority ? "eager" : "lazy"} decoding="async" />;
|
return <img className={className} src={src} alt="" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RichDescription({ text }: { text: string }) {
|
function RichDescription({ text }: { text: string }) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue