Compare commits

...

2 Commits

Author SHA1 Message Date
DCCONSTRUCTIONS 3094464f62 perf: optimize launcher boot and media loading 2026-05-15 19:53:10 +03:00
DCCONSTRUCTIONS 3799483638 FEAT - LAUNCHER: expose system Codex agent entitlement 2026-05-15 14:25:36 +03:00
6 changed files with 161 additions and 166 deletions

View File

@ -1543,7 +1543,19 @@ if (process.env.NODE_ENV === "production") {
throw new Error("Launcher production build is missing. Run npm run build before starting the server.");
}
app.use(express.static(distRoot, { index: false }));
app.use(express.static(distRoot, {
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) => {
if (req.method !== "GET" && req.method !== "HEAD") {
next();
@ -2545,12 +2557,16 @@ function isPublicPoolUser(data, user) {
function resolveUserServiceModules(data, user, serviceSlug, clientId) {
if (!user?.id) return {};
const normalizedClientId = normalizeOptionalText(clientId);
if (!normalizedClientId) return {};
const service = data.services.find(
(candidate) => candidate.slug === serviceSlug || candidate.authentikApplicationSlug === serviceSlug
);
if (!service?.id) return {};
if (hasSystemServiceModuleEntitlement(user, service, "codex_agents")) {
return { codex_agents: true };
}
const normalizedClientId = normalizeOptionalText(clientId);
if (!normalizedClientId) return {};
return Object.fromEntries(
(data.serviceModuleEntitlements ?? [])
@ -2565,6 +2581,10 @@ 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() {
const urls = [config.taskLogoutUrl];
const launcherData = readLauncherData();

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { Client } from "../entities/client/types";
import type { Invite } from "../entities/invite/types";
import { syncServiceLaunchLink } from "../entities/service/links";
@ -21,7 +21,6 @@ import {
ensureAdminTaskManagerProjectMembership,
ensureAdminTaskManagerWorkspaceMembership,
fetchAdminTaskManagerWorkspaces,
fetchControlPlaneSnapshot,
reorderAdminServices,
retryAdminSync,
rejectAdminAccessRequest,
@ -64,21 +63,24 @@ import { acceptInvite, fetchPublicInvite, registerInvite, type PublicInviteRespo
import type { AccessRequest, CreateAccessRequestCommand } from "../entities/access-request/types";
import { subscribeToNodeDCLogoutEvents } from "../shared/session/sessionSync";
import { loadPersistedLauncherData } from "../shared/api/storageApi";
import {
AdminOverlay,
type AccessAssignmentValue,
type CreateUserCommand,
type EnsureTaskManagerProjectMemberCommand,
type SetServiceModuleEntitlementCommand,
type SetUserServiceAccessCommand,
import type {
AccessAssignmentValue,
CreateUserCommand,
EnsureTaskManagerProjectMemberCommand,
SetServiceModuleEntitlementCommand,
SetUserServiceAccessCommand,
} from "../widgets/admin-overlay/AdminOverlay";
import { ProfileSettingsPanel } from "../widgets/profile-settings-panel/ProfileSettingsPanel";
import { ServiceRail } from "../widgets/service-rail/ServiceRail";
import { ServiceStage } from "../widgets/service-stage/ServiceStage";
import { TopBar, type LauncherAdminMode } from "../widgets/top-bar/TopBar";
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 =
| { status: "loading" }
| { status: "ready"; payload: PublicInviteResponse }
@ -101,6 +103,7 @@ export function LauncherApp() {
const [adminMode, setAdminMode] = useState<LauncherAdminMode>("admin");
const [authSession, setAuthSession] = useState<AuthSession | null>(null);
const [authApps, setAuthApps] = useState<LauncherAuthApp[] | null>(null);
const [runtimeReady, setRuntimeReady] = useState(false);
const [profileSettingsOpen, setProfileSettingsOpen] = useState(false);
const [pendingAccessAssignments, setPendingAccessAssignments] = useState<Record<string, AccessAssignmentValue>>({});
const [pendingServiceModuleEntitlements, setPendingServiceModuleEntitlements] = useState<Record<string, boolean>>({});
@ -113,6 +116,7 @@ export function LauncherApp() {
const runtimeDataRef = useRef(data);
const runtimeProfileIdRef = useRef(activeProfileId);
const runtimeClientIdRef = useRef(activeClientId);
const runtimeRefreshInFlightRef = useRef<Promise<void> | null>(null);
const resolvedProfileId = useMemo(
() => resolveRuntimeProfileId(data, authSession, activeProfileId),
[activeProfileId, authSession, data]
@ -219,38 +223,63 @@ export function LauncherApp() {
const selectedService = launcherServices.find((service) => service.id === selectedServiceId);
useEffect(() => {
let isMounted = true;
const refreshRuntimeState = useCallback(async () => {
if (runtimeRefreshInFlightRef.current) {
return runtimeRefreshInFlightRef.current;
}
fetchAuthSession()
.then(async (session) => {
if (!isMounted) return;
const request = (async () => {
try {
const nextSession = await fetchAuthSession();
setAuthSession(session);
setAuthSession(nextSession);
if (!session.authenticated) {
if (!nextSession.authenticated) {
setAuthApps([]);
setRuntimeReady(true);
return;
}
const apps = await fetchAvailableApps();
const [persistedData, apps] = await Promise.all([
loadPersistedLauncherData(),
fetchAvailableApps(),
]);
if (isMounted) {
setAuthApps(apps);
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(() => {
let isMounted = true;
refreshRuntimeState()
.catch((error: unknown) => {
if (!isMounted) return;
setAuthSession({ authenticated: false, loginUrl: "/auth/login" });
setAuthApps([]);
setRuntimeReady(true);
console.warn(error instanceof Error ? error.message : "Не удалось проверить сессию платформы");
});
return () => {
isMounted = false;
};
}, []);
}, [refreshRuntimeState]);
useEffect(() => {
if (!authSession || authSession.authenticated) return;
@ -338,41 +367,6 @@ export function LauncherApp() {
}
}, [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(() => {
if (!adminOpen || !canOpenAdminApi) return;
void refreshTaskManagerWorkspaces();
@ -385,32 +379,6 @@ export function LauncherApp() {
setAdminMode("admin");
}, [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(() => {
if (!authSession?.authenticated) return;
@ -423,10 +391,6 @@ export function LauncherApp() {
const eventSource = new EventSource("/api/events");
eventSource.addEventListener("nodedc-ready", () => {
void refreshMountedRuntimeState();
});
eventSource.addEventListener("nodedc-runtime", () => {
void refreshMountedRuntimeState();
});
@ -466,7 +430,7 @@ export function LauncherApp() {
if (document.visibilityState === "visible") {
void refreshRuntimeState();
}
}, 5000);
}, 60000);
return () => {
window.clearInterval(intervalId);
@ -871,7 +835,7 @@ export function LauncherApp() {
);
}
if (!authSession) {
if (!authSession || (authSession.authenticated && !runtimeReady)) {
return null;
}
@ -923,58 +887,62 @@ export function LauncherApp() {
onSelectNext={() => handleStageStep("next")}
/>
{adminOpen && me.permissions.canOpenAdmin ? (
<AdminOverlay
data={data}
me={runtimeMe}
mode={runtimeMe.launcherRole === "root_admin" ? adminMode : "admin"}
activeClientId={resolvedClientId}
onClose={() => setAdminOpen(false)}
onSetUserServiceAccess={handleSetUserServiceAccess}
onCreateInvite={handleCreateInvite}
onUpdateInvite={handleUpdateInvite}
onDeleteInvite={handleDeleteInvite}
onUpdateAccessRequest={handleUpdateAccessRequest}
onApproveAccessRequest={handleApproveAccessRequest}
onRejectAccessRequest={handleRejectAccessRequest}
onApproveTaskerInviteRequest={handleApproveTaskerInviteRequest}
onRejectTaskerInviteRequest={handleRejectTaskerInviteRequest}
onRetrySync={handleRetrySync}
onCreateClient={handleCreateClient}
onUpdateClient={handleUpdateClient}
onDeleteClient={handleDeleteClient}
onCreateUser={handleCreateUser}
onUpdateUser={handleUpdateUser}
onDeleteUser={handleDeleteUser}
onUpdateMembership={handleUpdateMembership}
onDeleteMembership={handleDeleteMembership}
pendingAccessAssignments={pendingAccessAssignments}
pendingServiceModuleEntitlements={pendingServiceModuleEntitlements}
onCreateGroup={handleCreateGroup}
onUpdateGroup={handleUpdateGroup}
onDeleteGroup={handleDeleteGroup}
onUpdateService={handleUpdateService}
onReorderServices={handleReorderServices}
onCreateService={handleCreateService}
onDeleteService={handleDeleteService}
onUpdateSettings={handleUpdateSettings}
taskManagerWorkspaces={taskManagerWorkspaces}
taskManagerWorkspacesLoading={taskManagerWorkspacesLoading}
taskManagerWorkspacesError={taskManagerWorkspacesError}
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()}
onSetTaskManagerWorkspaceMemberRole={handleSetTaskManagerWorkspaceMemberRole}
onSetTaskManagerProjectMemberRole={handleSetTaskManagerProjectMemberRole}
onSetServiceModuleEntitlement={handleSetServiceModuleEntitlement}
/>
<Suspense fallback={null}>
<AdminOverlay
data={data}
me={runtimeMe}
mode={runtimeMe.launcherRole === "root_admin" ? adminMode : "admin"}
activeClientId={resolvedClientId}
onClose={() => setAdminOpen(false)}
onSetUserServiceAccess={handleSetUserServiceAccess}
onCreateInvite={handleCreateInvite}
onUpdateInvite={handleUpdateInvite}
onDeleteInvite={handleDeleteInvite}
onUpdateAccessRequest={handleUpdateAccessRequest}
onApproveAccessRequest={handleApproveAccessRequest}
onRejectAccessRequest={handleRejectAccessRequest}
onApproveTaskerInviteRequest={handleApproveTaskerInviteRequest}
onRejectTaskerInviteRequest={handleRejectTaskerInviteRequest}
onRetrySync={handleRetrySync}
onCreateClient={handleCreateClient}
onUpdateClient={handleUpdateClient}
onDeleteClient={handleDeleteClient}
onCreateUser={handleCreateUser}
onUpdateUser={handleUpdateUser}
onDeleteUser={handleDeleteUser}
onUpdateMembership={handleUpdateMembership}
onDeleteMembership={handleDeleteMembership}
pendingAccessAssignments={pendingAccessAssignments}
pendingServiceModuleEntitlements={pendingServiceModuleEntitlements}
onCreateGroup={handleCreateGroup}
onUpdateGroup={handleUpdateGroup}
onDeleteGroup={handleDeleteGroup}
onUpdateService={handleUpdateService}
onReorderServices={handleReorderServices}
onCreateService={handleCreateService}
onDeleteService={handleDeleteService}
onUpdateSettings={handleUpdateSettings}
taskManagerWorkspaces={taskManagerWorkspaces}
taskManagerWorkspacesLoading={taskManagerWorkspacesLoading}
taskManagerWorkspacesError={taskManagerWorkspacesError}
pendingTaskManagerMemberships={pendingTaskManagerMemberships}
pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
onRefreshTaskManagerWorkspaces={() => void refreshTaskManagerWorkspaces()}
onSetTaskManagerWorkspaceMemberRole={handleSetTaskManagerWorkspaceMemberRole}
onSetTaskManagerProjectMemberRole={handleSetTaskManagerProjectMemberRole}
onSetServiceModuleEntitlement={handleSetServiceModuleEntitlement}
/>
</Suspense>
) : null}
{profileSettingsOpen && activeProfileUser ? (
<ProfileSettingsPanel
user={activeProfileUser}
onClose={() => setProfileSettingsOpen(false)}
onSaveProfile={handleUpdateOwnProfile}
onChangePassword={handleUpdateOwnPassword}
/>
<Suspense fallback={null}>
<ProfileSettingsPanel
user={activeProfileUser}
onClose={() => setProfileSettingsOpen(false)}
onSaveProfile={handleUpdateOwnProfile}
onChangePassword={handleUpdateOwnPassword}
/>
</Suspense>
) : null}
<ServiceRail services={launcherServices} selectedServiceId={selectedServiceId} onSelect={handleServiceSelect} />
</main>

View File

@ -754,7 +754,6 @@ code {
content: "";
opacity: 0.34;
transform: rotate(-2deg);
animation: stageVideoDrift 16s linear infinite;
mix-blend-mode: screen;
}
@ -1045,15 +1044,6 @@ code {
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 {
from {
transform: translate3d(-0.8rem, 0, 0) rotate(-9deg) scale(1);
@ -1201,7 +1191,7 @@ code {
flex: 0 0 var(--service-rail-card-size);
overflow: hidden;
border-radius: 1rem;
background: color-mix(in srgb, var(--tile-accent) 36%, rgba(255, 255, 255, 0.12));
background: #050506;
}
.service-tile__media-asset {

View File

@ -3425,7 +3425,8 @@ function OperationalCoreAccessModal({
</div>
<div className="task-module-access-list">
{operationalCoreModules.map((module) => {
const enabled = hasServiceModuleEntitlement(data, client.id, user.id, service.id, module.id);
const systemEnabled = hasSystemServiceModuleEntitlement(user, 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 pending = Boolean(pendingServiceModuleEntitlements[pendingKey]);
@ -3435,7 +3436,7 @@ function OperationalCoreAccessModal({
className={cn("task-module-access-row", enabled && "task-module-access-row--enabled", pending && "task-module-access-row--pending")}
type="button"
aria-pressed={enabled}
disabled={pending}
disabled={pending || systemEnabled}
onClick={() =>
onSetServiceModuleEntitlement({
clientId: client.id,
@ -3454,7 +3455,7 @@ function OperationalCoreAccessModal({
<span className="task-module-checker" aria-hidden="true">
{enabled ? <span /> : null}
</span>
<span>{pending ? "Сохраняем..." : enabled ? module.enabledLabel : module.disabledLabel}</span>
<span>{pending ? "Сохраняем..." : systemEnabled ? "Системно включён" : enabled ? module.enabledLabel : module.disabledLabel}</span>
</span>
</button>
);
@ -4813,6 +4814,10 @@ 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 {
return service.slug === "task-manager" || service.authentikApplicationSlug === "task-manager";
}

View File

@ -1,5 +1,5 @@
import { ChevronRight } from "lucide-react";
import { DEFAULT_AMBIENT_MEDIA, resolveAmbientMedia } from "../../entities/service/media";
import { resolveAmbientMedia } from "../../entities/service/media";
import type { LauncherServiceView } from "../../entities/service/types";
import { cn } from "../../shared/lib/cn";
@ -34,8 +34,8 @@ export function ServiceRail({
>
<RailMedia
className="service-tile__media-asset"
src={service.media.coverImage ?? service.media.ambientVideo ?? DEFAULT_AMBIENT_MEDIA}
kind={service.media.coverKind ?? service.media.ambientKind}
src={service.media.coverImage ?? service.media.thumbnail ?? null}
kind={service.media.coverKind ?? "image"}
/>
</span>
<span className="service-tile__content">
@ -57,15 +57,17 @@ function RailMedia({
kind,
className,
}: {
src: string;
src?: string | null;
kind?: LauncherServiceView["media"]["coverKind"];
className: string;
}) {
if (!src) return null;
if (kind === "video" || isVideoSource(src)) {
return <video className={className} src={src} autoPlay loop muted playsInline />;
return <video className={className} src={src} autoPlay loop muted playsInline preload="metadata" />;
}
return <img className={className} src={src} alt="" />;
return <img className={className} src={src} alt="" loading="lazy" decoding="async" />;
}
function isVideoSource(src: string) {

View File

@ -62,7 +62,7 @@ export function ServiceStage({
<section className="service-stage" style={style}>
<div className="stage-video-shell">
<div className="stage-video-stream" aria-hidden="true">
<StageMedia className="stage-video-gif" src={ambientMedia.src} kind={ambientMedia.kind} />
<StageMedia className="stage-video-gif" src={ambientMedia.src} kind={ambientMedia.kind} priority={!service} />
</div>
{service ? (
@ -155,12 +155,22 @@ function ServiceIcon({ slug }: { slug: string }) {
return <Bot size={18} />;
}
function StageMedia({ src, kind, className }: { src: string; kind?: LauncherServiceView["media"]["coverKind"]; className: string }) {
function StageMedia({
src,
kind,
className,
priority = false,
}: {
src: string;
kind?: LauncherServiceView["media"]["coverKind"];
className: string;
priority?: boolean;
}) {
if (kind === "video" || isVideoSource(src)) {
return <video className={className} src={src} autoPlay loop muted playsInline />;
return <video className={className} src={src} autoPlay loop muted playsInline preload="metadata" />;
}
return <img className={className} src={src} alt="" />;
return <img className={className} src={src} alt="" loading={priority ? "eager" : "lazy"} decoding="async" />;
}
function RichDescription({ text }: { text: string }) {