@exoscient/control-panel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/AppControlPanel.d.ts +77 -0
- package/dist/AppControlPanel.d.ts.map +1 -0
- package/dist/AppControlPanel.js +1625 -0
- package/dist/AppControlPanel.js.map +1 -0
- package/dist/ControlPanelShell.d.ts +39 -0
- package/dist/ControlPanelShell.d.ts.map +1 -0
- package/dist/ControlPanelShell.js +152 -0
- package/dist/ControlPanelShell.js.map +1 -0
- package/dist/ExoLauncherSimulator.d.ts +36 -0
- package/dist/ExoLauncherSimulator.d.ts.map +1 -0
- package/dist/ExoLauncherSimulator.js +253 -0
- package/dist/ExoLauncherSimulator.js.map +1 -0
- package/dist/TaskDetail.d.ts +180 -0
- package/dist/TaskDetail.d.ts.map +1 -0
- package/dist/TaskDetail.js +889 -0
- package/dist/TaskDetail.js.map +1 -0
- package/dist/TaskListPage.d.ts +28 -0
- package/dist/TaskListPage.d.ts.map +1 -0
- package/dist/TaskListPage.js +16 -0
- package/dist/TaskListPage.js.map +1 -0
- package/dist/TaskWorkspace.d.ts +62 -0
- package/dist/TaskWorkspace.d.ts.map +1 -0
- package/dist/TaskWorkspace.js +592 -0
- package/dist/TaskWorkspace.js.map +1 -0
- package/dist/ai-plane.d.ts +75 -0
- package/dist/ai-plane.d.ts.map +1 -0
- package/dist/ai-plane.js +124 -0
- package/dist/ai-plane.js.map +1 -0
- package/dist/browser-icons.d.ts +25 -0
- package/dist/browser-icons.d.ts.map +1 -0
- package/dist/browser-icons.js +125 -0
- package/dist/browser-icons.js.map +1 -0
- package/dist/client-actions.d.ts +45 -0
- package/dist/client-actions.d.ts.map +1 -0
- package/dist/client-actions.js +48 -0
- package/dist/client-actions.js.map +1 -0
- package/dist/control-panel-shared.d.ts +58 -0
- package/dist/control-panel-shared.d.ts.map +1 -0
- package/dist/control-panel-shared.js +79 -0
- package/dist/control-panel-shared.js.map +1 -0
- package/dist/control-panel.css +4156 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/repository-workflow.d.ts +27 -0
- package/dist/repository-workflow.d.ts.map +1 -0
- package/dist/repository-workflow.js +24 -0
- package/dist/repository-workflow.js.map +1 -0
- package/dist/result.d.ts +6 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +77 -0
- package/dist/result.js.map +1 -0
- package/dist/task-consistency.d.ts +28 -0
- package/dist/task-consistency.d.ts.map +1 -0
- package/dist/task-consistency.js +25 -0
- package/dist/task-consistency.js.map +1 -0
- package/dist/task-detail.browser.js +2709 -0
- package/dist/task-detail.css +1601 -0
- package/dist/task-state.d.ts +11 -0
- package/dist/task-state.d.ts.map +1 -0
- package/dist/task-state.js +103 -0
- package/dist/task-state.js.map +1 -0
- package/dist/telemetry.d.ts +39 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +106 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/trace.d.ts +80 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +694 -0
- package/dist/trace.js.map +1 -0
- package/dist/updates.d.ts +72 -0
- package/dist/updates.d.ts.map +1 -0
- package/dist/updates.js +269 -0
- package/dist/updates.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1625 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { InlineSpinner, LoadingState, WorkingIndicator, formatDateTime, isTaskBusy, useElapsedNow, } from './ai-plane';
|
|
4
|
+
import { emptyRepositoryWorktreeStatus, repositoryBranchOptions, repositoryTaskCompletionBehaviors, repositoryWorkflowModes, } from './repository-workflow';
|
|
5
|
+
import { Activity, AppWindow, BriefcaseBusiness, ChevronDown, CheckCircle2, CircleOff, ClipboardList, Copy, ExternalLink, GitBranch, Image, Info, KeyRound, Menu, Pencil, Plus, RefreshCw, Save, Server, Smartphone, TerminalSquare, Trash2, Upload, WandSparkles, X, } from 'lucide-react';
|
|
6
|
+
import { ClientUpdateService } from './ai-plane';
|
|
7
|
+
import { ControlPanelNavigation, ControlPanelShell, } from './ControlPanelShell';
|
|
8
|
+
import { TaskWorkspace, } from './TaskWorkspace';
|
|
9
|
+
import { ExoLauncherSimulator, } from './ExoLauncherSimulator';
|
|
10
|
+
import { formatBytes, formatContainerPorts, taskModelOptionsFromStatus, } from './control-panel-shared';
|
|
11
|
+
const brandGraphicRoles = [
|
|
12
|
+
{ value: 'general', label: 'General' },
|
|
13
|
+
{ value: 'mobile-launcher', label: 'Mobile launcher' },
|
|
14
|
+
{ value: 'web-page-icon', label: 'Web page icon' },
|
|
15
|
+
{ value: 'control-panel', label: 'Control panel' },
|
|
16
|
+
{ value: 'host-listing', label: 'Host listing' },
|
|
17
|
+
{ value: 'splash', label: 'Splash' },
|
|
18
|
+
{ value: 'device-bundle', label: 'Device bundle' },
|
|
19
|
+
];
|
|
20
|
+
const initialParams = new URLSearchParams(window.location.search);
|
|
21
|
+
const initialMode = parseInitialMode(initialParams.get('start-mode') ?? initialParams.get('mode'));
|
|
22
|
+
const initialViewParam = initialParams.get('view');
|
|
23
|
+
const viewSlugs = {
|
|
24
|
+
tasks: 'tasks',
|
|
25
|
+
outlets: 'outlets',
|
|
26
|
+
launcherSimulator: 'exo-launcher-simulator',
|
|
27
|
+
controlRepo: 'control-panel-repo',
|
|
28
|
+
repo: 'repo',
|
|
29
|
+
deployments: 'deployments',
|
|
30
|
+
info: 'info',
|
|
31
|
+
api: 'api',
|
|
32
|
+
status: 'status',
|
|
33
|
+
};
|
|
34
|
+
function resolveParamView(attachedRepositoryEnabled, customViews) {
|
|
35
|
+
const allCustomViews = flattenCustomViews(customViews);
|
|
36
|
+
return initialViewParam === 'deviceApps' || initialViewParam === 'outlets'
|
|
37
|
+
? 'outlets'
|
|
38
|
+
: initialViewParam === 'launcherSimulator' || initialViewParam === 'exoLauncherSimulator'
|
|
39
|
+
? 'launcherSimulator'
|
|
40
|
+
: initialViewParam === 'controlPanelRepo' || initialViewParam === 'controlRepo'
|
|
41
|
+
? 'controlRepo'
|
|
42
|
+
: initialViewParam === 'repo' && attachedRepositoryEnabled
|
|
43
|
+
? 'repo'
|
|
44
|
+
: initialViewParam === 'deployments' && attachedRepositoryEnabled
|
|
45
|
+
? 'deployments'
|
|
46
|
+
: allCustomViews.some((customView) => initialViewParam === customView.id || initialViewParam === (customView.slug ?? customView.id))
|
|
47
|
+
? allCustomViews.find((customView) => initialViewParam === customView.id || initialViewParam === (customView.slug ?? customView.id)).id
|
|
48
|
+
: initialViewParam === 'api'
|
|
49
|
+
? 'api'
|
|
50
|
+
: initialViewParam === 'tasks'
|
|
51
|
+
? 'tasks'
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
function firstEnabledView(attachedRepositoryEnabled, customViews, disabledViews) {
|
|
55
|
+
const candidates = [
|
|
56
|
+
'tasks',
|
|
57
|
+
'info',
|
|
58
|
+
'outlets',
|
|
59
|
+
...flattenCustomViews(customViews).filter((item) => !item.hidden).map((item) => item.id),
|
|
60
|
+
'api',
|
|
61
|
+
'controlRepo',
|
|
62
|
+
...(attachedRepositoryEnabled ? ['repo', 'deployments'] : []),
|
|
63
|
+
'status',
|
|
64
|
+
];
|
|
65
|
+
return candidates.find((candidate) => !disabledViews.has(candidate)) ?? 'tasks';
|
|
66
|
+
}
|
|
67
|
+
function initialViewFor(attachedRepositoryEnabled, base, customViews, disabledViews, initialView) {
|
|
68
|
+
const fromPath = viewFromPath(base, customViews);
|
|
69
|
+
if (fromPath && !disabledViews.has(fromPath))
|
|
70
|
+
return fromPath;
|
|
71
|
+
const paramView = resolveParamView(attachedRepositoryEnabled, customViews);
|
|
72
|
+
if (paramView && !disabledViews.has(paramView))
|
|
73
|
+
return paramView;
|
|
74
|
+
if (initialView && !disabledViews.has(initialView))
|
|
75
|
+
return initialView;
|
|
76
|
+
return firstEnabledView(attachedRepositoryEnabled, customViews, disabledViews);
|
|
77
|
+
}
|
|
78
|
+
const authTokenKey = 'platform.authToken';
|
|
79
|
+
function authFetch(input, init = {}) {
|
|
80
|
+
const headers = new Headers(init.headers);
|
|
81
|
+
const token = localStorage.getItem(authTokenKey);
|
|
82
|
+
if (token)
|
|
83
|
+
headers.set('authorization', `Bearer ${token}`);
|
|
84
|
+
return fetch(input, { ...init, headers });
|
|
85
|
+
}
|
|
86
|
+
function hasTaskPermission(payload, permission, applicationId) {
|
|
87
|
+
if (!payload.required)
|
|
88
|
+
return true;
|
|
89
|
+
if (payload.access?.hostAdmin)
|
|
90
|
+
return true;
|
|
91
|
+
return Boolean(payload.access?.appPermissions?.some((item) => item.appId === applicationId && item.permission?.toLocaleLowerCase() === permission));
|
|
92
|
+
}
|
|
93
|
+
export function AppControlPanel({ config, customViews = [], disabledViews, initialView, onLogout, hidePlatformLink = false, accessOverride, currentUserId = null, taskDataSourceExtensions, }) {
|
|
94
|
+
const { base, appIconHref, applicationId, applicationHandle, applicationTitle, attachedRepositoryEnabled, defaultOutletSettings, } = config;
|
|
95
|
+
const disabledViewSet = useMemo(() => new Set(disabledViews ?? []), [disabledViews]);
|
|
96
|
+
const [accessReady, setAccessReady] = useState(false);
|
|
97
|
+
const [accessRequired, setAccessRequired] = useState(false);
|
|
98
|
+
const [accessDenied, setAccessDenied] = useState(false);
|
|
99
|
+
const [canEditAppSettings, setCanEditAppSettings] = useState(false);
|
|
100
|
+
const [canEditRepoWorkflow, setCanEditRepoWorkflow] = useState(false);
|
|
101
|
+
const [taskCreatePermission, setTaskCreatePermission] = useState('code');
|
|
102
|
+
const [canSelectTaskModel, setCanSelectTaskModel] = useState(false);
|
|
103
|
+
const [taskModelOptions, setTaskModelOptions] = useState([]);
|
|
104
|
+
const [view, setView] = useState(() => initialViewFor(attachedRepositoryEnabled, base, customViews, disabledViewSet, initialView));
|
|
105
|
+
const [appInfoDialogOpen, setAppInfoDialogOpen] = useState(false);
|
|
106
|
+
const [appInfoEditTarget, setAppInfoEditTarget] = useState('title');
|
|
107
|
+
const [appInfoDraft, setAppInfoDraft] = useState({
|
|
108
|
+
title: '',
|
|
109
|
+
description: '',
|
|
110
|
+
outlets: defaultOutletSettings,
|
|
111
|
+
telegramBot: { liveToken: '', testToken: '' },
|
|
112
|
+
});
|
|
113
|
+
const [brandGraphics, setBrandGraphics] = useState([]);
|
|
114
|
+
const [brandGraphicDialog, setBrandGraphicDialog] = useState(null);
|
|
115
|
+
const [brandGraphicDraft, setBrandGraphicDraft] = useState({ role: 'general', label: '', dataUrl: '', fileName: '', width: 0, height: 0 });
|
|
116
|
+
const [brandGraphicSubmitting, setBrandGraphicSubmitting] = useState(false);
|
|
117
|
+
const [generateGraphicDialogOpen, setGenerateGraphicDialogOpen] = useState(false);
|
|
118
|
+
const [generateGraphicDraft, setGenerateGraphicDraft] = useState({ role: 'general', size: '512x512', prompt: '' });
|
|
119
|
+
const [status, setStatus] = useState(null);
|
|
120
|
+
const [statusLoading, setStatusLoading] = useState(false);
|
|
121
|
+
const [appInfo, setAppInfo] = useState(null);
|
|
122
|
+
const [webLinks, setWebLinks] = useState([]);
|
|
123
|
+
const [deviceApps, setDeviceApps] = useState([]);
|
|
124
|
+
const [repoInfo, setRepoInfo] = useState(null);
|
|
125
|
+
const [controlPanelRepoInfo, setControlPanelRepoInfo] = useState(null);
|
|
126
|
+
const [controlPanelRepoSaving, setControlPanelRepoSaving] = useState(false);
|
|
127
|
+
const [controlPanelRepoError, setControlPanelRepoError] = useState(null);
|
|
128
|
+
const [repoFetching, setRepoFetching] = useState(false);
|
|
129
|
+
const [repoWorkspaceEdits, setRepoWorkspaceEdits] = useState({});
|
|
130
|
+
const [repoWorkflowDialog, setRepoWorkflowDialog] = useState(null);
|
|
131
|
+
const [repoInlineSelection, setRepoInlineSelection] = useState(null);
|
|
132
|
+
const [controlPanelRepoInlineSelection, setControlPanelRepoInlineSelection] = useState(null);
|
|
133
|
+
const [repoSavingWorkflow, setRepoSavingWorkflow] = useState(false);
|
|
134
|
+
const [repoSavingWorkspaceId, setRepoSavingWorkspaceId] = useState(null);
|
|
135
|
+
const [repoResetDialog, setRepoResetDialog] = useState(null);
|
|
136
|
+
const [repoResetting, setRepoResetting] = useState(false);
|
|
137
|
+
const [repoDeleteLocalBranchDialog, setRepoDeleteLocalBranchDialog] = useState(null);
|
|
138
|
+
const [repoDeletingLocalBranch, setRepoDeletingLocalBranch] = useState(false);
|
|
139
|
+
const [repoDeleteDialog, setRepoDeleteDialog] = useState(null);
|
|
140
|
+
const [repoDeleting, setRepoDeleting] = useState(false);
|
|
141
|
+
const [repoAttachOpen, setRepoAttachOpen] = useState(false);
|
|
142
|
+
const [repoAttachUrl, setRepoAttachUrl] = useState('');
|
|
143
|
+
const [repoAttachId, setRepoAttachId] = useState('');
|
|
144
|
+
const [repoAttachLabel, setRepoAttachLabel] = useState('');
|
|
145
|
+
const [repoWorkingBranch, setRepoWorkingBranch] = useState('main');
|
|
146
|
+
const [repoWorkflowMode, setRepoWorkflowMode] = useState('code-merge-request');
|
|
147
|
+
const [repoAccessKey, setRepoAccessKey] = useState('');
|
|
148
|
+
const [repoAttachError, setRepoAttachError] = useState('');
|
|
149
|
+
const [repoPreparingAccess, setRepoPreparingAccess] = useState(false);
|
|
150
|
+
const [repoAttaching, setRepoAttaching] = useState(false);
|
|
151
|
+
const [externalDeployments, setExternalDeployments] = useState(null);
|
|
152
|
+
const [externalDeploymentsLoading, setExternalDeploymentsLoading] = useState(false);
|
|
153
|
+
const [apiClients, setApiClients] = useState([]);
|
|
154
|
+
const [apiClientsLoading, setApiClientsLoading] = useState(false);
|
|
155
|
+
const [apiClientsError, setApiClientsError] = useState(null);
|
|
156
|
+
const [apiClientDialogOpen, setApiClientDialogOpen] = useState(false);
|
|
157
|
+
const [apiClientSubmitting, setApiClientSubmitting] = useState(false);
|
|
158
|
+
const [createdApiToken, setCreatedApiToken] = useState(null);
|
|
159
|
+
const [apiClientDraft, setApiClientDraft] = useState({
|
|
160
|
+
name: '',
|
|
161
|
+
accessLevels: ['query'],
|
|
162
|
+
operations: ['start_task', 'read_task', 'terminate_own_task'],
|
|
163
|
+
callbackAllowlist: '',
|
|
164
|
+
});
|
|
165
|
+
const [copiedDeviceApp, setCopiedDeviceApp] = useState(null);
|
|
166
|
+
const [launcherSimulatorTarget, setLauncherSimulatorTarget] = useState(() => parseLauncherSimulatorTarget(initialParams));
|
|
167
|
+
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
|
168
|
+
const [initialModeTaskId, setInitialModeTaskId] = useState(null);
|
|
169
|
+
const [initialModeStartedAt, setInitialModeStartedAt] = useState(null);
|
|
170
|
+
const initialModeAttemptedRef = useRef(false);
|
|
171
|
+
const initialModeOutcomeHandledRef = useRef(false);
|
|
172
|
+
const [onboardingTask, setOnboardingTask] = useState(null);
|
|
173
|
+
const [onboardingSubmitting, setOnboardingSubmitting] = useState(false);
|
|
174
|
+
const [taskWorkspaceDetailState, setTaskWorkspaceDetailState] = useState('none');
|
|
175
|
+
const [appInfoSubmitting, setAppInfoSubmitting] = useState(false);
|
|
176
|
+
const [platformStatus, setPlatformStatus] = useState({ available: true, message: 'Platform is available.' });
|
|
177
|
+
const updateService = useMemo(() => new ClientUpdateService(base, () => localStorage.getItem(authTokenKey)), []);
|
|
178
|
+
const taskCreatePermissionRef = useRef(taskCreatePermission);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
taskCreatePermissionRef.current = taskCreatePermission;
|
|
181
|
+
}, [taskCreatePermission]);
|
|
182
|
+
const taskDataSource = useMemo(() => ({
|
|
183
|
+
async loadTasks() {
|
|
184
|
+
const response = await authFetch(`${base}/api/state/tasks`);
|
|
185
|
+
if (!response.ok)
|
|
186
|
+
throw new Error(`Task state failed: ${response.status}`);
|
|
187
|
+
return await response.json();
|
|
188
|
+
},
|
|
189
|
+
async loadTask(taskId) {
|
|
190
|
+
const response = await authFetch(`${base}/api/state/tasks/${taskId}`);
|
|
191
|
+
if (!response.ok)
|
|
192
|
+
throw new Error(`Task detail failed: ${response.status}`);
|
|
193
|
+
return await response.json();
|
|
194
|
+
},
|
|
195
|
+
async loadTaskTrace(taskId, turnId) {
|
|
196
|
+
const response = await authFetch(`${base}/api/state/tasks/${taskId}/turns/${turnId}/trace`);
|
|
197
|
+
if (!response.ok)
|
|
198
|
+
throw new Error(`Task trace failed: ${response.status}`);
|
|
199
|
+
return await response.json();
|
|
200
|
+
},
|
|
201
|
+
async createTask(input) {
|
|
202
|
+
const response = await authFetch(`${base}/api/tasks`, {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
headers: { 'Content-Type': 'application/json' },
|
|
205
|
+
body: JSON.stringify({ ...input, taskPermission: taskCreatePermissionRef.current ?? 'code' }),
|
|
206
|
+
});
|
|
207
|
+
// The server is the authority on whether the request is accepted. Surface its rejection (e.g. a
|
|
208
|
+
// conflict) by throwing the server message so the caller can show it inline, rather than swallowing.
|
|
209
|
+
if (!response.ok)
|
|
210
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Your task could not be created.');
|
|
211
|
+
return await response.json();
|
|
212
|
+
},
|
|
213
|
+
async sendFollowUp(taskId, input) {
|
|
214
|
+
const response = await authFetch(`${base}/api/tasks/${taskId}/messages`, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: { 'Content-Type': 'application/json' },
|
|
217
|
+
body: JSON.stringify({ ...input, taskPermission: taskCreatePermissionRef.current ?? 'code' }),
|
|
218
|
+
});
|
|
219
|
+
if (!response.ok)
|
|
220
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Your message could not be sent.');
|
|
221
|
+
return await response.json();
|
|
222
|
+
},
|
|
223
|
+
async submitClientAction(taskId, input) {
|
|
224
|
+
const response = await authFetch(`${base}/api/tasks/${taskId}/external-continuations`, {
|
|
225
|
+
method: 'POST',
|
|
226
|
+
headers: { 'Content-Type': 'application/json' },
|
|
227
|
+
body: JSON.stringify({ token: input.token, message: input.message }),
|
|
228
|
+
});
|
|
229
|
+
if (!response.ok)
|
|
230
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Your decision could not be submitted.');
|
|
231
|
+
return await response.json();
|
|
232
|
+
},
|
|
233
|
+
async interruptTask(taskId, message) {
|
|
234
|
+
const response = await authFetch(`${base}/api/tasks/${taskId}/interrupt`, {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
headers: { 'Content-Type': 'application/json' },
|
|
237
|
+
body: JSON.stringify({ message }),
|
|
238
|
+
});
|
|
239
|
+
return response.ok ? await response.json() : null;
|
|
240
|
+
},
|
|
241
|
+
async steerTask(taskId, message) {
|
|
242
|
+
const response = await authFetch(`${base}/api/tasks/${taskId}/steer`, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: { 'Content-Type': 'application/json' },
|
|
245
|
+
body: JSON.stringify({ message }),
|
|
246
|
+
});
|
|
247
|
+
return response.ok ? await response.json() : null;
|
|
248
|
+
},
|
|
249
|
+
uploadAttachment: uploadTaskAttachment,
|
|
250
|
+
subscribe: (subscription) => updateService.subscribe(subscription),
|
|
251
|
+
setUpdateCursor: (cursor) => updateService.setCursor(cursor),
|
|
252
|
+
...(taskDataSourceExtensions ?? {}),
|
|
253
|
+
}), [base, updateService, taskDataSourceExtensions]);
|
|
254
|
+
const displayTitle = appInfo?.title || applicationTitle;
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
document.title = `${displayTitle} Control Panel`;
|
|
257
|
+
}, [displayTitle]);
|
|
258
|
+
const checkAccess = useCallback(async () => {
|
|
259
|
+
if (accessOverride) {
|
|
260
|
+
setAccessRequired(Boolean(accessOverride.required));
|
|
261
|
+
setAccessDenied(false);
|
|
262
|
+
setCanEditAppSettings(accessOverride.canEditAppSettings ?? true);
|
|
263
|
+
setCanEditRepoWorkflow(accessOverride.canEditRepoWorkflow ?? true);
|
|
264
|
+
setTaskCreatePermission(accessOverride.taskCreatePermission ?? 'code');
|
|
265
|
+
setCanSelectTaskModel(accessOverride.canSelectTaskModel ?? false);
|
|
266
|
+
setAccessReady(true);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const response = await authFetch(`${base}/api/access/me`);
|
|
271
|
+
if (response.status === 401 || response.status === 403) {
|
|
272
|
+
setAccessRequired(true);
|
|
273
|
+
setAccessDenied(true);
|
|
274
|
+
setCanEditAppSettings(false);
|
|
275
|
+
setCanEditRepoWorkflow(false);
|
|
276
|
+
setTaskCreatePermission(null);
|
|
277
|
+
setCanSelectTaskModel(false);
|
|
278
|
+
setAccessReady(true);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!response.ok)
|
|
282
|
+
throw new Error(`Access check failed: ${response.status}`);
|
|
283
|
+
const payload = await response.json();
|
|
284
|
+
const canCreateCodeTask = hasTaskPermission(payload, 'code', applicationId);
|
|
285
|
+
const canCreateQueryTask = hasTaskPermission(payload, 'query', applicationId);
|
|
286
|
+
setAccessRequired(Boolean(payload.required));
|
|
287
|
+
setAccessDenied(false);
|
|
288
|
+
setCanEditAppSettings(hasTaskPermission(payload, 'devops', applicationId));
|
|
289
|
+
setCanEditRepoWorkflow(!payload.required || Boolean(payload.access?.hostAdmin));
|
|
290
|
+
setTaskCreatePermission(canCreateCodeTask ? 'code' : canCreateQueryTask ? 'query' : null);
|
|
291
|
+
setCanSelectTaskModel(Boolean(payload.access?.hostAdmin));
|
|
292
|
+
setAccessReady(true);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
setAccessRequired(false);
|
|
296
|
+
setAccessDenied(false);
|
|
297
|
+
setCanEditAppSettings(true);
|
|
298
|
+
setCanEditRepoWorkflow(true);
|
|
299
|
+
setTaskCreatePermission('code');
|
|
300
|
+
setCanSelectTaskModel(false);
|
|
301
|
+
setAccessReady(true);
|
|
302
|
+
}
|
|
303
|
+
}, [accessOverride]);
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
void checkAccess();
|
|
306
|
+
}, [checkAccess]);
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (!canSelectTaskModel) {
|
|
309
|
+
setTaskModelOptions([]);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
void loadTaskModelOptions();
|
|
313
|
+
}, [canSelectTaskModel]);
|
|
314
|
+
async function loadTaskModelOptions() {
|
|
315
|
+
try {
|
|
316
|
+
const response = await authFetch(`${base}/api/coding-agent/status`);
|
|
317
|
+
if (!response.ok)
|
|
318
|
+
throw new Error(`Coding agent status failed: ${response.status}`);
|
|
319
|
+
setTaskModelOptions(taskModelOptionsFromStatus(await response.json()));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
setTaskModelOptions([]);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const outletCount = webLinks.length + deviceApps.length;
|
|
326
|
+
const onboardingTaskBusy = Boolean(onboardingTask && isTaskBusy(onboardingTask));
|
|
327
|
+
const initialModeBusy = onboardingSubmitting || onboardingTaskBusy;
|
|
328
|
+
const elapsedNow = useElapsedNow(initialModeBusy);
|
|
329
|
+
const onboardingStartedAtValue = initialModeStartedAt ?? onboardingTask?.startedAt ?? onboardingTask?.createdAt ?? null;
|
|
330
|
+
const initialModeElapsed = onboardingStartedAtValue ? formatElapsedMs(Math.max(0, elapsedNow - new Date(onboardingStartedAtValue).getTime())) : '';
|
|
331
|
+
async function loadStatus() {
|
|
332
|
+
setStatusLoading(true);
|
|
333
|
+
try {
|
|
334
|
+
const response = await authFetch(`${base}/api/status`);
|
|
335
|
+
if (!response.ok)
|
|
336
|
+
throw new Error(`Status failed: ${response.status}`);
|
|
337
|
+
setStatus(await response.json());
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// Transient status-fetch failure. The /api/platform-status poll is the single source of the
|
|
341
|
+
// availability banner, so don't flip the whole UI to "down" because one snapshot fetch blipped.
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
setStatusLoading(false);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async function loadAppInfo() {
|
|
348
|
+
try {
|
|
349
|
+
const response = await authFetch(`${base}/api/app-info`);
|
|
350
|
+
if (!response.ok)
|
|
351
|
+
throw new Error(`App info failed: ${response.status}`);
|
|
352
|
+
setAppInfo(await response.json());
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// Transient app-info fetch failure; the /api/platform-status poll owns the availability banner.
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async function loadBrandGraphics() {
|
|
359
|
+
try {
|
|
360
|
+
const response = await authFetch(`${base}/api/brand-graphics`);
|
|
361
|
+
if (!response.ok)
|
|
362
|
+
throw new Error(`Brand graphics failed: ${response.status}`);
|
|
363
|
+
const document = await response.json();
|
|
364
|
+
setBrandGraphics(document.graphics);
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
setBrandGraphics([]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function loadWebLinks() {
|
|
371
|
+
try {
|
|
372
|
+
const response = await authFetch(`${base}/api/web-links`);
|
|
373
|
+
if (!response.ok)
|
|
374
|
+
throw new Error(`Web links failed: ${response.status}`);
|
|
375
|
+
const links = await response.json();
|
|
376
|
+
setWebLinks(links);
|
|
377
|
+
return links;
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
setWebLinks([]);
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function loadDeviceApps() {
|
|
385
|
+
try {
|
|
386
|
+
const response = await authFetch(`${base}/api/device-apps`);
|
|
387
|
+
if (!response.ok)
|
|
388
|
+
throw new Error(`Device apps failed: ${response.status}`);
|
|
389
|
+
const apps = await response.json();
|
|
390
|
+
setDeviceApps(apps);
|
|
391
|
+
return apps;
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
setDeviceApps([]);
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async function loadRepoInfo() {
|
|
399
|
+
try {
|
|
400
|
+
const response = await authFetch(`${base}/api/repo-info`);
|
|
401
|
+
if (!response.ok)
|
|
402
|
+
throw new Error(`Repo info failed: ${response.status}`);
|
|
403
|
+
setRepoInfo(await response.json());
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
setRepoInfo(null);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async function loadControlPanelRepoInfo() {
|
|
410
|
+
try {
|
|
411
|
+
const response = await authFetch(`${base}/api/control-panel-repo-info`);
|
|
412
|
+
if (!response.ok)
|
|
413
|
+
throw new Error(`Control panel repo info failed: `);
|
|
414
|
+
setControlPanelRepoInfo(await response.json());
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
setControlPanelRepoInfo(null);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function saveControlPanelRepoWorkflow(patch) {
|
|
421
|
+
if (controlPanelRepoSaving)
|
|
422
|
+
return false;
|
|
423
|
+
setControlPanelRepoSaving(true);
|
|
424
|
+
setControlPanelRepoError(null);
|
|
425
|
+
try {
|
|
426
|
+
const response = await authFetch(`${base}/api/control-panel-repo-workflow`, {
|
|
427
|
+
method: 'PUT',
|
|
428
|
+
headers: { 'Content-Type': 'application/json' },
|
|
429
|
+
body: JSON.stringify({
|
|
430
|
+
workingBranch: patch.workingBranch,
|
|
431
|
+
workflowMode: patch.mode,
|
|
432
|
+
taskCompletionBehavior: patch.taskCompletionBehavior,
|
|
433
|
+
}),
|
|
434
|
+
});
|
|
435
|
+
if (!response.ok)
|
|
436
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Control panel repo workflow update failed.');
|
|
437
|
+
setControlPanelRepoInfo(await response.json());
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
catch (error) {
|
|
441
|
+
setControlPanelRepoError(error instanceof Error ? error.message : 'Control panel repo workflow update failed.');
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
setControlPanelRepoSaving(false);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function openControlPanelRepoInlineSelection(field) {
|
|
449
|
+
if (!controlPanelRepoInfo)
|
|
450
|
+
return;
|
|
451
|
+
setControlPanelRepoInlineSelection({
|
|
452
|
+
field,
|
|
453
|
+
draft: field === 'workflow' ? controlPanelRepoInfo.workflow.mode : controlPanelRepoInfo.taskCompletionBehavior || 'complete-if-possible',
|
|
454
|
+
error: null,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
function updateControlPanelRepoInlineSelection(field, draft) {
|
|
458
|
+
setControlPanelRepoInlineSelection({ field, draft, error: null });
|
|
459
|
+
}
|
|
460
|
+
async function saveControlPanelRepoInlineSelection(field) {
|
|
461
|
+
if (controlPanelRepoSaving || !controlPanelRepoInlineSelection || controlPanelRepoInlineSelection.field !== field)
|
|
462
|
+
return;
|
|
463
|
+
const draft = controlPanelRepoInlineSelection.draft;
|
|
464
|
+
const saved = await saveControlPanelRepoWorkflow(field === 'workflow' ? { mode: draft } : { taskCompletionBehavior: draft });
|
|
465
|
+
if (saved)
|
|
466
|
+
setControlPanelRepoInlineSelection(null);
|
|
467
|
+
}
|
|
468
|
+
async function loadExternalDeployments() {
|
|
469
|
+
if (!attachedRepositoryEnabled)
|
|
470
|
+
return;
|
|
471
|
+
setExternalDeploymentsLoading(true);
|
|
472
|
+
try {
|
|
473
|
+
const response = await authFetch(`${base}/api/external-deployments`);
|
|
474
|
+
if (!response.ok)
|
|
475
|
+
throw new Error(`External deployments failed: ${response.status}`);
|
|
476
|
+
setExternalDeployments(await response.json());
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
setExternalDeployments({ schemaVersion: 1, deployments: [] });
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
setExternalDeploymentsLoading(false);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
async function loadApiClients() {
|
|
486
|
+
setApiClientsLoading(true);
|
|
487
|
+
setApiClientsError(null);
|
|
488
|
+
try {
|
|
489
|
+
const response = await authFetch(`${base}/api/api-clients`);
|
|
490
|
+
if (!response.ok)
|
|
491
|
+
throw new Error(response.status === 403 ? 'Devops access is required to manage API clients.' : `API clients failed: ${response.status}`);
|
|
492
|
+
const document = await response.json();
|
|
493
|
+
setApiClients(document.clients);
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
setApiClientsError(error instanceof Error ? error.message : 'API clients could not be loaded.');
|
|
497
|
+
}
|
|
498
|
+
finally {
|
|
499
|
+
setApiClientsLoading(false);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function openApiClientDialog() {
|
|
503
|
+
setApiClientDraft({
|
|
504
|
+
name: '',
|
|
505
|
+
accessLevels: ['query'],
|
|
506
|
+
operations: ['start_task', 'read_task', 'terminate_own_task'],
|
|
507
|
+
callbackAllowlist: '',
|
|
508
|
+
});
|
|
509
|
+
setCreatedApiToken(null);
|
|
510
|
+
setApiClientDialogOpen(true);
|
|
511
|
+
}
|
|
512
|
+
async function submitApiClient(event) {
|
|
513
|
+
event.preventDefault();
|
|
514
|
+
if (!apiClientDraft.name.trim() || apiClientSubmitting)
|
|
515
|
+
return;
|
|
516
|
+
setApiClientSubmitting(true);
|
|
517
|
+
try {
|
|
518
|
+
const response = await authFetch(`${base}/api/api-clients`, {
|
|
519
|
+
method: 'POST',
|
|
520
|
+
headers: { 'Content-Type': 'application/json' },
|
|
521
|
+
body: JSON.stringify({
|
|
522
|
+
name: apiClientDraft.name,
|
|
523
|
+
accessLevels: apiClientDraft.accessLevels,
|
|
524
|
+
operations: apiClientDraft.operations,
|
|
525
|
+
callbackAllowlist: apiClientDraft.callbackAllowlist
|
|
526
|
+
.split(/\r?\n|,/)
|
|
527
|
+
.map((item) => item.trim())
|
|
528
|
+
.filter(Boolean),
|
|
529
|
+
}),
|
|
530
|
+
});
|
|
531
|
+
if (response.ok) {
|
|
532
|
+
const created = await response.json();
|
|
533
|
+
setApiClients((current) => [created.client, ...current]);
|
|
534
|
+
setCreatedApiToken(created.token);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
setApiClientsError(`API client was not created: ${response.status}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
setApiClientSubmitting(false);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function revokeApiClient(client) {
|
|
545
|
+
const response = await authFetch(`${base}/api/api-clients/${client.id}/revoke`, { method: 'POST' });
|
|
546
|
+
if (response.ok) {
|
|
547
|
+
const updated = await response.json();
|
|
548
|
+
setApiClients((current) => current.map((item) => item.id === updated.id ? updated : item));
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function toggleApiClientDraftValue(field, value) {
|
|
552
|
+
setApiClientDraft((draft) => {
|
|
553
|
+
const selected = draft[field].includes(value)
|
|
554
|
+
? draft[field].filter((item) => item !== value)
|
|
555
|
+
: [...draft[field], value];
|
|
556
|
+
return { ...draft, [field]: selected.length > 0 ? selected : [value] };
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
async function syncRepoNow(repository) {
|
|
560
|
+
if (repoFetching)
|
|
561
|
+
return;
|
|
562
|
+
setRepoFetching(true);
|
|
563
|
+
try {
|
|
564
|
+
const response = await authFetch(`${base}/api/repositories/${encodeURIComponent(repository.id)}/sync`, { method: 'POST' });
|
|
565
|
+
if (response.ok) {
|
|
566
|
+
setRepoInfo(await response.json());
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
await loadRepoInfo();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
setRepoFetching(false);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
async function prepareRepositoryAccess() {
|
|
577
|
+
if (!repoAttachUrl.trim() || repoPreparingAccess)
|
|
578
|
+
return;
|
|
579
|
+
setRepoAttachError('');
|
|
580
|
+
setRepoAccessKey('');
|
|
581
|
+
setRepoPreparingAccess(true);
|
|
582
|
+
try {
|
|
583
|
+
const response = await authFetch(`${base}/api/repositories/access-key`, {
|
|
584
|
+
method: 'POST',
|
|
585
|
+
headers: { 'Content-Type': 'application/json' },
|
|
586
|
+
body: JSON.stringify({ repositoryUrl: repoAttachUrl.trim(), id: repoAttachId.trim() || null }),
|
|
587
|
+
});
|
|
588
|
+
if (!response.ok)
|
|
589
|
+
throw new Error(await response.text());
|
|
590
|
+
const result = await response.json();
|
|
591
|
+
setRepoAttachId(result.id);
|
|
592
|
+
setRepoAttachUrl(result.repositoryUrl);
|
|
593
|
+
setRepoAccessKey(result.publicKey);
|
|
594
|
+
}
|
|
595
|
+
catch (error) {
|
|
596
|
+
setRepoAttachError(error instanceof Error ? error.message : 'Repository access key preparation failed.');
|
|
597
|
+
}
|
|
598
|
+
finally {
|
|
599
|
+
setRepoPreparingAccess(false);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async function attachRepository() {
|
|
603
|
+
if (!repoAttachUrl.trim() || repoAttaching)
|
|
604
|
+
return;
|
|
605
|
+
setRepoAttachError('');
|
|
606
|
+
setRepoAttaching(true);
|
|
607
|
+
try {
|
|
608
|
+
const response = await authFetch(`${base}/api/repositories`, {
|
|
609
|
+
method: 'POST',
|
|
610
|
+
headers: { 'Content-Type': 'application/json' },
|
|
611
|
+
body: JSON.stringify({
|
|
612
|
+
repositoryUrl: repoAttachUrl.trim(),
|
|
613
|
+
id: repoAttachId.trim() || null,
|
|
614
|
+
label: repoAttachLabel.trim() || null,
|
|
615
|
+
workingBranch: repoWorkingBranch.trim() || 'main',
|
|
616
|
+
workflowMode: repoWorkflowMode,
|
|
617
|
+
}),
|
|
618
|
+
});
|
|
619
|
+
if (!response.ok)
|
|
620
|
+
throw new Error(await response.text());
|
|
621
|
+
setRepoInfo(await response.json());
|
|
622
|
+
setRepoAttachOpen(false);
|
|
623
|
+
setRepoAttachUrl('');
|
|
624
|
+
setRepoAttachId('');
|
|
625
|
+
setRepoAttachLabel('');
|
|
626
|
+
setRepoAccessKey('');
|
|
627
|
+
setRepoWorkingBranch('main');
|
|
628
|
+
setRepoWorkflowMode('code-merge-request');
|
|
629
|
+
}
|
|
630
|
+
catch (error) {
|
|
631
|
+
setRepoAttachError(error instanceof Error ? error.message : 'Repository attachment failed.');
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
setRepoAttaching(false);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
function updateRepoWorkflowDialog(patch) {
|
|
638
|
+
setRepoWorkflowDialog((current) => current
|
|
639
|
+
? {
|
|
640
|
+
...current,
|
|
641
|
+
draft: {
|
|
642
|
+
...current.draft,
|
|
643
|
+
...patch,
|
|
644
|
+
},
|
|
645
|
+
error: null,
|
|
646
|
+
}
|
|
647
|
+
: current);
|
|
648
|
+
}
|
|
649
|
+
function openRepoInlineSelection(repository, field) {
|
|
650
|
+
setRepoInlineSelection({
|
|
651
|
+
repositoryId: repository.id,
|
|
652
|
+
field,
|
|
653
|
+
draft: field === 'branch' ? repository.workflow.workingBranch : repository.workflow.mode,
|
|
654
|
+
error: null,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
function updateRepoInlineSelection(repository, field, draft) {
|
|
658
|
+
setRepoInlineSelection({
|
|
659
|
+
repositoryId: repository.id,
|
|
660
|
+
field,
|
|
661
|
+
draft,
|
|
662
|
+
error: null,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
function repoWorkspaceTaskCompletionDraft(repository) {
|
|
666
|
+
return repoWorkspaceEdits[repository.id] || repository.taskCompletionBehavior || 'complete-if-possible';
|
|
667
|
+
}
|
|
668
|
+
function updateRepoWorkspaceTaskCompletionDraft(repository, taskCompletionBehavior) {
|
|
669
|
+
setRepoWorkspaceEdits((current) => ({
|
|
670
|
+
...current,
|
|
671
|
+
[repository.id]: taskCompletionBehavior,
|
|
672
|
+
}));
|
|
673
|
+
}
|
|
674
|
+
async function saveRepoWorkflow(event) {
|
|
675
|
+
event.preventDefault();
|
|
676
|
+
if (repoSavingWorkflow || !repoWorkflowDialog)
|
|
677
|
+
return;
|
|
678
|
+
const { repository, draft } = repoWorkflowDialog;
|
|
679
|
+
setRepoSavingWorkflow(true);
|
|
680
|
+
setRepoWorkflowDialog((current) => current ? { ...current, error: null } : current);
|
|
681
|
+
try {
|
|
682
|
+
const response = await authFetch(`${base}/api/repositories/${encodeURIComponent(repository.id)}/workflow`, {
|
|
683
|
+
method: 'PUT',
|
|
684
|
+
headers: { 'Content-Type': 'application/json' },
|
|
685
|
+
body: JSON.stringify({
|
|
686
|
+
workingBranch: draft.workingBranch,
|
|
687
|
+
workflowMode: canEditRepoWorkflow ? draft.mode : undefined,
|
|
688
|
+
}),
|
|
689
|
+
});
|
|
690
|
+
if (!response.ok)
|
|
691
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Repository workflow update failed.');
|
|
692
|
+
setRepoInfo(await response.json());
|
|
693
|
+
setRepoWorkflowDialog(null);
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
setRepoWorkflowDialog((current) => current
|
|
697
|
+
? {
|
|
698
|
+
...current,
|
|
699
|
+
error: error instanceof Error ? error.message : 'Repository workflow update failed.',
|
|
700
|
+
}
|
|
701
|
+
: current);
|
|
702
|
+
}
|
|
703
|
+
finally {
|
|
704
|
+
setRepoSavingWorkflow(false);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
async function saveRepoInlineSelection(repository, field) {
|
|
708
|
+
if (repoSavingWorkflow || !repoInlineSelection || repoInlineSelection.repositoryId !== repository.id || repoInlineSelection.field !== field)
|
|
709
|
+
return;
|
|
710
|
+
const draft = repoInlineSelection.draft.trim();
|
|
711
|
+
if (!draft)
|
|
712
|
+
return;
|
|
713
|
+
setRepoSavingWorkflow(true);
|
|
714
|
+
setRepoInlineSelection((current) => current ? { ...current, error: null } : current);
|
|
715
|
+
try {
|
|
716
|
+
const response = await authFetch(`${base}/api/repositories/${encodeURIComponent(repository.id)}/workflow`, {
|
|
717
|
+
method: 'PUT',
|
|
718
|
+
headers: { 'Content-Type': 'application/json' },
|
|
719
|
+
body: JSON.stringify({
|
|
720
|
+
workingBranch: field === 'branch' ? draft : repository.workflow.workingBranch,
|
|
721
|
+
workflowMode: canEditRepoWorkflow && field === 'workflow' ? draft : undefined,
|
|
722
|
+
}),
|
|
723
|
+
});
|
|
724
|
+
if (!response.ok)
|
|
725
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Repository workflow update failed.');
|
|
726
|
+
setRepoInfo(await response.json());
|
|
727
|
+
setRepoInlineSelection(null);
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
setRepoInlineSelection((current) => current
|
|
731
|
+
? {
|
|
732
|
+
...current,
|
|
733
|
+
error: error instanceof Error ? error.message : 'Repository workflow update failed.',
|
|
734
|
+
}
|
|
735
|
+
: current);
|
|
736
|
+
}
|
|
737
|
+
finally {
|
|
738
|
+
setRepoSavingWorkflow(false);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function saveRepoWorkspaceTaskCompletion(repository) {
|
|
742
|
+
if (repoSavingWorkspaceId)
|
|
743
|
+
return;
|
|
744
|
+
const taskCompletionBehavior = repoWorkspaceTaskCompletionDraft(repository);
|
|
745
|
+
setRepoSavingWorkspaceId(repository.id);
|
|
746
|
+
try {
|
|
747
|
+
const response = await authFetch(`${base}/api/repositories/${encodeURIComponent(repository.id)}/workspace`, {
|
|
748
|
+
method: 'PUT',
|
|
749
|
+
headers: { 'Content-Type': 'application/json' },
|
|
750
|
+
body: JSON.stringify({ taskCompletionBehavior }),
|
|
751
|
+
});
|
|
752
|
+
if (!response.ok)
|
|
753
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Repository workspace update failed.');
|
|
754
|
+
setRepoInfo(await response.json());
|
|
755
|
+
setRepoWorkspaceEdits((current) => {
|
|
756
|
+
const next = { ...current };
|
|
757
|
+
delete next[repository.id];
|
|
758
|
+
return next;
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
finally {
|
|
762
|
+
setRepoSavingWorkspaceId(null);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function openRepoResetDialog(repository) {
|
|
766
|
+
setRepoResetDialog({
|
|
767
|
+
repository,
|
|
768
|
+
error: null,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
async function resetRepoBranch(event) {
|
|
772
|
+
event.preventDefault();
|
|
773
|
+
if (repoResetting || !repoResetDialog)
|
|
774
|
+
return;
|
|
775
|
+
const { repository } = repoResetDialog;
|
|
776
|
+
setRepoResetting(true);
|
|
777
|
+
setRepoResetDialog((current) => current ? { ...current, error: null } : current);
|
|
778
|
+
try {
|
|
779
|
+
const response = await authFetch(`${base}/api/repositories/${encodeURIComponent(repository.id)}/reset-branch`, { method: 'POST' });
|
|
780
|
+
if (!response.ok)
|
|
781
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Repository branch reset failed.');
|
|
782
|
+
setRepoInfo(await response.json());
|
|
783
|
+
setRepoResetDialog(null);
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
setRepoResetDialog((current) => current
|
|
787
|
+
? {
|
|
788
|
+
...current,
|
|
789
|
+
error: error instanceof Error ? error.message : 'Repository branch reset failed.',
|
|
790
|
+
}
|
|
791
|
+
: current);
|
|
792
|
+
}
|
|
793
|
+
finally {
|
|
794
|
+
setRepoResetting(false);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function openRepoDeleteLocalBranchDialog(repository) {
|
|
798
|
+
setRepoDeleteLocalBranchDialog({
|
|
799
|
+
repository,
|
|
800
|
+
error: null,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
async function deleteRepoLocalBranch(event) {
|
|
804
|
+
event.preventDefault();
|
|
805
|
+
if (repoDeletingLocalBranch || !repoDeleteLocalBranchDialog)
|
|
806
|
+
return;
|
|
807
|
+
const { repository } = repoDeleteLocalBranchDialog;
|
|
808
|
+
setRepoDeletingLocalBranch(true);
|
|
809
|
+
setRepoDeleteLocalBranchDialog((current) => current ? { ...current, error: null } : current);
|
|
810
|
+
try {
|
|
811
|
+
const response = await authFetch(`${base}/api/repositories/${encodeURIComponent(repository.id)}/delete-local-branch`, { method: 'POST' });
|
|
812
|
+
if (!response.ok)
|
|
813
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Local branch deletion failed.');
|
|
814
|
+
setRepoInfo(await response.json());
|
|
815
|
+
setRepoDeleteLocalBranchDialog(null);
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
setRepoDeleteLocalBranchDialog((current) => current
|
|
819
|
+
? {
|
|
820
|
+
...current,
|
|
821
|
+
error: error instanceof Error ? error.message : 'Local branch deletion failed.',
|
|
822
|
+
}
|
|
823
|
+
: current);
|
|
824
|
+
}
|
|
825
|
+
finally {
|
|
826
|
+
setRepoDeletingLocalBranch(false);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function openRepoDeleteDialog(repository) {
|
|
830
|
+
setRepoDeleteDialog({
|
|
831
|
+
repository,
|
|
832
|
+
error: null,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
async function deleteRepository(event) {
|
|
836
|
+
event.preventDefault();
|
|
837
|
+
if (repoDeleting || !repoDeleteDialog)
|
|
838
|
+
return;
|
|
839
|
+
const { repository } = repoDeleteDialog;
|
|
840
|
+
setRepoDeleting(true);
|
|
841
|
+
setRepoDeleteDialog((current) => current ? { ...current, error: null } : current);
|
|
842
|
+
try {
|
|
843
|
+
const response = await authFetch(`${base}/api/repositories/${encodeURIComponent(repository.id)}`, { method: 'DELETE' });
|
|
844
|
+
if (!response.ok)
|
|
845
|
+
throw new Error(parseErrorMessage(await response.text()) || 'Repository deletion failed.');
|
|
846
|
+
setRepoInfo(await response.json());
|
|
847
|
+
setRepoDeleteDialog(null);
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
setRepoDeleteDialog((current) => current
|
|
851
|
+
? {
|
|
852
|
+
...current,
|
|
853
|
+
error: error instanceof Error ? error.message : 'Repository deletion failed.',
|
|
854
|
+
}
|
|
855
|
+
: current);
|
|
856
|
+
}
|
|
857
|
+
finally {
|
|
858
|
+
setRepoDeleting(false);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function waitForCreatedOutlets() {
|
|
862
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
863
|
+
const [links, apps] = await Promise.all([loadWebLinks(), loadDeviceApps()]);
|
|
864
|
+
if (links.length > 0 || apps.length > 0) {
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
await delay(1500);
|
|
868
|
+
}
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
const loadPlatformStatus = useCallback(async () => {
|
|
872
|
+
try {
|
|
873
|
+
const response = await authFetch(`${base}/api/platform-status`);
|
|
874
|
+
if (!response.ok)
|
|
875
|
+
throw new Error(`Platform status failed: ${response.status}`);
|
|
876
|
+
setPlatformStatus(await response.json());
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
setPlatformStatus({ available: false, message: 'The platform is down. Wait until it is back up.' });
|
|
880
|
+
}
|
|
881
|
+
}, []);
|
|
882
|
+
useEffect(() => {
|
|
883
|
+
loadStatus();
|
|
884
|
+
loadAppInfo();
|
|
885
|
+
loadBrandGraphics();
|
|
886
|
+
loadWebLinks();
|
|
887
|
+
loadDeviceApps();
|
|
888
|
+
if (attachedRepositoryEnabled)
|
|
889
|
+
loadRepoInfo();
|
|
890
|
+
loadPlatformStatus();
|
|
891
|
+
let platformTimer;
|
|
892
|
+
const schedulePlatform = () => {
|
|
893
|
+
platformTimer = window.setTimeout(async () => {
|
|
894
|
+
await loadPlatformStatus();
|
|
895
|
+
schedulePlatform();
|
|
896
|
+
}, 30000);
|
|
897
|
+
};
|
|
898
|
+
schedulePlatform();
|
|
899
|
+
const statusTimer = window.setInterval(loadStatus, 30000);
|
|
900
|
+
return () => {
|
|
901
|
+
window.clearInterval(statusTimer);
|
|
902
|
+
if (platformTimer !== undefined)
|
|
903
|
+
window.clearTimeout(platformTimer);
|
|
904
|
+
};
|
|
905
|
+
}, [loadPlatformStatus]);
|
|
906
|
+
useEffect(() => {
|
|
907
|
+
if (view === 'outlets') {
|
|
908
|
+
loadWebLinks();
|
|
909
|
+
loadDeviceApps();
|
|
910
|
+
}
|
|
911
|
+
}, [view]);
|
|
912
|
+
useEffect(() => {
|
|
913
|
+
if (view === 'launcherSimulator') {
|
|
914
|
+
loadDeviceApps();
|
|
915
|
+
}
|
|
916
|
+
}, [view]);
|
|
917
|
+
useEffect(() => {
|
|
918
|
+
if (view === 'deployments') {
|
|
919
|
+
loadExternalDeployments();
|
|
920
|
+
}
|
|
921
|
+
}, [view]);
|
|
922
|
+
useEffect(() => {
|
|
923
|
+
if (view !== 'outlets' || outletCount > 0) {
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
let cancelled = false;
|
|
927
|
+
const timer = window.setInterval(() => {
|
|
928
|
+
if (!cancelled) {
|
|
929
|
+
loadWebLinks();
|
|
930
|
+
loadDeviceApps();
|
|
931
|
+
}
|
|
932
|
+
}, 1500);
|
|
933
|
+
return () => {
|
|
934
|
+
cancelled = true;
|
|
935
|
+
window.clearInterval(timer);
|
|
936
|
+
};
|
|
937
|
+
}, [view, outletCount]);
|
|
938
|
+
useEffect(() => {
|
|
939
|
+
if (view === 'info') {
|
|
940
|
+
loadAppInfo();
|
|
941
|
+
}
|
|
942
|
+
}, [view]);
|
|
943
|
+
useEffect(() => {
|
|
944
|
+
if (view === 'api') {
|
|
945
|
+
loadApiClients();
|
|
946
|
+
}
|
|
947
|
+
}, [view]);
|
|
948
|
+
useEffect(() => {
|
|
949
|
+
if (view === 'status') {
|
|
950
|
+
loadStatus();
|
|
951
|
+
}
|
|
952
|
+
}, [view]);
|
|
953
|
+
useEffect(() => {
|
|
954
|
+
if (view === 'controlRepo') {
|
|
955
|
+
loadControlPanelRepoInfo();
|
|
956
|
+
}
|
|
957
|
+
}, [view]);
|
|
958
|
+
useEffect(() => {
|
|
959
|
+
if (view === 'repo') {
|
|
960
|
+
loadRepoInfo();
|
|
961
|
+
}
|
|
962
|
+
}, [view]);
|
|
963
|
+
useEffect(() => {
|
|
964
|
+
if (initialMode === 'none' || !appInfo || !accessReady) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (initialModeAttemptedRef.current || !taskCreatePermission) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const prompt = initialMode === 'create-app'
|
|
971
|
+
? initialCreationPrompt(config)
|
|
972
|
+
: initialMode === 'discuss'
|
|
973
|
+
? discussIdeaPrompt(appInfo, config)
|
|
974
|
+
: initialMode === 'customize'
|
|
975
|
+
? customizeImportedRepoPrompt(appInfo, config)
|
|
976
|
+
: null;
|
|
977
|
+
if (!prompt) {
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
initialModeAttemptedRef.current = true;
|
|
981
|
+
void (async () => {
|
|
982
|
+
setOnboardingSubmitting(true);
|
|
983
|
+
try {
|
|
984
|
+
const snapshot = await taskDataSource.loadTasks();
|
|
985
|
+
const existing = [...snapshot.state].sort((left, right) => new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime())[0];
|
|
986
|
+
if (existing) {
|
|
987
|
+
consumeInitialModeQuery();
|
|
988
|
+
setInitialModeTaskId(existing.id);
|
|
989
|
+
setOnboardingTask(existing);
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
setInitialModeStartedAt(new Date().toISOString());
|
|
993
|
+
const created = await taskDataSource.createTask({ message: prompt, attachments: [] });
|
|
994
|
+
if (created) {
|
|
995
|
+
consumeInitialModeQuery();
|
|
996
|
+
setInitialModeTaskId(created.id);
|
|
997
|
+
setOnboardingTask(created);
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
initialModeAttemptedRef.current = false;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
initialModeAttemptedRef.current = false;
|
|
1005
|
+
}
|
|
1006
|
+
finally {
|
|
1007
|
+
setOnboardingSubmitting(false);
|
|
1008
|
+
}
|
|
1009
|
+
})();
|
|
1010
|
+
}, [accessReady, appInfo, config, taskCreatePermission, taskDataSource]);
|
|
1011
|
+
useEffect(() => {
|
|
1012
|
+
if (!initialModeTaskId) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (onboardingTask && !onboardingTaskBusy) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
let cancelled = false;
|
|
1019
|
+
const poll = async () => {
|
|
1020
|
+
try {
|
|
1021
|
+
const snapshot = await taskDataSource.loadTask(initialModeTaskId);
|
|
1022
|
+
if (!cancelled)
|
|
1023
|
+
setOnboardingTask(snapshot.state);
|
|
1024
|
+
}
|
|
1025
|
+
catch {
|
|
1026
|
+
// Polling failures are non-fatal; the next tick retries.
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
const timer = window.setInterval(() => void poll(), 1500);
|
|
1030
|
+
return () => {
|
|
1031
|
+
cancelled = true;
|
|
1032
|
+
window.clearInterval(timer);
|
|
1033
|
+
};
|
|
1034
|
+
}, [initialModeTaskId, onboardingTask, onboardingTaskBusy, taskDataSource]);
|
|
1035
|
+
useEffect(() => {
|
|
1036
|
+
if (initialMode !== 'create-app' || onboardingTask?.status !== 'completed') {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
void loadWebLinks();
|
|
1040
|
+
void loadDeviceApps();
|
|
1041
|
+
}, [onboardingTask?.status]);
|
|
1042
|
+
useEffect(() => {
|
|
1043
|
+
if (initialMode !== 'create-app' || onboardingTask?.status !== 'completed' || initialModeOutcomeHandledRef.current) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
initialModeOutcomeHandledRef.current = true;
|
|
1047
|
+
void (async () => {
|
|
1048
|
+
if (webLinks.length > 0 || deviceApps.length > 0 || await waitForCreatedOutlets()) {
|
|
1049
|
+
window.location.replace(controlViewUrl('outlets'));
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
window.location.replace(controlHomeUrl());
|
|
1053
|
+
})();
|
|
1054
|
+
}, [onboardingTask?.status]);
|
|
1055
|
+
function uploadTaskAttachment(file, onProgress) {
|
|
1056
|
+
return new Promise((resolve, reject) => {
|
|
1057
|
+
const request = new XMLHttpRequest();
|
|
1058
|
+
const form = new FormData();
|
|
1059
|
+
form.append('file', file);
|
|
1060
|
+
request.open('POST', `${base}/api/task-attachments`);
|
|
1061
|
+
const token = localStorage.getItem(authTokenKey);
|
|
1062
|
+
if (token)
|
|
1063
|
+
request.setRequestHeader('authorization', `Bearer ${token}`);
|
|
1064
|
+
request.upload.onprogress = (event) => {
|
|
1065
|
+
if (event.lengthComputable)
|
|
1066
|
+
onProgress((event.loaded / event.total) * 100);
|
|
1067
|
+
};
|
|
1068
|
+
request.onload = () => {
|
|
1069
|
+
if (request.status >= 200 && request.status < 300) {
|
|
1070
|
+
resolve(JSON.parse(request.responseText));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
reject(new Error(parseErrorMessage(request.responseText) ?? `Upload failed: ${request.status}`));
|
|
1074
|
+
};
|
|
1075
|
+
request.onerror = () => reject(new Error('Upload failed.'));
|
|
1076
|
+
request.send(form);
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
function openAppInfoDialog(target) {
|
|
1080
|
+
if (!canEditAppSettings)
|
|
1081
|
+
return;
|
|
1082
|
+
setAppInfoEditTarget(target);
|
|
1083
|
+
setAppInfoDraft({
|
|
1084
|
+
title: appInfo?.title || applicationTitle,
|
|
1085
|
+
description: appInfo?.description || '',
|
|
1086
|
+
outlets: appInfo?.outlets || defaultOutletSettings,
|
|
1087
|
+
telegramBot: {
|
|
1088
|
+
liveToken: appInfo?.telegramBot?.liveToken || '',
|
|
1089
|
+
testToken: appInfo?.telegramBot?.testToken || '',
|
|
1090
|
+
},
|
|
1091
|
+
});
|
|
1092
|
+
setAppInfoDialogOpen(true);
|
|
1093
|
+
}
|
|
1094
|
+
async function submitAppInfo(event) {
|
|
1095
|
+
event.preventDefault();
|
|
1096
|
+
if (!canEditAppSettings || !appInfoDraft.title.trim() || appInfoSubmitting)
|
|
1097
|
+
return;
|
|
1098
|
+
setAppInfoSubmitting(true);
|
|
1099
|
+
try {
|
|
1100
|
+
const response = await authFetch(`${base}/api/app-info`, {
|
|
1101
|
+
method: 'PUT',
|
|
1102
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1103
|
+
body: JSON.stringify({
|
|
1104
|
+
title: appInfoDraft.title,
|
|
1105
|
+
description: appInfoDraft.description,
|
|
1106
|
+
outlets: appInfoDraft.outlets,
|
|
1107
|
+
telegramBot: appInfoDraft.telegramBot,
|
|
1108
|
+
}),
|
|
1109
|
+
});
|
|
1110
|
+
if (response.ok) {
|
|
1111
|
+
setAppInfo(await response.json());
|
|
1112
|
+
setAppInfoDialogOpen(false);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
finally {
|
|
1116
|
+
setAppInfoSubmitting(false);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
function openBrandGraphicDialog(mode, target) {
|
|
1120
|
+
if (!canEditAppSettings)
|
|
1121
|
+
return;
|
|
1122
|
+
setBrandGraphicDraft({
|
|
1123
|
+
role: target?.role || 'general',
|
|
1124
|
+
label: target?.label || '',
|
|
1125
|
+
dataUrl: '',
|
|
1126
|
+
fileName: '',
|
|
1127
|
+
width: target?.width ?? 0,
|
|
1128
|
+
height: target?.height ?? 0,
|
|
1129
|
+
});
|
|
1130
|
+
setBrandGraphicDialog({ mode, target });
|
|
1131
|
+
}
|
|
1132
|
+
async function selectBrandGraphicFile(file) {
|
|
1133
|
+
if (!file)
|
|
1134
|
+
return;
|
|
1135
|
+
const dataUrl = await fileToDataUrl(file);
|
|
1136
|
+
const size = await imageSize(dataUrl).catch(() => ({ width: 0, height: 0 }));
|
|
1137
|
+
setBrandGraphicDraft((draft) => ({
|
|
1138
|
+
...draft,
|
|
1139
|
+
dataUrl,
|
|
1140
|
+
fileName: file.name,
|
|
1141
|
+
width: size.width,
|
|
1142
|
+
height: size.height,
|
|
1143
|
+
}));
|
|
1144
|
+
}
|
|
1145
|
+
async function submitBrandGraphic(event) {
|
|
1146
|
+
event.preventDefault();
|
|
1147
|
+
if (!canEditAppSettings || !brandGraphicDialog || !brandGraphicDraft.dataUrl || brandGraphicSubmitting)
|
|
1148
|
+
return;
|
|
1149
|
+
setBrandGraphicSubmitting(true);
|
|
1150
|
+
try {
|
|
1151
|
+
const response = await authFetch(`${base}/api/brand-graphics${brandGraphicDialog.mode === 'replace' ? `/${brandGraphicDialog.target?.id}` : ''}`, {
|
|
1152
|
+
method: brandGraphicDialog.mode === 'replace' ? 'PUT' : 'POST',
|
|
1153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1154
|
+
body: JSON.stringify({
|
|
1155
|
+
role: brandGraphicDraft.role,
|
|
1156
|
+
label: brandGraphicDraft.label,
|
|
1157
|
+
dataUrl: brandGraphicDraft.dataUrl,
|
|
1158
|
+
width: brandGraphicDraft.width || null,
|
|
1159
|
+
height: brandGraphicDraft.height || null,
|
|
1160
|
+
source: 'uploaded',
|
|
1161
|
+
}),
|
|
1162
|
+
});
|
|
1163
|
+
if (response.ok) {
|
|
1164
|
+
const document = await response.json();
|
|
1165
|
+
setBrandGraphics(document.graphics);
|
|
1166
|
+
setBrandGraphicDialog(null);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
finally {
|
|
1170
|
+
setBrandGraphicSubmitting(false);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
async function deleteBrandGraphic(graphic) {
|
|
1174
|
+
if (!canEditAppSettings)
|
|
1175
|
+
return;
|
|
1176
|
+
const response = await authFetch(`${base}/api/brand-graphics/${graphic.id}`, { method: 'DELETE' });
|
|
1177
|
+
if (response.ok) {
|
|
1178
|
+
const document = await response.json();
|
|
1179
|
+
setBrandGraphics(document.graphics);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async function createBrandGraphicTask(event) {
|
|
1183
|
+
event.preventDefault();
|
|
1184
|
+
if (!canEditAppSettings)
|
|
1185
|
+
return;
|
|
1186
|
+
const response = await authFetch(`${base}/api/brand-graphics/generate-task`, {
|
|
1187
|
+
method: 'POST',
|
|
1188
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1189
|
+
body: JSON.stringify(generateGraphicDraft),
|
|
1190
|
+
});
|
|
1191
|
+
if (response.ok) {
|
|
1192
|
+
openView('tasks');
|
|
1193
|
+
setGenerateGraphicDialogOpen(false);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
async function copyOutletLink(key, value) {
|
|
1197
|
+
await navigator.clipboard.writeText(value);
|
|
1198
|
+
setCopiedDeviceApp(key);
|
|
1199
|
+
window.setTimeout(() => setCopiedDeviceApp((current) => current === key ? null : current), 1800);
|
|
1200
|
+
}
|
|
1201
|
+
function openView(nextView) {
|
|
1202
|
+
if (nextView !== view) {
|
|
1203
|
+
window.history.pushState(null, '', pathForView(nextView, base, customViews));
|
|
1204
|
+
}
|
|
1205
|
+
setView(nextView);
|
|
1206
|
+
setMobileNavOpen(false);
|
|
1207
|
+
}
|
|
1208
|
+
function openLauncherSimulator(deviceApp) {
|
|
1209
|
+
const nextTarget = deviceApp
|
|
1210
|
+
? {
|
|
1211
|
+
deviceAppHandle: deviceApp.handle,
|
|
1212
|
+
releaseVersion: deviceApp.latestRelease.version,
|
|
1213
|
+
}
|
|
1214
|
+
: launcherSimulatorTarget
|
|
1215
|
+
?? (deviceApps[0] ? { deviceAppHandle: deviceApps[0].handle, releaseVersion: deviceApps[0].latestRelease.version } : null);
|
|
1216
|
+
if (nextTarget) {
|
|
1217
|
+
setLauncherSimulatorTarget(nextTarget);
|
|
1218
|
+
}
|
|
1219
|
+
if (view !== 'launcherSimulator' || nextTarget) {
|
|
1220
|
+
window.history.pushState(null, '', launcherSimulatorPath(base, customViews, nextTarget));
|
|
1221
|
+
}
|
|
1222
|
+
setView('launcherSimulator');
|
|
1223
|
+
setMobileNavOpen(false);
|
|
1224
|
+
}
|
|
1225
|
+
function updateLauncherSimulatorTarget(target) {
|
|
1226
|
+
setLauncherSimulatorTarget(target);
|
|
1227
|
+
if (view === 'launcherSimulator') {
|
|
1228
|
+
window.history.replaceState(null, '', launcherSimulatorPath(base, customViews, target));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
useEffect(() => {
|
|
1232
|
+
const syncViewFromPath = () => {
|
|
1233
|
+
const next = viewFromPath(base, customViews);
|
|
1234
|
+
if (next === 'launcherSimulator') {
|
|
1235
|
+
setLauncherSimulatorTarget(parseLauncherSimulatorTarget(new URLSearchParams(window.location.search)));
|
|
1236
|
+
}
|
|
1237
|
+
setView(next && !disabledViewSet.has(next)
|
|
1238
|
+
? next
|
|
1239
|
+
: firstEnabledView(attachedRepositoryEnabled, customViews, disabledViewSet));
|
|
1240
|
+
setMobileNavOpen(false);
|
|
1241
|
+
};
|
|
1242
|
+
window.addEventListener('popstate', syncViewFromPath);
|
|
1243
|
+
return () => window.removeEventListener('popstate', syncViewFromPath);
|
|
1244
|
+
}, [base, customViews, disabledViewSet, attachedRepositoryEnabled]);
|
|
1245
|
+
const allCustomViews = flattenCustomViews(customViews);
|
|
1246
|
+
const customView = allCustomViews.find((item) => item.id === view && !item.hidden) ?? null;
|
|
1247
|
+
const customViewContext = {
|
|
1248
|
+
base,
|
|
1249
|
+
authFetch,
|
|
1250
|
+
openMenu: () => setMobileNavOpen(true),
|
|
1251
|
+
openView,
|
|
1252
|
+
platformStatus,
|
|
1253
|
+
setPlatformStatus,
|
|
1254
|
+
};
|
|
1255
|
+
const customNavigationItem = (item) => {
|
|
1256
|
+
const childViews = (item.children ?? []).filter((child) => !child.hidden);
|
|
1257
|
+
const childViewIds = flattenCustomViews(childViews).map((child) => child.id);
|
|
1258
|
+
return {
|
|
1259
|
+
id: item.id,
|
|
1260
|
+
label: item.label,
|
|
1261
|
+
icon: item.icon ?? _jsx(ClipboardList, { "aria-hidden": "true" }),
|
|
1262
|
+
active: view === item.id,
|
|
1263
|
+
childActive: childViewIds.includes(view),
|
|
1264
|
+
onSelect: () => openView(item.id),
|
|
1265
|
+
children: childViews.map((child) => customNavigationItem(child)),
|
|
1266
|
+
};
|
|
1267
|
+
};
|
|
1268
|
+
if (!accessReady) {
|
|
1269
|
+
return _jsx(LoadingState, { label: "Loading access" });
|
|
1270
|
+
}
|
|
1271
|
+
if (accessRequired && accessDenied) {
|
|
1272
|
+
return _jsx(HostAccessRedirect, {});
|
|
1273
|
+
}
|
|
1274
|
+
const navigationItems = [
|
|
1275
|
+
{ id: 'tasks', label: 'Agent Tasks', icon: _jsx(BriefcaseBusiness, { "aria-hidden": "true" }), active: view === 'tasks', onSelect: () => openView('tasks') },
|
|
1276
|
+
{ id: 'info', label: 'App Info', icon: _jsx(Info, { "aria-hidden": "true" }), active: view === 'info', onSelect: () => openView('info') },
|
|
1277
|
+
{ id: 'outlets', label: 'Outlets', icon: _jsx(AppWindow, { "aria-hidden": "true" }), active: view === 'outlets', onSelect: () => openView('outlets') },
|
|
1278
|
+
...(deviceApps.length > 0 ? [
|
|
1279
|
+
{ id: 'launcherSimulator', label: 'Exo Launcher Simulator', icon: _jsx(Smartphone, { "aria-hidden": "true" }), active: view === 'launcherSimulator', onSelect: () => openLauncherSimulator() },
|
|
1280
|
+
] : []),
|
|
1281
|
+
...customViews.filter((item) => !item.hidden).map((item) => customNavigationItem(item)),
|
|
1282
|
+
{ id: 'api', label: 'API Access', icon: _jsx(KeyRound, { "aria-hidden": "true" }), active: view === 'api', onSelect: () => openView('api') },
|
|
1283
|
+
{ id: 'controlRepo', label: 'Control Panel Repo', icon: _jsx(GitBranch, { "aria-hidden": "true" }), active: view === 'controlRepo', onSelect: () => openView('controlRepo') },
|
|
1284
|
+
...(attachedRepositoryEnabled ? [
|
|
1285
|
+
{ id: 'repo', label: 'Attached Repositories', icon: _jsx(GitBranch, { "aria-hidden": "true" }), active: view === 'repo', onSelect: () => openView('repo') },
|
|
1286
|
+
{ id: 'deployments', label: 'External Deployments', icon: _jsx(TerminalSquare, { "aria-hidden": "true" }), active: view === 'deployments', onSelect: () => openView('deployments') },
|
|
1287
|
+
] : []),
|
|
1288
|
+
{ id: 'status', label: 'Status', icon: _jsx(Activity, { "aria-hidden": "true" }), active: view === 'status', onSelect: () => openView('status') },
|
|
1289
|
+
].filter((item) => !disabledViewSet.has(item.id));
|
|
1290
|
+
const onboardingBanner = initialMode !== 'none' && initialModeBusy ? (_jsxs("div", { className: "ai-creating-banner", role: "status", children: [initialMode === 'discuss' ? 'The AI is reviewing your idea' : 'The AI is creating your application', initialModeElapsed ? `, ${initialModeElapsed}` : '', "."] })) : null;
|
|
1291
|
+
return (_jsxs(_Fragment, { children: [_jsxs(ControlPanelShell, { brandTitle: displayTitle, brandLogoSrc: appIconHref, mobileNavOpen: mobileNavOpen, onMobileNavOpenChange: setMobileNavOpen, onLogout: onLogout, navigation: (_jsxs(_Fragment, { children: [_jsx(ControlPanelNavigation, { items: navigationItems, onMobileNavOpenChange: setMobileNavOpen }), !hidePlatformLink && (_jsxs("div", { className: "nav-links", "aria-label": "Platform links", children: [_jsx("p", { children: "Platform" }), _jsxs("a", { href: hostControlPanelUrl(), target: "_blank", rel: "noreferrer", children: [_jsx(Server, { "aria-hidden": "true" }), _jsx("span", { children: "Host control panel" }), _jsx(ExternalLink, { className: "trailing-icon", "aria-hidden": "true" })] })] }))] })), taskDetailState: taskWorkspaceDetailState, platformBanner: !platformStatus.available && (_jsx("div", { className: "platform-banner", role: "status", children: platformStatus.message })), children: [_jsx(TaskWorkspace, { dataSource: taskDataSource, platformStatus: platformStatus, onPlatformStatusChange: setPlatformStatus, onOpenMenu: () => setMobileNavOpen(true), onTaskDetailStateChange: setTaskWorkspaceDetailState, listVisible: view === 'tasks', currentUserId: currentUserId, modelOptions: canSelectTaskModel ? taskModelOptions : [], initialTaskId: initialModeTaskId, banner: onboardingBanner }), view === 'outlets' && (_jsxs("section", { children: [_jsx("div", { className: "page-heading", children: _jsx("button", { className: "page-menu-button", type: "button", onClick: () => setMobileNavOpen(true), "aria-label": "Open menu", title: "Open menu", children: _jsx(Menu, { "aria-hidden": "true" }) }) }), _jsx("p", { className: "section-intro", children: "Published web routes and device install targets for this application." }), outletCount === 0 ? (_jsxs("div", { className: "empty-panel", children: [_jsx(AppWindow, { "aria-hidden": "true" }), _jsx("h2", { children: "No outlets yet" }), _jsx("p", { children: "When an agent publishes a web route or prepares a device bundle, it will add an outlet card here." })] })) : (_jsxs("div", { className: "outlet-grid", children: [webLinks.map((webApp) => (_jsxs("article", { className: "outlet-card clickable", children: [_jsx("a", { className: "outlet-card-hit-area", href: webApp.href, target: "_blank", rel: "noreferrer", "aria-label": `Open ${webApp.title}` }), _jsxs("div", { className: "outlet-card-main", children: [_jsxs("div", { className: "outlet-detail", children: [_jsxs("div", { className: "outlet-title", children: [_jsx(AppWindow, { "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("h2", { children: webApp.title }), _jsx("div", { className: "outlet-breadcrumbs", "aria-label": `${webApp.title} typology`, children: (webApp.typology?.length ? webApp.typology : [labelToken(webApp.kind), labelToken(webApp.environment), labelToken(webApp.hosting)]).map((item) => (_jsx("span", { children: item }, item))) }), webApp.description && _jsx("p", { children: webApp.description })] })] }), _jsxs("div", { className: "install-link-row", children: [_jsx("span", { className: "install-link-text", children: webApp.href }), _jsx("button", { className: "icon-button", type: "button", onClick: (event) => {
|
|
1292
|
+
event.stopPropagation();
|
|
1293
|
+
copyOutletLink(webApp.href, webApp.href);
|
|
1294
|
+
}, "aria-label": copiedDeviceApp === webApp.href ? 'Web link copied' : 'Copy web link', title: copiedDeviceApp === webApp.href ? 'Copied' : 'Copy web link', children: _jsx(Copy, { "aria-hidden": "true" }) })] })] }), _jsx("div", { className: "outlet-qr", children: _jsx("img", { src: `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(webApp.href)}`, alt: `QR code for ${webApp.title}`, loading: "lazy" }) })] })] }, webApp.href))), deviceApps.map((deviceApp) => (_jsxs("article", { className: "outlet-card clickable", children: [_jsx("a", { className: "outlet-card-hit-area", href: deviceApp.installUrl, target: "_blank", rel: "noreferrer", "aria-label": `Open ${deviceApp.name}` }), _jsxs("div", { className: "outlet-card-main", children: [_jsxs("div", { className: "outlet-detail", children: [_jsxs("div", { className: "outlet-title", children: [_jsx(Smartphone, { "aria-hidden": "true" }), _jsxs("div", { children: [_jsx("h2", { children: deviceApp.name }), _jsxs("div", { className: "outlet-breadcrumbs", "aria-label": `${deviceApp.name} typology`, children: [_jsx("span", { children: "Device launcher bundle" }), _jsx("span", { children: launcherHostSummary(deviceApp.latestRelease.hostSupport || deviceApp.hostSupport) }), _jsx("span", { children: deviceApp.latestRelease.version })] })] })] }), _jsxs("div", { className: "install-link-row", children: [_jsx("span", { className: "install-link-text", children: deviceApp.installUrl }), _jsx("button", { className: "icon-button", type: "button", onClick: (event) => {
|
|
1295
|
+
event.stopPropagation();
|
|
1296
|
+
copyOutletLink(deviceApp.handle, deviceApp.installUrl);
|
|
1297
|
+
}, "aria-label": copiedDeviceApp === deviceApp.handle ? 'Install link copied' : 'Copy install link', title: copiedDeviceApp === deviceApp.handle ? 'Copied' : 'Copy install link', children: _jsx(Copy, { "aria-hidden": "true" }) })] }), _jsx("div", { className: "outlet-actions", children: _jsxs("button", { className: "secondary-button", type: "button", onClick: (event) => {
|
|
1298
|
+
event.stopPropagation();
|
|
1299
|
+
event.preventDefault();
|
|
1300
|
+
openLauncherSimulator(deviceApp);
|
|
1301
|
+
}, children: [_jsx(Smartphone, { "aria-hidden": "true" }), _jsx("span", { children: "Simulate" })] }) })] }), _jsx("div", { className: "outlet-qr", children: _jsx("img", { src: `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=${encodeURIComponent(deviceApp.installUrl)}`, alt: `QR code for ${deviceApp.name} install link`, loading: "lazy" }) })] })] }, deviceApp.handle)))] }))] })), view === 'launcherSimulator' && (_jsx(ExoLauncherSimulator, { base: base, deviceApps: deviceApps, target: launcherSimulatorTarget, onTargetChange: updateLauncherSimulatorTarget, onOpenMenu: () => setMobileNavOpen(true) })), customView && customView.render(customViewContext), view === 'controlRepo' && (_jsxs("section", { children: [_jsxs("div", { className: "page-heading repo-heading", children: [_jsx("button", { className: "page-menu-button", type: "button", onClick: () => setMobileNavOpen(true), "aria-label": "Open menu", title: "Open menu", children: _jsx(Menu, { "aria-hidden": "true" }) }), _jsxs("button", { className: "secondary-button", type: "button", onClick: loadControlPanelRepoInfo, children: [_jsx(RefreshCw, { "aria-hidden": "true" }), _jsx("span", { children: "Refresh" })] })] }), _jsx("p", { className: "section-intro", children: "Generated control panel source for this app workspace." }), _jsxs("section", { className: "repo-panel", children: [_jsxs("div", { className: "repo-section-title", children: [_jsx("h2", { children: "Control Panel Repo" }), controlPanelRepoInfo?.hasUncommittedChanges && _jsx("span", { className: "status-pill pending", children: "Pending changes" })] }), _jsxs("dl", { className: "repo-meta", children: [_jsxs("div", { children: [_jsx("dt", { children: "Source Folder" }), _jsx("dd", { children: "/workspace" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Git Root" }), _jsx("dd", { children: controlPanelRepoInfo?.repositoryRoot || 'Not a git checkout' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Remote" }), _jsx("dd", { children: controlPanelRepoInfo?.remoteFetchUrl || 'Not configured' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Branch" }), _jsx("dd", { children: controlPanelRepoInfo?.branch || 'Detached or unavailable' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Commit" }), _jsx("dd", { title: controlPanelRepoInfo?.commit || undefined, children: shortCommit(controlPanelRepoInfo?.commit) })] })] }), controlPanelRepoInfo && (_jsxs("div", { className: "repo-control-workflow-grid", children: [_jsxs("label", { children: [_jsx("span", { children: "Target branch" }), _jsx("input", { defaultValue: controlPanelRepoInfo.workflow.workingBranch || 'main', disabled: controlPanelRepoSaving, onBlur: (event) => {
|
|
1302
|
+
const workingBranch = event.currentTarget.value.trim();
|
|
1303
|
+
if (workingBranch && workingBranch !== controlPanelRepoInfo.workflow.workingBranch) {
|
|
1304
|
+
void saveControlPanelRepoWorkflow({ workingBranch });
|
|
1305
|
+
}
|
|
1306
|
+
}, onKeyDown: (event) => {
|
|
1307
|
+
if (event.key === 'Enter')
|
|
1308
|
+
event.currentTarget.blur();
|
|
1309
|
+
} }, controlPanelRepoInfo.workflow.workingBranch || 'main')] }), _jsxs("label", { children: [_jsx("span", { children: "Workflow mode" }), _jsxs("span", { className: "repo-inline-control", children: [_jsx("span", { children: (controlPanelRepoInfo.availableWorkflowModes.length > 0
|
|
1310
|
+
? controlPanelRepoInfo.availableWorkflowModes
|
|
1311
|
+
: repositoryWorkflowModes).find((mode) => mode.value === controlPanelRepoInfo.workflow.mode)?.label || controlPanelRepoInfo.workflow.mode }), _jsxs("details", { className: "repo-inline-menu", open: controlPanelRepoInlineSelection?.field === 'workflow', onToggle: (event) => {
|
|
1312
|
+
if (event.currentTarget.open) {
|
|
1313
|
+
openControlPanelRepoInlineSelection('workflow');
|
|
1314
|
+
}
|
|
1315
|
+
else if (controlPanelRepoInlineSelection?.field === 'workflow') {
|
|
1316
|
+
setControlPanelRepoInlineSelection(null);
|
|
1317
|
+
}
|
|
1318
|
+
}, children: [_jsx("summary", { className: "icon-button", "aria-label": "Change workflow mode", title: "Change workflow mode", "aria-disabled": controlPanelRepoSaving, children: _jsx(ChevronDown, { "aria-hidden": "true" }) }), _jsxs("div", { className: "repo-inline-menu-panel", children: [_jsx("select", { value: controlPanelRepoInlineSelection?.field === 'workflow' ? controlPanelRepoInlineSelection.draft : controlPanelRepoInfo.workflow.mode, disabled: controlPanelRepoSaving, onChange: (event) => updateControlPanelRepoInlineSelection('workflow', event.target.value), children: (controlPanelRepoInfo.availableWorkflowModes.length > 0
|
|
1319
|
+
? controlPanelRepoInfo.availableWorkflowModes
|
|
1320
|
+
: repositoryWorkflowModes).map((mode) => (_jsx("option", { value: mode.value, children: mode.label }, mode.value))) }), _jsx("button", { className: "secondary-button", type: "button", onClick: () => void saveControlPanelRepoInlineSelection('workflow'), disabled: controlPanelRepoSaving, children: controlPanelRepoSaving && controlPanelRepoInlineSelection?.field === 'workflow' ? _jsx(WorkingIndicator, { label: "Saving" }) : _jsx("span", { children: "Confirm" }) })] })] })] })] }), _jsxs("label", { children: [_jsx("span", { children: "Agent completion" }), _jsxs("span", { className: "repo-inline-control", children: [_jsx("span", { children: repositoryTaskCompletionBehaviors.find((item) => item.value === controlPanelRepoInfo.taskCompletionBehavior)?.label || 'Complete Git Workflow if possible' }), _jsxs("details", { className: "repo-inline-menu", open: controlPanelRepoInlineSelection?.field === 'completion', onToggle: (event) => {
|
|
1321
|
+
if (event.currentTarget.open) {
|
|
1322
|
+
openControlPanelRepoInlineSelection('completion');
|
|
1323
|
+
}
|
|
1324
|
+
else if (controlPanelRepoInlineSelection?.field === 'completion') {
|
|
1325
|
+
setControlPanelRepoInlineSelection(null);
|
|
1326
|
+
}
|
|
1327
|
+
}, children: [_jsx("summary", { className: "icon-button", "aria-label": "Change agent completion", title: "Change agent completion", "aria-disabled": controlPanelRepoSaving, children: _jsx(ChevronDown, { "aria-hidden": "true" }) }), _jsxs("div", { className: "repo-inline-menu-panel", children: [_jsx("select", { value: controlPanelRepoInlineSelection?.field === 'completion' ? controlPanelRepoInlineSelection.draft : controlPanelRepoInfo.taskCompletionBehavior || 'complete-if-possible', disabled: controlPanelRepoSaving, onChange: (event) => updateControlPanelRepoInlineSelection('completion', event.target.value), children: repositoryTaskCompletionBehaviors.map((item) => (_jsx("option", { value: item.value, children: item.label }, item.value))) }), _jsx("button", { className: "secondary-button", type: "button", onClick: () => void saveControlPanelRepoInlineSelection('completion'), disabled: controlPanelRepoSaving, children: controlPanelRepoSaving && controlPanelRepoInlineSelection?.field === 'completion' ? _jsx(WorkingIndicator, { label: "Saving" }) : _jsx("span", { children: "Confirm" }) })] })] })] })] }), _jsx("div", { className: "repo-control-workflow-state", "aria-live": "polite", children: controlPanelRepoSaving ? _jsx(WorkingIndicator, { label: "Saving" }) : _jsx(Save, { "aria-hidden": "true" }) })] })), controlPanelRepoError && _jsx("p", { className: "form-error", children: controlPanelRepoError }), controlPanelRepoInfo?.discoveryMessage && (_jsx("p", { className: "repo-warning", children: controlPanelRepoInfo.discoveryMessage })), controlPanelRepoInfo?.hasUncommittedChanges && (_jsxs("details", { className: "repo-dirty", open: true, children: [_jsxs("summary", { children: ["Uncommitted changes (", controlPanelRepoInfo.changeCount, ")"] }), _jsx("ul", { children: controlPanelRepoInfo.changedPaths.map((path) => _jsx("li", { children: path }, path)) })] }))] })] })), attachedRepositoryEnabled && view === 'repo' && (_jsxs("section", { children: [_jsxs("div", { className: "page-heading repo-heading", children: [_jsx("button", { className: "page-menu-button", type: "button", onClick: () => setMobileNavOpen(true), "aria-label": "Open menu", title: "Open menu", children: _jsx(Menu, { "aria-hidden": "true" }) }), _jsx("div", { className: "repo-heading-actions", children: _jsxs("button", { className: "secondary-button", type: "button", onClick: () => setRepoAttachOpen((open) => !open), children: [_jsx(Plus, { "aria-hidden": "true" }), _jsx("span", { children: "Add Repo" })] }) })] }), repoAttachOpen && (_jsxs("section", { className: "repo-panel repo-attach-panel", children: [_jsxs("div", { className: "repo-section-title", children: [_jsx("h2", { children: "Attach Repository" }), _jsx("button", { className: "icon-button", type: "button", onClick: () => setRepoAttachOpen(false), "aria-label": "Close attach repository", children: _jsx(X, { "aria-hidden": "true" }) })] }), _jsxs("div", { className: "repo-form-grid", children: [_jsxs("label", { children: [_jsx("span", { children: "Repository URL" }), _jsx("input", { value: repoAttachUrl, onChange: (event) => setRepoAttachUrl(event.target.value), placeholder: "git@github.com:owner/repo.git" })] }), _jsxs("label", { children: [_jsx("span", { children: "ID" }), _jsx("input", { value: repoAttachId, onChange: (event) => setRepoAttachId(event.target.value), placeholder: "auto" })] }), _jsxs("label", { children: [_jsx("span", { children: "Label" }), _jsx("input", { value: repoAttachLabel, onChange: (event) => setRepoAttachLabel(event.target.value), placeholder: "Repository name" })] }), _jsxs("label", { children: [_jsx("span", { children: "Workflow" }), _jsx("select", { value: repoWorkflowMode, onChange: (event) => setRepoWorkflowMode(event.target.value), children: repositoryWorkflowModes.map((mode) => _jsx("option", { value: mode.value, children: mode.label }, mode.value)) })] }), _jsxs("label", { children: [_jsx("span", { children: "Target branch" }), _jsx("input", { value: repoWorkingBranch, onChange: (event) => setRepoWorkingBranch(event.target.value) })] })] }), _jsxs("div", { className: "repo-form-actions", children: [_jsx("button", { className: "secondary-button", type: "button", onClick: prepareRepositoryAccess, disabled: repoPreparingAccess || !repoAttachUrl.trim(), children: repoPreparingAccess ? _jsx(WorkingIndicator, { label: "Preparing" }) : (_jsxs(_Fragment, { children: [_jsx(Copy, { "aria-hidden": "true" }), _jsx("span", { children: "Prepare Key" })] })) }), _jsx("button", { className: "secondary-button", type: "button", onClick: attachRepository, disabled: repoAttaching || !repoAttachUrl.trim(), children: repoAttaching ? _jsx(WorkingIndicator, { label: "Attaching" }) : (_jsxs(_Fragment, { children: [_jsx(GitBranch, { "aria-hidden": "true" }), _jsx("span", { children: "Attach" })] })) })] }), repoAccessKey && (_jsxs("div", { className: "repo-key-box", children: [_jsx("p", { children: "Add this deploy key with read access before attaching." }), _jsx("pre", { children: repoAccessKey })] })), repoAttachError && _jsx("p", { className: "form-error", children: repoAttachError })] })), !repoInfo?.attached ? (_jsxs("div", { className: "empty-panel", children: [_jsx(GitBranch, { "aria-hidden": "true" }), _jsx("h2", { children: "No repositories attached" }), _jsx("p", { children: "Add a repository to connect source, branch workflow, and sync status." })] })) : (_jsx("div", { className: "repo-view", children: (repoInfo.repositories.length > 0 ? repoInfo.repositories : [{
|
|
1328
|
+
id: 'primary',
|
|
1329
|
+
label: 'Primary',
|
|
1330
|
+
repositoryUrl: repoInfo.repositoryUrl,
|
|
1331
|
+
localPath: repoInfo.localPath || 'repo',
|
|
1332
|
+
remoteFetchUrl: repoInfo.remoteFetchUrl,
|
|
1333
|
+
branch: repoInfo.branch,
|
|
1334
|
+
currentBranchOnRemote: false,
|
|
1335
|
+
commit: repoInfo.commit,
|
|
1336
|
+
shallow: repoInfo.shallow,
|
|
1337
|
+
workflow: { workingBranch: 'main', mode: 'code-merge-request' },
|
|
1338
|
+
taskCompletionBehavior: 'complete-if-possible',
|
|
1339
|
+
branches: [],
|
|
1340
|
+
submodules: repoInfo.submodules,
|
|
1341
|
+
worktree: emptyRepositoryWorktreeStatus(),
|
|
1342
|
+
fetchError: repoInfo.fetchError,
|
|
1343
|
+
}]).map((repository) => {
|
|
1344
|
+
const activeTask = repository.activeTask;
|
|
1345
|
+
const pullRequestLocked = activeTask?.status === 'pr-pending';
|
|
1346
|
+
return (_jsxs("section", { className: "repo-panel", children: [_jsxs("div", { className: "repo-section-title", children: [_jsx("h2", { children: repository.label }), _jsx("span", { className: "status-pill created", children: repositoryWorkflowModes.find((mode) => mode.value === repository.workflow.mode)?.label || repository.workflow.mode })] }), _jsx("div", { className: "repo-section-title", children: _jsx("h3", { children: "Repository Data" }) }), _jsxs("dl", { className: "repo-meta", children: [_jsxs("div", { children: [_jsx("dt", { children: "Remote" }), _jsx("dd", { children: repository.remoteFetchUrl || repository.repositoryUrl || 'Unknown' })] }), canEditRepoWorkflow && (_jsxs("div", { children: [_jsx("dt", { children: "Workflow Mode" }), _jsx("dd", { children: _jsxs("span", { className: "repo-inline-control", children: [_jsx("span", { children: repositoryWorkflowModes.find((mode) => mode.value === repository.workflow.mode)?.label || repository.workflow.mode }), _jsxs("details", { className: "repo-inline-menu", open: repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'workflow', onToggle: (event) => {
|
|
1347
|
+
if (event.currentTarget.open) {
|
|
1348
|
+
openRepoInlineSelection(repository, 'workflow');
|
|
1349
|
+
}
|
|
1350
|
+
else if (repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'workflow') {
|
|
1351
|
+
setRepoInlineSelection(null);
|
|
1352
|
+
}
|
|
1353
|
+
}, children: [_jsx("summary", { className: "icon-button", "aria-label": "Change workflow", title: "Change workflow", "aria-disabled": pullRequestLocked, children: _jsx(ChevronDown, { "aria-hidden": "true" }) }), _jsxs("div", { className: "repo-inline-menu-panel", children: [_jsx("select", { value: repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'workflow' ? repoInlineSelection.draft : repository.workflow.mode, onChange: (event) => updateRepoInlineSelection(repository, 'workflow', event.target.value), disabled: pullRequestLocked || repoSavingWorkflow, children: repositoryWorkflowModes.map((mode) => _jsx("option", { value: mode.value, children: mode.label }, mode.value)) }), _jsx("button", { className: "secondary-button", type: "button", onClick: () => void saveRepoInlineSelection(repository, 'workflow'), disabled: repoSavingWorkflow || pullRequestLocked, children: repoSavingWorkflow && repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'workflow' ? _jsx(WorkingIndicator, { label: "Saving" }) : _jsx("span", { children: "Confirm" }) }), repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'workflow' && repoInlineSelection.error && _jsx("p", { className: "form-error", children: repoInlineSelection.error })] })] })] }) })] }))] }), _jsx("div", { className: "repo-section-title", children: _jsx("h3", { children: "Current User Workspace Data" }) }), _jsxs("dl", { className: "repo-meta", children: [_jsxs("div", { children: [_jsx("dt", { children: "Folder" }), _jsxs("dd", { children: ["/", repository.localPath] })] }), _jsxs("div", { children: [_jsx("dt", { children: "Target Branch" }), _jsx("dd", { children: _jsxs("span", { className: "repo-inline-control", children: [_jsx("span", { children: repository.workflow.workingBranch }), _jsxs("details", { className: "repo-inline-menu", open: repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'branch', onToggle: (event) => {
|
|
1354
|
+
if (event.currentTarget.open) {
|
|
1355
|
+
openRepoInlineSelection(repository, 'branch');
|
|
1356
|
+
}
|
|
1357
|
+
else if (repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'branch') {
|
|
1358
|
+
setRepoInlineSelection(null);
|
|
1359
|
+
}
|
|
1360
|
+
}, children: [_jsx("summary", { className: "icon-button", "aria-label": "Change target branch", title: "Change target branch", "aria-disabled": pullRequestLocked, children: _jsx(ChevronDown, { "aria-hidden": "true" }) }), _jsxs("div", { className: "repo-inline-menu-panel", children: [_jsx("select", { value: repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'branch' ? repoInlineSelection.draft : repository.workflow.workingBranch, onChange: (event) => updateRepoInlineSelection(repository, 'branch', event.target.value), disabled: pullRequestLocked || repoSavingWorkflow, children: repositoryBranchOptions(repository, repository.workflow.workingBranch).map((branch) => (_jsx("option", { value: branch, children: branch }, branch))) }), _jsx("button", { className: "secondary-button", type: "button", onClick: () => void saveRepoInlineSelection(repository, 'branch'), disabled: repoSavingWorkflow || pullRequestLocked, children: repoSavingWorkflow && repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'branch' ? _jsx(WorkingIndicator, { label: "Saving" }) : _jsx("span", { children: "Confirm" }) }), repoInlineSelection?.repositoryId === repository.id && repoInlineSelection.field === 'branch' && repoInlineSelection.error && _jsx("p", { className: "form-error", children: repoInlineSelection.error })] })] })] }) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Working Branch" }), _jsx("dd", { children: repositoryWorkingBranchDisplay(repository) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Commit" }), _jsx("dd", { children: shortCommit(repository.commit) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Checkout" }), _jsx("dd", { children: repository.shallow ? 'Shallow' : repository.shallow === false ? 'Full' : 'Unknown' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Agent Completion" }), _jsx("dd", { children: _jsxs("span", { className: "repo-inline-control", children: [_jsx("span", { children: repositoryTaskCompletionBehaviors.find((item) => item.value === repository.taskCompletionBehavior)?.label || 'Complete Git Workflow if possible' }), _jsxs("details", { className: "repo-inline-menu", children: [_jsx("summary", { className: "icon-button", "aria-label": "Change agent completion", title: "Change agent completion", "aria-disabled": pullRequestLocked, children: _jsx(ChevronDown, { "aria-hidden": "true" }) }), _jsxs("div", { className: "repo-inline-menu-panel", children: [_jsx("select", { value: repoWorkspaceTaskCompletionDraft(repository), onChange: (event) => updateRepoWorkspaceTaskCompletionDraft(repository, event.target.value), disabled: pullRequestLocked, children: repositoryTaskCompletionBehaviors.map((item) => (_jsx("option", { value: item.value, children: item.label }, item.value))) }), _jsx("button", { className: "secondary-button", type: "button", onClick: () => saveRepoWorkspaceTaskCompletion(repository), disabled: repoSavingWorkspaceId !== null || pullRequestLocked, children: repoSavingWorkspaceId === repository.id ? _jsx(WorkingIndicator, { label: "Saving" }) : _jsx("span", { children: "Confirm" }) })] })] })] }) })] })] }), repository.branch && repository.workflow.workingBranch && repository.branch !== repository.workflow.workingBranch && (_jsxs("p", { className: "repo-warning", children: ["Working branch is ", repository.branch, "; target branch is ", repository.workflow.workingBranch, "."] })), repository.worktree.hasUncommittedChanges && (_jsxs("details", { className: "repo-dirty", open: true, children: [_jsxs("summary", { children: ["Uncommitted changes (", repository.worktree.changeCount, ")"] }), _jsx("ul", { children: repository.worktree.changedPaths.map((path) => _jsx("li", { children: path }, path)) }), repository.worktree.changeCount > repository.worktree.changedPaths.length && (_jsxs("p", { children: [repository.worktree.changeCount - repository.worktree.changedPaths.length, " more changes not shown."] }))] })), activeTask && (_jsxs("div", { className: "repo-active-task", children: [_jsx("span", { className: pullRequestLocked ? 'status-pill pending' : 'status-pill created', children: pullRequestLocked ? 'PR pending' : activeTask.status }), activeTask.branch && _jsxs("span", { children: ["Branch: ", activeTask.branch] }), activeTask.pullRequest?.url && (_jsxs("a", { href: activeTask.pullRequest.url, target: "_blank", rel: "noreferrer", children: ["Pull request", activeTask.pullRequest.number ? ` #${activeTask.pullRequest.number}` : ''] }))] })), _jsxs("div", { className: "repo-action-row", children: [_jsx("button", { className: "secondary-button", type: "button", onClick: () => syncRepoNow(repository), disabled: repoFetching, children: repoFetching ? _jsx(WorkingIndicator, { label: "Syncing" }) : (_jsxs(_Fragment, { children: [_jsx(RefreshCw, { "aria-hidden": "true" }), _jsx("span", { children: "Sync" })] })) }), activeTask && (_jsx(_Fragment, { children: activeTask.pullRequest?.url && (_jsxs("a", { className: "secondary-button", href: activeTask.pullRequest.url, target: "_blank", rel: "noreferrer", children: [_jsx(GitBranch, { "aria-hidden": "true" }), _jsx("span", { children: "Open PR" })] })) })), repository.branch && !repository.currentBranchOnRemote && (_jsxs("button", { className: "secondary-button danger", type: "button", onClick: () => openRepoDeleteLocalBranchDialog(repository), disabled: repository.worktree.hasUncommittedChanges || pullRequestLocked, title: pullRequestLocked ? 'Pull request is pending.' : repository.worktree.hasUncommittedChanges ? 'Commit or discard local changes before deleting the working branch.' : 'Delete the working branch after switching back to the target branch.', children: [_jsx(Trash2, { "aria-hidden": "true" }), _jsx("span", { children: "Delete Local Branch" })] })), repository.worktree.hasUncommittedChanges && (_jsxs("button", { className: "secondary-button danger", type: "button", onClick: () => openRepoResetDialog(repository), disabled: pullRequestLocked, children: [_jsx(Trash2, { "aria-hidden": "true" }), _jsx("span", { children: "Reset Branch" })] })), _jsxs("button", { className: "secondary-button danger", type: "button", onClick: () => openRepoDeleteDialog(repository), disabled: repository.worktree.hasUncommittedChanges || pullRequestLocked, title: pullRequestLocked ? 'Pull request is pending.' : repository.worktree.hasUncommittedChanges ? 'Commit or discard local changes before deleting this repository.' : 'Delete this attached repository checkout.', children: [_jsx(Trash2, { "aria-hidden": "true" }), _jsx("span", { children: "Delete Repo" })] })] }), repository.submodules.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "repo-section-title", children: [_jsx("h3", { children: "Submodules" }), repository.submodules.some((item) => item.status !== 'checked-out') && (_jsx("span", { className: "status-pill missing", children: "Access issue" }))] }), _jsx("div", { className: "repo-submodule-list", children: repository.submodules.map((submodule) => (_jsxs("article", { className: "repo-submodule", children: [_jsxs("div", { children: [_jsx("h3", { children: submodule.path }), _jsx("p", { children: submodule.url || 'No URL declared' }), _jsxs("div", { className: "repo-submodule-meta", children: [submodule.branch && _jsx("span", { children: submodule.branch }), submodule.commit && _jsx("span", { children: shortCommit(submodule.commit) })] })] }), _jsx("span", { className: submodule.status === 'checked-out' ? 'status-pill created' : 'status-pill missing', children: submodule.status === 'checked-out' ? 'checked out' : submodule.status })] }, `${repository.id}-${submodule.path}`))) })] })), repository.fetchError && (_jsxs("details", { className: "repo-error", open: true, children: [_jsx("summary", { children: "Sync error" }), _jsx("pre", { children: repository.fetchError })] }))] }, repository.id));
|
|
1361
|
+
}) }))] })), attachedRepositoryEnabled && view === 'deployments' && (_jsxs("section", { children: [_jsxs("div", { className: "page-heading repo-heading", children: [_jsx("button", { className: "page-menu-button", type: "button", onClick: () => setMobileNavOpen(true), "aria-label": "Open menu", title: "Open menu", children: _jsx(Menu, { "aria-hidden": "true" }) }), _jsxs("button", { className: "secondary-button", type: "button", onClick: loadExternalDeployments, disabled: externalDeploymentsLoading, children: [externalDeploymentsLoading ? _jsx(InlineSpinner, {}) : _jsx(RefreshCw, { "aria-hidden": "true" }), _jsx("span", { children: externalDeploymentsLoading ? 'Loading' : 'Refresh' })] })] }), _jsx("p", { className: "section-intro", children: "External production and test deployments connected to this attached repository." }), externalDeploymentsLoading && !externalDeployments ? (_jsx(LoadingState, { label: "Loading deployments" })) : !(externalDeployments?.deployments?.length) ? (_jsxs("div", { className: "empty-panel", children: [_jsx(TerminalSquare, { "aria-hidden": "true" }), _jsx("h2", { children: "No external deployments yet" }), _jsx("p", { children: "When production or staging access is added, deployment metadata and live checks will appear here." })] })) : (_jsx("div", { className: "repo-view", children: (externalDeployments?.deployments ?? []).map((deployment, index) => (_jsxs("section", { className: "repo-panel", children: [_jsxs("div", { className: "repo-section-title", children: [_jsx("h2", { children: deployment.name || deployment.id || `Deployment ${index + 1}` }), _jsx("span", { className: "status-pill created", children: labelToken(deployment.role || deployment.environment || 'external') })] }), _jsxs("dl", { className: "repo-meta", children: [_jsxs("div", { children: [_jsx("dt", { children: "Environment" }), _jsx("dd", { children: labelToken(deployment.environment || deployment.role || 'unknown') })] }), _jsxs("div", { children: [_jsx("dt", { children: "Access" }), _jsx("dd", { children: labelToken(deployment.accessMode || 'unknown') })] }), _jsxs("div", { children: [_jsx("dt", { children: "URL" }), _jsx("dd", { children: deployment.publicUrl ? _jsx("a", { href: deployment.publicUrl, target: "_blank", rel: "noreferrer", children: deployment.publicUrl }) : 'Unknown' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Host" }), _jsx("dd", { children: deployment.user && deployment.host ? `${deployment.user}@${deployment.host}` : deployment.host || 'Unknown' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Folder" }), _jsx("dd", { children: deployment.workingDirectory || 'Unknown' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Service" }), _jsx("dd", { children: deployment.serviceName || 'Unknown' })] }), _jsxs("div", { children: [_jsx("dt", { children: "Health" }), _jsx("dd", { children: deployment.healthUrl ? _jsx("a", { href: deployment.healthUrl, target: "_blank", rel: "noreferrer", children: deployment.healthUrl }) : 'Unknown' })] })] }), externalDeploymentNotes(deployment.notes).length ? (_jsx("ul", { className: "deployment-notes", children: externalDeploymentNotes(deployment.notes).map((note) => _jsx("li", { children: note }, note)) })) : null] }, deployment.id || deployment.name || index))) }))] })), view === 'api' && (_jsxs("section", { children: [_jsxs("div", { className: "page-heading repo-heading", children: [_jsx("button", { className: "page-menu-button", type: "button", onClick: () => setMobileNavOpen(true), "aria-label": "Open menu", title: "Open menu", children: _jsx(Menu, { "aria-hidden": "true" }) }), _jsxs("div", { className: "button-row", children: [_jsxs("button", { className: "secondary-button", type: "button", onClick: loadApiClients, disabled: apiClientsLoading, children: [apiClientsLoading ? _jsx(InlineSpinner, {}) : _jsx(RefreshCw, { "aria-hidden": "true" }), _jsx("span", { children: apiClientsLoading ? 'Refreshing' : 'Refresh' })] }), _jsxs("button", { className: "primary", type: "button", onClick: openApiClientDialog, children: [_jsx(Plus, { "aria-hidden": "true" }), _jsx("span", { children: "New Client" })] })] })] }), apiClientsError && _jsx("div", { className: "platform-banner", children: apiClientsError }), apiClients.length === 0 && !apiClientsLoading ? (_jsxs("div", { className: "empty-panel", children: [_jsx(KeyRound, { "aria-hidden": "true" }), _jsx("h2", { children: "No API clients" }), _jsx("p", { children: "External callers can start and monitor agent tasks after a client token is created." })] })) : (_jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Name" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Access" }), _jsx("th", { children: "Operations" }), _jsx("th", { children: "Token" }), _jsx("th", { children: "Last used" }), _jsx("th", { children: "Actions" })] }) }), _jsx("tbody", { children: apiClients.map((client) => (_jsxs("tr", { children: [_jsx("td", { children: client.name }), _jsx("td", { children: _jsx("span", { className: client.status === 'active' ? 'status-pill created' : 'status-pill missing', children: client.status }) }), _jsx("td", { children: client.accessLevels.join(', ') }), _jsx("td", { children: client.operations.map(formatApiOperation).join(', ') }), _jsx("td", { children: _jsxs("code", { children: [client.tokenPrefix, "..."] }) }), _jsx("td", { children: client.lastUsedAt ? formatDateTime(client.lastUsedAt) : 'Never' }), _jsx("td", { children: _jsxs("button", { className: "secondary-button", type: "button", onClick: () => revokeApiClient(client), disabled: client.status !== 'active', children: [_jsx(Trash2, { "aria-hidden": "true" }), _jsx("span", { children: "Revoke" })] }) })] }, client.id))) })] }))] })), view === 'status' && (_jsxs("section", { className: "page-view status-view", children: [_jsxs("div", { className: "page-toolbar", children: [_jsx("button", { className: "page-menu-button", type: "button", onClick: () => setMobileNavOpen(true), "aria-label": "Open menu", title: "Open menu", children: _jsx(Menu, { "aria-hidden": "true" }) }), _jsx("button", { className: "icon-button", type: "button", onClick: () => void loadStatus(), disabled: statusLoading, "aria-label": statusLoading ? 'Refreshing status' : 'Refresh status', title: statusLoading ? 'Refreshing' : 'Refresh', children: statusLoading ? _jsx(InlineSpinner, {}) : _jsx(RefreshCw, { "aria-hidden": "true" }) }), _jsx("h1", { className: "page-title", children: "Status" })] }), _jsxs("div", { className: "metric-grid", children: [_jsxs("div", { className: "metric", children: [_jsx(Server, { "aria-hidden": "true" }), _jsx("span", { children: "Total space used" }), _jsx("strong", { children: formatBytes(status?.totalSpaceUsedBytes ?? 0) })] }), _jsxs("div", { className: "metric", children: [_jsx(CheckCircle2, { "aria-hidden": "true" }), _jsx("span", { children: "Containers created" }), _jsx("strong", { children: status?.containersCreated ?? 0 })] }), (status?.updateCounts ?? []).map((item) => (_jsxs("div", { className: "metric", children: [_jsx(Activity, { "aria-hidden": "true" }), _jsx("span", { children: item.label }), _jsx("strong", { children: item.count ?? 'n/a' }), item.error && _jsx("small", { children: item.error })] }, item.label)))] }), _jsxs("table", { children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: "Container" }), _jsx("th", { children: "Status" }), _jsx("th", { children: "Ports" })] }) }), _jsx("tbody", { children: (status?.containers ?? []).map((container) => (_jsxs("tr", { children: [_jsx("td", { children: container.name }), _jsx("td", { children: _jsxs("span", { className: container.available ? 'status-pill created' : 'status-pill missing', children: [container.available ? _jsx(CheckCircle2, { "aria-hidden": "true" }) : _jsx(CircleOff, { "aria-hidden": "true" }), _jsx("span", { children: container.available ? 'created' : 'not created' })] }) }), _jsx("td", { children: formatContainerPorts(container.ports) })] }, container.name))) })] })] })), view === 'info' && (_jsxs("section", { children: [_jsx("div", { className: "page-heading", children: _jsx("button", { className: "page-menu-button", type: "button", onClick: () => setMobileNavOpen(true), "aria-label": "Open menu", title: "Open menu", children: _jsx(Menu, { "aria-hidden": "true" }) }) }), _jsxs("div", { className: "info-panel", children: [_jsxs("dl", { children: [_jsxs("div", { children: [_jsx("dt", { children: "Handle" }), _jsx("dd", { children: appInfo?.handle || applicationHandle })] }), _jsxs("div", { children: [_jsx("dt", { children: "Title" }), _jsxs("dd", { className: "editable-info-line", children: [_jsx("span", { children: appInfo?.title || applicationTitle }), _jsx("button", { className: "icon-button", type: "button", onClick: () => openAppInfoDialog('title'), disabled: !canEditAppSettings, "aria-label": "Edit title", title: canEditAppSettings ? 'Edit title' : 'DevOps permission required', children: _jsx(Pencil, { "aria-hidden": "true" }) })] })] }), _jsxs("div", { children: [_jsx("dt", { children: "Description" }), _jsxs("dd", { className: "editable-info-line", children: [_jsx("span", { children: appInfo?.description || 'No description' }), _jsx("button", { className: "icon-button", type: "button", onClick: () => openAppInfoDialog('description'), disabled: !canEditAppSettings, "aria-label": "Edit description", title: canEditAppSettings ? 'Edit description' : 'DevOps permission required', children: _jsx(Pencil, { "aria-hidden": "true" }) })] })] }), _jsxs("div", { children: [_jsx("dt", { children: "Outlets" }), _jsxs("dd", { className: "editable-info-line", children: [_jsx("span", { children: outletSummary(appInfo?.outlets || defaultOutletSettings) }), _jsx("button", { className: "icon-button", type: "button", onClick: () => openAppInfoDialog('outlets'), disabled: !canEditAppSettings, "aria-label": "Edit outlets", title: canEditAppSettings ? 'Edit outlets' : 'DevOps permission required', children: _jsx(Pencil, { "aria-hidden": "true" }) })] })] }), _jsxs("div", { children: [_jsx("dt", { children: "Telegram bot tokens" }), _jsx("dd", { children: telegramTokenSummary(appInfo?.outlets || defaultOutletSettings, appInfo?.telegramBot) })] })] }), _jsxs("div", { className: "brand-graphics-heading", children: [_jsx("h2", { children: "Brand graphics" }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => openBrandGraphicDialog('add'), disabled: !canEditAppSettings, title: canEditAppSettings ? 'Add brand graphic' : 'DevOps permission required', "aria-label": "Add brand graphic", children: [_jsx(Plus, { "aria-hidden": "true" }), _jsx("span", { children: "Add" })] }), _jsxs("button", { type: "button", onClick: () => setGenerateGraphicDialogOpen(true), disabled: !canEditAppSettings, title: canEditAppSettings ? 'Generate brand graphic' : 'DevOps permission required', "aria-label": "Generate brand graphic", children: [_jsx(WandSparkles, { "aria-hidden": "true" }), _jsx("span", { children: "Generate" })] })] })] }), _jsx("div", { className: "brand-graphics-grid", children: brandGraphics.length === 0 ? (_jsxs("div", { className: "empty-brand-graphics", children: [_jsx(Image, { "aria-hidden": "true" }), _jsx("span", { children: "No brand graphics" })] })) : brandGraphics.map((graphic) => (_jsxs("article", { className: "brand-graphic-card", children: [_jsx("img", { src: `${base}/${graphic.fileName}`, alt: graphic.label }), _jsxs("div", { children: [_jsx("strong", { children: graphic.label }), _jsx("span", { children: graphic.role }), _jsxs("span", { children: [graphic.width && graphic.height ? `${graphic.width} x ${graphic.height}` : 'Resolution not set', " \u00B7 ", graphic.source] })] }), _jsxs("div", { className: "brand-graphic-actions", children: [_jsx("button", { type: "button", onClick: () => openBrandGraphicDialog('replace', graphic), disabled: !canEditAppSettings, title: canEditAppSettings ? 'Replace brand graphic' : 'DevOps permission required', "aria-label": `Replace ${graphic.label}`, children: _jsx(Upload, { "aria-hidden": "true" }) }), _jsx("button", { type: "button", onClick: () => deleteBrandGraphic(graphic), disabled: !canEditAppSettings, title: canEditAppSettings ? 'Delete brand graphic' : 'DevOps permission required', "aria-label": `Delete ${graphic.label}`, children: _jsx(Trash2, { "aria-hidden": "true" }) })] })] }, graphic.id))) })] })] }))] }), appInfoDialogOpen && (_jsx("div", { className: "modal-layer", role: "presentation", onMouseDown: () => setAppInfoDialogOpen(false), children: _jsxs("div", { className: "modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "app-info-title", onMouseDown: (event) => event.stopPropagation(), children: [_jsxs("header", { children: [_jsxs("h2", { id: "app-info-title", children: ["Edit ", appInfoEditTarget] }), _jsx("button", { className: "icon-button", onClick: () => setAppInfoDialogOpen(false), "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] }), _jsxs("form", { className: "app-info-form", onSubmit: submitAppInfo, children: [appInfoEditTarget === 'title' && _jsxs("label", { children: [_jsx("span", { children: "Title" }), _jsx("input", { autoFocus: true, value: appInfoDraft.title, onChange: (event) => setAppInfoDraft((draft) => ({ ...draft, title: event.target.value })) })] }), appInfoEditTarget === 'description' && _jsxs("label", { children: [_jsx("span", { children: "Description" }), _jsx("textarea", { value: appInfoDraft.description, onChange: (event) => setAppInfoDraft((draft) => ({ ...draft, description: event.target.value })) })] }), appInfoEditTarget === 'outlets' && _jsxs("div", { className: "outlet-settings-form", children: [_jsxs("label", { className: "checkbox-row", children: [_jsx("input", { type: "checkbox", checked: appInfoDraft.outlets.web, onChange: (event) => setAppInfoDraft((draft) => ({ ...draft, outlets: { ...draft.outlets, web: event.target.checked } })) }), _jsx("span", { children: "Web" })] }), _jsxs("label", { className: "checkbox-row", children: [_jsx("input", { type: "checkbox", checked: appInfoDraft.outlets.deviceLauncherBundles.length > 0, onChange: (event) => setAppInfoDraft((draft) => ({
|
|
1362
|
+
...draft,
|
|
1363
|
+
outlets: {
|
|
1364
|
+
...draft.outlets,
|
|
1365
|
+
deviceLauncherBundles: event.target.checked
|
|
1366
|
+
? [{ hosts: draft.outlets.deviceLauncherBundles[0]?.hosts?.length ? draft.outlets.deviceLauncherBundles[0].hosts : [{ form: 'mobile', os: 'android' }] }]
|
|
1367
|
+
: [],
|
|
1368
|
+
},
|
|
1369
|
+
})) }), _jsx("span", { children: "Device launcher bundle" })] }), appInfoDraft.outlets.deviceLauncherBundles.length > 0 && _jsxs("div", { className: "nested-outlet-settings", children: [_jsxs("label", { children: [_jsx("span", { children: "Form" }), _jsxs("select", { value: appInfoDraft.outlets.deviceLauncherBundles[0]?.hosts?.[0]?.form || 'universal', onChange: (event) => setAppInfoDraft((draft) => ({
|
|
1370
|
+
...draft,
|
|
1371
|
+
outlets: {
|
|
1372
|
+
...draft.outlets,
|
|
1373
|
+
deviceLauncherBundles: [{ hosts: [{ form: event.target.value, os: draft.outlets.deviceLauncherBundles[0]?.hosts?.[0]?.os || 'universal' }] }],
|
|
1374
|
+
},
|
|
1375
|
+
})), children: [_jsx("option", { value: "universal", children: "Universal" }), _jsx("option", { value: "mobile", children: "Mobile" }), _jsx("option", { value: "tablet", children: "Tablet" }), _jsx("option", { value: "pc", children: "PC" })] })] }), _jsxs("label", { children: [_jsx("span", { children: "OS" }), _jsxs("select", { value: appInfoDraft.outlets.deviceLauncherBundles[0]?.hosts?.[0]?.os || 'universal', onChange: (event) => setAppInfoDraft((draft) => ({
|
|
1376
|
+
...draft,
|
|
1377
|
+
outlets: {
|
|
1378
|
+
...draft.outlets,
|
|
1379
|
+
deviceLauncherBundles: [{ hosts: [{ form: draft.outlets.deviceLauncherBundles[0]?.hosts?.[0]?.form || 'universal', os: event.target.value }] }],
|
|
1380
|
+
},
|
|
1381
|
+
})), children: [_jsx("option", { value: "universal", children: "Universal" }), _jsx("option", { value: "android", children: "Android" }), _jsx("option", { value: "ios", children: "iOS" }), _jsx("option", { value: "mac", children: "macOS" }), _jsx("option", { value: "windows", children: "Windows" })] })] })] }), _jsxs("label", { className: "checkbox-row", children: [_jsx("input", { type: "checkbox", checked: appInfoDraft.outlets.androidApp, onChange: (event) => setAppInfoDraft((draft) => ({ ...draft, outlets: { ...draft.outlets, androidApp: event.target.checked } })) }), _jsx("span", { children: "Android app" })] }), _jsxs("label", { className: "checkbox-row", children: [_jsx("input", { type: "checkbox", checked: appInfoDraft.outlets.iosApp, onChange: (event) => setAppInfoDraft((draft) => ({ ...draft, outlets: { ...draft.outlets, iosApp: event.target.checked } })) }), _jsx("span", { children: "iOS app" })] }), _jsxs("label", { className: "checkbox-row", children: [_jsx("input", { type: "checkbox", checked: appInfoDraft.outlets.telegram, onChange: (event) => setAppInfoDraft((draft) => ({
|
|
1382
|
+
...draft,
|
|
1383
|
+
outlets: { ...draft.outlets, telegram: event.target.checked },
|
|
1384
|
+
telegramBot: event.target.checked ? draft.telegramBot : { liveToken: '', testToken: '' },
|
|
1385
|
+
})) }), _jsx("span", { children: "Telegram bot" })] }), _jsxs("label", { children: [_jsx("span", { children: "Telegram bot token for real use" }), _jsx("input", { type: "password", value: appInfoDraft.telegramBot.liveToken || '', disabled: !appInfoDraft.outlets.telegram, onChange: (event) => setAppInfoDraft((draft) => ({ ...draft, telegramBot: { ...draft.telegramBot, liveToken: event.target.value } })) })] }), _jsxs("label", { children: [_jsx("span", { children: "Telegram bot token for testing" }), _jsx("input", { type: "password", value: appInfoDraft.telegramBot.testToken || '', disabled: !appInfoDraft.outlets.telegram, onChange: (event) => setAppInfoDraft((draft) => ({ ...draft, telegramBot: { ...draft.telegramBot, testToken: event.target.value } })) })] })] }), _jsxs("div", { className: "modal-actions", children: [_jsx("button", { type: "button", onClick: () => setAppInfoDialogOpen(false), children: "Cancel" }), !platformStatus.available && _jsx("p", { className: "platform-modal-note", children: "The platform is down. Wait until it is back up." }), _jsx("button", { className: "primary", disabled: !canEditAppSettings || !platformStatus.available || appInfoSubmitting, children: appInfoSubmitting ? _jsx(WorkingIndicator, { label: "Saving" }) : (_jsxs(_Fragment, { children: [_jsx(Save, { "aria-hidden": "true" }), _jsx("span", { children: "Save" })] })) })] })] })] }) })), brandGraphicDialog && (_jsx("div", { className: "modal-layer", role: "presentation", onMouseDown: () => setBrandGraphicDialog(null), children: _jsxs("div", { className: "modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "brand-graphic-title", onMouseDown: (event) => event.stopPropagation(), children: [_jsxs("header", { children: [_jsx("h2", { id: "brand-graphic-title", children: brandGraphicDialog.mode === 'replace' ? 'Replace brand graphic' : 'Add brand graphic' }), _jsx("button", { className: "icon-button", onClick: () => setBrandGraphicDialog(null), "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] }), _jsxs("form", { className: "app-info-form", onSubmit: submitBrandGraphic, children: [_jsxs("label", { children: [_jsx("span", { children: "Role" }), _jsx("select", { value: brandGraphicDraft.role, onChange: (event) => setBrandGraphicDraft((draft) => ({ ...draft, role: event.target.value })), children: brandGraphicRoles.map((role) => _jsx("option", { value: role.value, children: role.label }, role.value)) })] }), _jsxs("label", { children: [_jsx("span", { children: "Label" }), _jsx("input", { value: brandGraphicDraft.label, onChange: (event) => setBrandGraphicDraft((draft) => ({ ...draft, label: event.target.value })), placeholder: "Default from role" })] }), _jsxs("label", { className: "file-drop", children: [_jsx(Upload, { "aria-hidden": "true" }), _jsx("span", { children: brandGraphicDraft.fileName || 'Choose image' }), _jsx("input", { type: "file", accept: "image/*", onChange: (event) => selectBrandGraphicFile(event.target.files?.[0] ?? null) })] }), brandGraphicDraft.dataUrl && _jsx("img", { className: "brand-graphic-preview", src: brandGraphicDraft.dataUrl, alt: "" }), _jsxs("div", { className: "modal-actions", children: [_jsx("button", { type: "button", onClick: () => setBrandGraphicDialog(null), children: "Cancel" }), _jsx("button", { className: "primary", disabled: !canEditAppSettings || !brandGraphicDraft.dataUrl || brandGraphicSubmitting, children: brandGraphicSubmitting ? _jsx(WorkingIndicator, { label: "Saving" }) : (_jsxs(_Fragment, { children: [_jsx(Save, { "aria-hidden": "true" }), _jsx("span", { children: "Save" })] })) })] })] })] }) })), generateGraphicDialogOpen && (_jsx("div", { className: "modal-layer", role: "presentation", onMouseDown: () => setGenerateGraphicDialogOpen(false), children: _jsxs("div", { className: "modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "generate-graphic-title", onMouseDown: (event) => event.stopPropagation(), children: [_jsxs("header", { children: [_jsx("h2", { id: "generate-graphic-title", children: "Generate brand graphic" }), _jsx("button", { className: "icon-button", onClick: () => setGenerateGraphicDialogOpen(false), "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] }), _jsxs("form", { className: "app-info-form", onSubmit: createBrandGraphicTask, children: [_jsxs("label", { children: [_jsx("span", { children: "Role" }), _jsx("select", { value: generateGraphicDraft.role, onChange: (event) => setGenerateGraphicDraft((draft) => ({ ...draft, role: event.target.value })), children: brandGraphicRoles.map((role) => _jsx("option", { value: role.value, children: role.label }, role.value)) })] }), _jsxs("label", { children: [_jsx("span", { children: "Resolution" }), _jsx("input", { value: generateGraphicDraft.size, onChange: (event) => setGenerateGraphicDraft((draft) => ({ ...draft, size: event.target.value })) })] }), _jsxs("label", { children: [_jsx("span", { children: "Prompt" }), _jsx("textarea", { value: generateGraphicDraft.prompt, onChange: (event) => setGenerateGraphicDraft((draft) => ({ ...draft, prompt: event.target.value })) })] }), _jsxs("div", { className: "modal-actions", children: [_jsx("button", { type: "button", onClick: () => setGenerateGraphicDialogOpen(false), children: "Cancel" }), _jsxs("button", { className: "primary", disabled: !canEditAppSettings, children: [_jsx(WandSparkles, { "aria-hidden": "true" }), _jsx("span", { children: "Start task" })] })] })] })] }) })), repoWorkflowDialog && (_jsx("div", { className: "modal-layer", role: "presentation", onMouseDown: () => !repoSavingWorkflow && setRepoWorkflowDialog(null), children: _jsxs("div", { className: "modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "repo-workflow-title", onMouseDown: (event) => event.stopPropagation(), children: [_jsxs("header", { children: [_jsx("h2", { id: "repo-workflow-title", children: repoWorkflowDialog.field === 'workflow' ? 'Change Workflow' : 'Change Target Branch' }), _jsx("button", { className: "icon-button", onClick: () => setRepoWorkflowDialog(null), disabled: repoSavingWorkflow, "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] }), _jsxs("form", { className: "app-info-form", onSubmit: saveRepoWorkflow, children: [_jsxs("label", { children: [_jsx("span", { children: "Repository" }), _jsx("input", { value: repoWorkflowDialog.repository.label, readOnly: true })] }), repoWorkflowDialog.field === 'branch' && (_jsxs("label", { children: [_jsx("span", { children: "Target branch" }), _jsx("select", { autoFocus: true, value: repoWorkflowDialog.draft.workingBranch, disabled: repoSavingWorkflow, onChange: (event) => updateRepoWorkflowDialog({ workingBranch: event.target.value }), children: repositoryBranchOptions(repoWorkflowDialog.repository, repoWorkflowDialog.draft.workingBranch).map((branch) => (_jsx("option", { value: branch, children: branch }, branch))) })] })), repoWorkflowDialog.field === 'workflow' && canEditRepoWorkflow && (_jsxs("label", { children: [_jsx("span", { children: "Workflow mode" }), _jsx("select", { autoFocus: true, value: repoWorkflowDialog.draft.mode, disabled: repoSavingWorkflow, onChange: (event) => updateRepoWorkflowDialog({ mode: event.target.value }), children: repositoryWorkflowModes.map((mode) => _jsx("option", { value: mode.value, children: mode.label }, mode.value)) })] })), repoWorkflowDialog.error && _jsx("p", { className: "form-error", children: repoWorkflowDialog.error }), _jsxs("div", { className: "modal-actions", children: [_jsx("button", { type: "button", onClick: () => setRepoWorkflowDialog(null), disabled: repoSavingWorkflow, children: "Cancel" }), _jsx("button", { className: "primary", disabled: repoSavingWorkflow || !repoWorkflowDialog.draft.workingBranch.trim(), children: repoSavingWorkflow ? _jsx(WorkingIndicator, { label: "Checking out branch" }) : (_jsxs(_Fragment, { children: [_jsx(Save, { "aria-hidden": "true" }), _jsx("span", { children: "Confirm" })] })) })] })] })] }) })), repoResetDialog && (_jsx("div", { className: "modal-layer", role: "presentation", onMouseDown: () => !repoResetting && setRepoResetDialog(null), children: _jsxs("div", { className: "modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "repo-reset-title", onMouseDown: (event) => event.stopPropagation(), children: [_jsxs("header", { children: [_jsx("h2", { id: "repo-reset-title", children: "Reset Repo Branch" }), _jsx("button", { className: "icon-button", onClick: () => setRepoResetDialog(null), disabled: repoResetting, "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] }), _jsxs("form", { className: "app-info-form", onSubmit: resetRepoBranch, children: [_jsxs("p", { className: "danger-note", children: ["This will discard uncommitted changes in ", repoResetDialog.repository.label, ", reset it to origin/", repoResetDialog.repository.workflow.workingBranch, ", and checkout the target branch."] }), repoResetDialog.repository.worktree.hasUncommittedChanges && (_jsxs("div", { className: "repo-reset-summary", children: [_jsxs("strong", { children: [repoResetDialog.repository.worktree.changeCount, " uncommitted changes will be discarded."] }), _jsx("ul", { children: repoResetDialog.repository.worktree.changedPaths.slice(0, 8).map((path) => _jsx("li", { children: path }, path)) })] })), repoResetDialog.error && _jsx("p", { className: "form-error", children: repoResetDialog.error }), _jsxs("div", { className: "modal-actions", children: [_jsx("button", { type: "button", onClick: () => setRepoResetDialog(null), disabled: repoResetting, children: "Cancel" }), _jsx("button", { className: "danger", disabled: repoResetting, children: repoResetting ? _jsx(WorkingIndicator, { label: "Resetting branch" }) : (_jsxs(_Fragment, { children: [_jsx(Trash2, { "aria-hidden": "true" }), _jsx("span", { children: "Reset Branch" })] })) })] })] })] }) })), repoDeleteDialog && (_jsx("div", { className: "modal-layer", role: "presentation", onMouseDown: () => !repoDeleting && setRepoDeleteDialog(null), children: _jsxs("div", { className: "modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "repo-delete-title", onMouseDown: (event) => event.stopPropagation(), children: [_jsxs("header", { children: [_jsx("h2", { id: "repo-delete-title", children: "Delete Attached Repo" }), _jsx("button", { className: "icon-button", onClick: () => setRepoDeleteDialog(null), disabled: repoDeleting, "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] }), _jsxs("form", { className: "app-info-form", onSubmit: deleteRepository, children: [_jsxs("p", { className: "danger-note", children: ["This will remove ", repoDeleteDialog.repository.label, " from this control panel and delete the local checkout at /", repoDeleteDialog.repository.localPath, "."] }), _jsx("p", { className: "muted", children: "The remote Git repository is not deleted. Attach it again later if you need a fresh checkout." }), repoDeleteDialog.error && _jsx("p", { className: "form-error", children: repoDeleteDialog.error }), _jsxs("div", { className: "modal-actions", children: [_jsx("button", { type: "button", onClick: () => setRepoDeleteDialog(null), disabled: repoDeleting, children: "Cancel" }), _jsx("button", { className: "danger", disabled: repoDeleting, children: repoDeleting ? _jsx(WorkingIndicator, { label: "Deleting repo" }) : (_jsxs(_Fragment, { children: [_jsx(Trash2, { "aria-hidden": "true" }), _jsx("span", { children: "Delete Repo" })] })) })] })] })] }) })), repoDeleteLocalBranchDialog && (_jsx("div", { className: "modal-layer", role: "presentation", onMouseDown: () => !repoDeletingLocalBranch && setRepoDeleteLocalBranchDialog(null), children: _jsxs("div", { className: "modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "repo-delete-local-branch-title", onMouseDown: (event) => event.stopPropagation(), children: [_jsxs("header", { children: [_jsx("h2", { id: "repo-delete-local-branch-title", children: "Delete Local Branch" }), _jsx("button", { className: "icon-button", onClick: () => setRepoDeleteLocalBranchDialog(null), disabled: repoDeletingLocalBranch, "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] }), _jsxs("form", { className: "app-info-form", onSubmit: deleteRepoLocalBranch, children: [_jsxs("p", { className: "danger-note", children: ["This will switch ", repoDeleteLocalBranchDialog.repository.label, " to target branch origin/", repoDeleteLocalBranchDialog.repository.workflow.workingBranch, " and delete the working branch ", repoDeleteLocalBranchDialog.repository.branch, "."] }), _jsx("p", { className: "muted", children: "This action is only allowed when the working branch is no longer present on origin." }), repoDeleteLocalBranchDialog.error && _jsx("p", { className: "form-error", children: repoDeleteLocalBranchDialog.error }), _jsxs("div", { className: "modal-actions", children: [_jsx("button", { type: "button", onClick: () => setRepoDeleteLocalBranchDialog(null), disabled: repoDeletingLocalBranch, children: "Cancel" }), _jsx("button", { className: "danger", disabled: repoDeletingLocalBranch, children: repoDeletingLocalBranch ? _jsx(WorkingIndicator, { label: "Deleting branch" }) : (_jsxs(_Fragment, { children: [_jsx(Trash2, { "aria-hidden": "true" }), _jsx("span", { children: "Delete Branch" })] })) })] })] })] }) })), apiClientDialogOpen && (_jsx("div", { className: "modal-layer", role: "presentation", onMouseDown: () => !apiClientSubmitting && setApiClientDialogOpen(false), children: _jsxs("div", { className: "modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "api-client-title", onMouseDown: (event) => event.stopPropagation(), children: [_jsxs("header", { children: [_jsx("h2", { id: "api-client-title", children: "New API Client" }), _jsx("button", { className: "icon-button", onClick: () => setApiClientDialogOpen(false), disabled: apiClientSubmitting, "aria-label": "Close", title: "Close", children: _jsx(X, { "aria-hidden": "true" }) })] }), createdApiToken ? (_jsxs("div", { className: "app-info-form api-token-panel", children: [_jsxs("label", { children: [_jsx("span", { children: "Token" }), _jsx("textarea", { readOnly: true, value: createdApiToken })] }), _jsx("div", { className: "modal-actions", children: _jsxs("button", { type: "button", className: "primary", onClick: () => setApiClientDialogOpen(false), children: [_jsx(CheckCircle2, { "aria-hidden": "true" }), _jsx("span", { children: "Done" })] }) })] })) : (_jsxs("form", { className: "app-info-form api-client-form", onSubmit: submitApiClient, children: [_jsxs("label", { children: [_jsx("span", { children: "Name" }), _jsx("input", { autoFocus: true, value: apiClientDraft.name, onChange: (event) => setApiClientDraft((draft) => ({ ...draft, name: event.target.value })) })] }), _jsxs("fieldset", { className: "checkbox-group", children: [_jsx("legend", { children: "Access levels" }), ['query', 'code', 'devops'].map((level) => (_jsxs("label", { className: "checkbox-row", children: [_jsx("input", { type: "checkbox", checked: apiClientDraft.accessLevels.includes(level), onChange: () => toggleApiClientDraftValue('accessLevels', level) }), _jsx("span", { children: level })] }, level)))] }), _jsxs("fieldset", { className: "checkbox-group", children: [_jsx("legend", { children: "Operations" }), [
|
|
1386
|
+
['start_task', 'Start task'],
|
|
1387
|
+
['read_task', 'Read progress'],
|
|
1388
|
+
['terminate_own_task', 'Terminate own task'],
|
|
1389
|
+
].map(([operation, label]) => (_jsxs("label", { className: "checkbox-row", children: [_jsx("input", { type: "checkbox", checked: apiClientDraft.operations.includes(operation), onChange: () => toggleApiClientDraftValue('operations', operation) }), _jsx("span", { children: label })] }, operation)))] }), _jsxs("label", { children: [_jsx("span", { children: "Callback allowlist" }), _jsx("textarea", { value: apiClientDraft.callbackAllowlist, onChange: (event) => setApiClientDraft((draft) => ({ ...draft, callbackAllowlist: event.target.value })), placeholder: "api.example.com" })] }), _jsxs("div", { className: "modal-actions", children: [_jsx("button", { type: "button", onClick: () => setApiClientDialogOpen(false), disabled: apiClientSubmitting, children: "Cancel" }), _jsx("button", { className: "primary", disabled: !apiClientDraft.name.trim() || apiClientSubmitting, children: apiClientSubmitting ? _jsx(WorkingIndicator, { label: "Creating" }) : (_jsxs(_Fragment, { children: [_jsx(KeyRound, { "aria-hidden": "true" }), _jsx("span", { children: "Create" })] })) })] })] }))] }) }))] }));
|
|
1390
|
+
}
|
|
1391
|
+
function HostAccessRedirect() {
|
|
1392
|
+
useEffect(() => {
|
|
1393
|
+
window.location.replace(`/?returnTo=${encodeURIComponent(window.location.href)}`);
|
|
1394
|
+
}, []);
|
|
1395
|
+
return _jsx(LoadingState, { label: "Opening host sign-in" });
|
|
1396
|
+
}
|
|
1397
|
+
function consumeInitialModeQuery() {
|
|
1398
|
+
const url = new URL(window.location.href);
|
|
1399
|
+
if (!url.searchParams.has('mode') && !url.searchParams.has('start-mode')) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
url.searchParams.delete('mode');
|
|
1403
|
+
url.searchParams.delete('start-mode');
|
|
1404
|
+
window.history.replaceState(null, '', `${url.pathname}${url.search}${url.hash}`);
|
|
1405
|
+
}
|
|
1406
|
+
function controlHomeUrl() {
|
|
1407
|
+
const url = new URL(window.location.href);
|
|
1408
|
+
url.search = '';
|
|
1409
|
+
url.hash = '';
|
|
1410
|
+
if (!url.pathname.endsWith('/')) {
|
|
1411
|
+
url.pathname = `${url.pathname}/`;
|
|
1412
|
+
}
|
|
1413
|
+
return url.toString();
|
|
1414
|
+
}
|
|
1415
|
+
function controlViewUrl(view) {
|
|
1416
|
+
const url = new URL(controlHomeUrl());
|
|
1417
|
+
url.searchParams.set('view', view);
|
|
1418
|
+
return url.toString();
|
|
1419
|
+
}
|
|
1420
|
+
function hostControlPanelUrl() {
|
|
1421
|
+
return new URL('/', window.location.origin).toString();
|
|
1422
|
+
}
|
|
1423
|
+
function outletSummary(outlets) {
|
|
1424
|
+
const selected = [
|
|
1425
|
+
outlets.web ? 'Web' : null,
|
|
1426
|
+
outlets.deviceLauncherBundles.length > 0 ? 'Launcher bundle' : null,
|
|
1427
|
+
outlets.androidApp ? 'Android app' : null,
|
|
1428
|
+
outlets.iosApp ? 'iOS app' : null,
|
|
1429
|
+
outlets.telegram ? 'Telegram bot' : null,
|
|
1430
|
+
].filter((item) => Boolean(item));
|
|
1431
|
+
return selected.length > 0 ? selected.join(', ') : 'No outlets selected';
|
|
1432
|
+
}
|
|
1433
|
+
function launcherHostSummary(hosts) {
|
|
1434
|
+
if (!hosts.length)
|
|
1435
|
+
return 'Universal';
|
|
1436
|
+
return hosts.map((host) => `${hostLabel(host.form)} / ${hostLabel(host.os)}`).join(', ');
|
|
1437
|
+
}
|
|
1438
|
+
function hostLabel(value) {
|
|
1439
|
+
switch (value) {
|
|
1440
|
+
case 'ios':
|
|
1441
|
+
return 'iOS';
|
|
1442
|
+
case 'mac':
|
|
1443
|
+
return 'macOS';
|
|
1444
|
+
case 'pc':
|
|
1445
|
+
return 'PC';
|
|
1446
|
+
default:
|
|
1447
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
function labelToken(value) {
|
|
1451
|
+
const normalized = (value || '').trim();
|
|
1452
|
+
if (!normalized)
|
|
1453
|
+
return 'Unknown';
|
|
1454
|
+
return normalized
|
|
1455
|
+
.split('-')
|
|
1456
|
+
.filter(Boolean)
|
|
1457
|
+
.map((word) => `${word.charAt(0).toLocaleUpperCase()}${word.slice(1)}`)
|
|
1458
|
+
.join(' ');
|
|
1459
|
+
}
|
|
1460
|
+
function externalDeploymentNotes(value) {
|
|
1461
|
+
if (Array.isArray(value))
|
|
1462
|
+
return value.filter((note) => note.trim());
|
|
1463
|
+
const note = value?.trim();
|
|
1464
|
+
return note ? [note] : [];
|
|
1465
|
+
}
|
|
1466
|
+
function telegramTokenSummary(outlets, telegramBot) {
|
|
1467
|
+
if (!outlets.telegram)
|
|
1468
|
+
return 'Telegram bot outlet is off';
|
|
1469
|
+
const live = telegramBot?.liveToken ? 'real-use token set' : 'real-use token empty';
|
|
1470
|
+
const test = telegramBot?.testToken ? 'test token set' : 'test token empty';
|
|
1471
|
+
return `${live}, ${test}`;
|
|
1472
|
+
}
|
|
1473
|
+
function initialCreationPrompt(config) {
|
|
1474
|
+
const { applicationTitle, applicationDescription, applicationHandle, applicationOutlets, } = config;
|
|
1475
|
+
return [
|
|
1476
|
+
'Create the following application',
|
|
1477
|
+
'',
|
|
1478
|
+
`Title: ${applicationTitle}`,
|
|
1479
|
+
'',
|
|
1480
|
+
'Details:',
|
|
1481
|
+
applicationDescription || 'No additional details provided.',
|
|
1482
|
+
'',
|
|
1483
|
+
'Deployment:',
|
|
1484
|
+
'Figure out whether this application needs deployable web services, mobile outlets, or both from the application metadata at:',
|
|
1485
|
+
'',
|
|
1486
|
+
'/workspace/app-meta.json',
|
|
1487
|
+
'',
|
|
1488
|
+
'Use that file as the source of truth for enabled outlets before deciding what to build, package, deploy, or verify.',
|
|
1489
|
+
'',
|
|
1490
|
+
`Unique name: ${applicationHandle}`,
|
|
1491
|
+
`Outlets: ${applicationOutlets || 'none selected'}`,
|
|
1492
|
+
'',
|
|
1493
|
+
'Required outlet and URL contract:',
|
|
1494
|
+
'- If a web outlet is selected, create the real-use web route at the empty app route with `platformctl container create app-web --path \'\'`; it opens at `/apps/' + applicationHandle + '/`.',
|
|
1495
|
+
'- If you create a test or preview web route, use `platformctl container create test-web --path test`; it opens at `/apps/' + applicationHandle + '/test/`.',
|
|
1496
|
+
'- Do not create the real-use web app at `/apps/' + applicationHandle + '/app/` unless the owner explicitly asks for a separate `/app` route.',
|
|
1497
|
+
'- Add every owner-visible web route to `/workspace/control-links.json` before reporting completion. Use entries such as `{"title":"Real-use app","path":""}` and `{"title":"Test app","path":"test"}`.',
|
|
1498
|
+
'- In the final summary, report app-relative routes unless an absolute URL came from verified outlet metadata in the current browser-facing environment. Do not report unverified `127.0.0.1` host ports.',
|
|
1499
|
+
].filter((section) => section.length > 0).join('\n');
|
|
1500
|
+
}
|
|
1501
|
+
function customizeImportedRepoPrompt(info, config) {
|
|
1502
|
+
const { applicationTitle, applicationDescription } = config;
|
|
1503
|
+
const title = (info.title || applicationTitle).trim();
|
|
1504
|
+
const description = (info.description || applicationDescription).trim();
|
|
1505
|
+
return [
|
|
1506
|
+
`Customize ${title}`,
|
|
1507
|
+
'',
|
|
1508
|
+
'This application was provisioned from a GitHub repository. Inspect the imported repository source, preserve useful existing code, and customize it for the stated application purpose.',
|
|
1509
|
+
'The imported source is expected under /workspace/source-repo when history was preserved, /workspace/source-snapshot when a snapshot was requested, or /workspace/repo when an external repository was attached. Check /workspace/state/github-source.json for the exact source metadata.',
|
|
1510
|
+
description ? ['', 'Application purpose:', description].join('\n') : '',
|
|
1511
|
+
'',
|
|
1512
|
+
'Use the app control panel and outlet metadata already present in this workspace. Do not replace the control panel. Build, deploy, and verify the result before reporting.',
|
|
1513
|
+
].filter((section) => section.length > 0).join('\n');
|
|
1514
|
+
}
|
|
1515
|
+
function discussIdeaPrompt(info, config) {
|
|
1516
|
+
const { applicationTitle, applicationDescription } = config;
|
|
1517
|
+
const title = (info.title || applicationTitle).trim();
|
|
1518
|
+
const description = (info.description || applicationDescription).trim();
|
|
1519
|
+
return [
|
|
1520
|
+
`Here is my ${title} idea`,
|
|
1521
|
+
description,
|
|
1522
|
+
'What do you think?',
|
|
1523
|
+
].filter((section) => section.length > 0).join('\n');
|
|
1524
|
+
}
|
|
1525
|
+
function parseInitialMode(value) {
|
|
1526
|
+
return value === 'create-app' || value === 'discuss' || value === 'customize' ? value : 'none';
|
|
1527
|
+
}
|
|
1528
|
+
function viewFromPath(base, customViews) {
|
|
1529
|
+
const routeBase = base || '';
|
|
1530
|
+
const pathname = window.location.pathname;
|
|
1531
|
+
const relativePath = routeBase && (pathname === routeBase || pathname.startsWith(`${routeBase}/`))
|
|
1532
|
+
? pathname.slice(routeBase.length)
|
|
1533
|
+
: pathname;
|
|
1534
|
+
const slug = relativePath.replace(/^\/+|\/+$/g, '');
|
|
1535
|
+
const entry = Object.entries(viewSlugs).find(([, viewSlug]) => viewSlug === slug);
|
|
1536
|
+
if (entry)
|
|
1537
|
+
return entry[0];
|
|
1538
|
+
return flattenCustomViews(customViews).find((customView) => (customView.slug ?? customView.id) === slug)?.id ?? null;
|
|
1539
|
+
}
|
|
1540
|
+
function pathForView(view, base, customViews) {
|
|
1541
|
+
const customView = flattenCustomViews(customViews).find((item) => item.id === view);
|
|
1542
|
+
const slug = customView?.slug ?? viewSlugs[view] ?? view;
|
|
1543
|
+
return `${base}/${slug}`.replace(/\/{2,}/g, '/');
|
|
1544
|
+
}
|
|
1545
|
+
function parseLauncherSimulatorTarget(params) {
|
|
1546
|
+
const deviceAppHandle = params.get('deviceApp')?.trim();
|
|
1547
|
+
const releaseVersion = params.get('release')?.trim();
|
|
1548
|
+
return deviceAppHandle && releaseVersion ? { deviceAppHandle, releaseVersion } : null;
|
|
1549
|
+
}
|
|
1550
|
+
function launcherSimulatorPath(base, customViews, target) {
|
|
1551
|
+
const path = pathForView('launcherSimulator', base, customViews);
|
|
1552
|
+
if (!target)
|
|
1553
|
+
return path;
|
|
1554
|
+
const params = new URLSearchParams({
|
|
1555
|
+
deviceApp: target.deviceAppHandle,
|
|
1556
|
+
release: target.releaseVersion,
|
|
1557
|
+
});
|
|
1558
|
+
return `${path}?${params.toString()}`;
|
|
1559
|
+
}
|
|
1560
|
+
function flattenCustomViews(customViews) {
|
|
1561
|
+
return customViews.flatMap((customView) => [
|
|
1562
|
+
customView,
|
|
1563
|
+
...flattenCustomViews(customView.children ?? []),
|
|
1564
|
+
]);
|
|
1565
|
+
}
|
|
1566
|
+
function delay(milliseconds) {
|
|
1567
|
+
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
|
|
1568
|
+
}
|
|
1569
|
+
function formatElapsedMs(elapsedMs) {
|
|
1570
|
+
const seconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
1571
|
+
const hours = Math.floor(seconds / 3600);
|
|
1572
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
1573
|
+
const remainingSeconds = seconds % 60;
|
|
1574
|
+
if (hours > 0)
|
|
1575
|
+
return `${hours}h ${minutes}m ${remainingSeconds}s`;
|
|
1576
|
+
if (minutes > 0)
|
|
1577
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
1578
|
+
return `${remainingSeconds}s`;
|
|
1579
|
+
}
|
|
1580
|
+
function formatApiOperation(value) {
|
|
1581
|
+
switch (value) {
|
|
1582
|
+
case 'start_task':
|
|
1583
|
+
return 'Start task';
|
|
1584
|
+
case 'read_task':
|
|
1585
|
+
return 'Read progress';
|
|
1586
|
+
case 'terminate_own_task':
|
|
1587
|
+
return 'Terminate own task';
|
|
1588
|
+
default:
|
|
1589
|
+
return labelToken(value);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
function parseErrorMessage(value) {
|
|
1593
|
+
try {
|
|
1594
|
+
const payload = JSON.parse(value);
|
|
1595
|
+
return typeof payload.error === 'string' ? payload.error : payload.error?.message;
|
|
1596
|
+
}
|
|
1597
|
+
catch {
|
|
1598
|
+
return undefined;
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
function shortCommit(commit) {
|
|
1602
|
+
return commit ? commit.slice(0, 12) : 'Unknown';
|
|
1603
|
+
}
|
|
1604
|
+
function repositoryWorkingBranchDisplay(repository) {
|
|
1605
|
+
const branch = repository.branch?.trim();
|
|
1606
|
+
const targetBranch = repository.workflow.workingBranch?.trim();
|
|
1607
|
+
return branch && branch !== targetBranch ? branch : '';
|
|
1608
|
+
}
|
|
1609
|
+
function fileToDataUrl(file) {
|
|
1610
|
+
return new Promise((resolve, reject) => {
|
|
1611
|
+
const reader = new FileReader();
|
|
1612
|
+
reader.onload = () => resolve(String(reader.result));
|
|
1613
|
+
reader.onerror = () => reject(reader.error ?? new Error('Unable to read file.'));
|
|
1614
|
+
reader.readAsDataURL(file);
|
|
1615
|
+
});
|
|
1616
|
+
}
|
|
1617
|
+
function imageSize(dataUrl) {
|
|
1618
|
+
return new Promise((resolve, reject) => {
|
|
1619
|
+
const image = new window.Image();
|
|
1620
|
+
image.onload = () => resolve({ width: image.naturalWidth, height: image.naturalHeight });
|
|
1621
|
+
image.onerror = () => reject(new Error('Unable to inspect image.'));
|
|
1622
|
+
image.src = dataUrl;
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
//# sourceMappingURL=AppControlPanel.js.map
|