@nextclaw/ui 0.9.4 → 0.9.5
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/CHANGELOG.md +6 -0
- package/dist/assets/{ChannelsList-DDfZIiJa.js → ChannelsList-Byfj2R01.js} +1 -1
- package/dist/assets/{ChatPage-FpRraTxm.js → ChatPage-DM1ewbWf.js} +1 -1
- package/dist/assets/{DocBrowser-Kndx8OJj.js → DocBrowser-BLv77lJ0.js} +1 -1
- package/dist/assets/{LogoBadge-hKHoLH9n.js → LogoBadge-D7j1al-w.js} +1 -1
- package/dist/assets/{MarketplacePage-CZIJyfjK.js → MarketplacePage-DuskLKYh.js} +1 -1
- package/dist/assets/{McpMarketplacePage-BGrAMA37.js → McpMarketplacePage-DpMjaD3m.js} +1 -1
- package/dist/assets/{ModelConfig-BpKQeGfb.js → ModelConfig-ubaecweS.js} +1 -1
- package/dist/assets/{ProvidersList-qfUL6mrW.js → ProvidersList-w8MJH2LI.js} +1 -1
- package/dist/assets/RemoteAccessPage-D79_5Kbn.js +1 -0
- package/dist/assets/{RuntimeConfig-CVlqNWKO.js → RuntimeConfig-BbX4yFKy.js} +1 -1
- package/dist/assets/{SearchConfig-DXFV6Mvx.js → SearchConfig-BmmmeyJd.js} +1 -1
- package/dist/assets/{SecretsConfig-BGW9aUqv.js → SecretsConfig-CWG8J01H.js} +1 -1
- package/dist/assets/{SessionsConfig-BByfa1ke.js → SessionsConfig-D-vg_Lgv.js} +1 -1
- package/dist/assets/{chat-message-ZwnDwDuQ.js → chat-message-CGXiVhyN.js} +1 -1
- package/dist/assets/{index-BWvap_iq.js → index-CeRbsQ90.js} +2 -2
- package/dist/assets/{label-Bklr3fXc.js → label-CCSffS1D.js} +1 -1
- package/dist/assets/{page-layout-sNhcbwtm.js → page-layout-ud8wZ8gX.js} +1 -1
- package/dist/assets/{popover-C3rJrJJG.js → popover-Bfoe6YBX.js} +1 -1
- package/dist/assets/{security-config-BueosYw1.js → security-config-DJJUCMov.js} +1 -1
- package/dist/assets/{skeleton-CiG6msbm.js → skeleton-IOOTmHzP.js} +1 -1
- package/dist/assets/{status-dot-CsIV5YrS.js → status-dot-Fz9-eKsl.js} +1 -1
- package/dist/assets/{switch-DSdHSIsC.js → switch-B-_SrMSL.js} +1 -1
- package/dist/assets/{tabs-custom-BB-VjdL2.js → tabs-custom-6Tm1ZHfS.js} +1 -1
- package/dist/assets/{useConfirmDialog-BL5s8KDC.js → useConfirmDialog-BeOW2bOI.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +4 -4
- package/src/components/remote/RemoteAccessPage.tsx +334 -241
- package/dist/assets/RemoteAccessPage-BQuMsngI.js +0 -1
|
@@ -18,6 +18,7 @@ import { StatusDot } from '@/components/ui/status-dot';
|
|
|
18
18
|
import { Switch } from '@/components/ui/switch';
|
|
19
19
|
import { formatDateTime, t } from '@/lib/i18n';
|
|
20
20
|
import { Activity, KeyRound, Laptop, RefreshCcw, ServerCog, ShieldCheck, SquareTerminal } from 'lucide-react';
|
|
21
|
+
import type { RemoteAccessView, RemoteDoctorView } from '@/api/remote.types';
|
|
21
22
|
|
|
22
23
|
function getRuntimeStatus(runtime: RemoteRuntimeView | null): { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string } {
|
|
23
24
|
if (!runtime) {
|
|
@@ -57,37 +58,23 @@ function KeyValueRow(props: { label: string; value?: string | number | null; mut
|
|
|
57
58
|
);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const browserAuthPollMutation = useRemoteBrowserAuthPoll();
|
|
64
|
-
const logoutMutation = useRemoteLogout();
|
|
65
|
-
const settingsMutation = useRemoteSettings();
|
|
66
|
-
const doctorMutation = useRemoteDoctor();
|
|
67
|
-
const serviceMutation = useRemoteServiceControl();
|
|
68
|
-
|
|
69
|
-
const status = remoteStatus.data;
|
|
70
|
-
const runtimeStatus = useMemo(() => getRuntimeStatus(status?.runtime ?? null), [status?.runtime]);
|
|
71
|
-
const serviceStatus = useMemo(() => getServiceStatus(status?.service ?? { running: false, currentProcess: false }), [status?.service]);
|
|
61
|
+
function resolvePlatformApiBase(platformApiBase: string, status: RemoteAccessView | undefined) {
|
|
62
|
+
return platformApiBase.trim() || status?.settings.platformApiBase || status?.account.apiBase || undefined;
|
|
63
|
+
}
|
|
72
64
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
65
|
+
function useRemoteBrowserAuthFlow(props: {
|
|
66
|
+
status: RemoteAccessView | undefined;
|
|
67
|
+
platformApiBase: string;
|
|
68
|
+
startBrowserAuth: ReturnType<typeof useRemoteBrowserAuthStart>;
|
|
69
|
+
pollBrowserAuth: ReturnType<typeof useRemoteBrowserAuthPoll>;
|
|
70
|
+
}) {
|
|
71
|
+
const { status, platformApiBase, startBrowserAuth, pollBrowserAuth } = props;
|
|
76
72
|
const [authSessionId, setAuthSessionId] = useState<string | null>(null);
|
|
77
73
|
const [authVerificationUri, setAuthVerificationUri] = useState<string | null>(null);
|
|
78
74
|
const [authStatusMessage, setAuthStatusMessage] = useState('');
|
|
79
75
|
const [authExpiresAt, setAuthExpiresAt] = useState<string | null>(null);
|
|
80
76
|
const [authPollIntervalMs, setAuthPollIntervalMs] = useState(1500);
|
|
81
77
|
|
|
82
|
-
useEffect(() => {
|
|
83
|
-
if (!status) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
setEnabled(status.settings.enabled);
|
|
87
|
-
setDeviceName(status.settings.deviceName);
|
|
88
|
-
setPlatformApiBase(status.settings.platformApiBase);
|
|
89
|
-
}, [status]);
|
|
90
|
-
|
|
91
78
|
useEffect(() => {
|
|
92
79
|
if (!status?.account.loggedIn) {
|
|
93
80
|
return;
|
|
@@ -107,9 +94,9 @@ export function RemoteAccessPage() {
|
|
|
107
94
|
let cancelled = false;
|
|
108
95
|
const timerId = window.setTimeout(async () => {
|
|
109
96
|
try {
|
|
110
|
-
const result = await
|
|
97
|
+
const result = await pollBrowserAuth.mutateAsync({
|
|
111
98
|
sessionId: authSessionId,
|
|
112
|
-
apiBase:
|
|
99
|
+
apiBase: resolvePlatformApiBase(platformApiBase, status)
|
|
113
100
|
});
|
|
114
101
|
if (cancelled) {
|
|
115
102
|
return;
|
|
@@ -141,19 +128,12 @@ export function RemoteAccessPage() {
|
|
|
141
128
|
cancelled = true;
|
|
142
129
|
window.clearTimeout(timerId);
|
|
143
130
|
};
|
|
144
|
-
}, [
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
status?.account.loggedIn,
|
|
151
|
-
status?.settings.platformApiBase
|
|
152
|
-
]);
|
|
153
|
-
|
|
154
|
-
const handleStartBrowserAuth = async () => {
|
|
155
|
-
const apiBase = platformApiBase.trim() || status?.settings.platformApiBase || status?.account.apiBase || undefined;
|
|
156
|
-
const result = await browserAuthStartMutation.mutateAsync({ apiBase });
|
|
131
|
+
}, [authPollIntervalMs, authSessionId, platformApiBase, pollBrowserAuth, status]);
|
|
132
|
+
|
|
133
|
+
const start = async () => {
|
|
134
|
+
const result = await startBrowserAuth.mutateAsync({
|
|
135
|
+
apiBase: resolvePlatformApiBase(platformApiBase, status)
|
|
136
|
+
});
|
|
157
137
|
setAuthSessionId(result.sessionId);
|
|
158
138
|
setAuthVerificationUri(result.verificationUri);
|
|
159
139
|
setAuthExpiresAt(result.expiresAt);
|
|
@@ -165,232 +145,345 @@ export function RemoteAccessPage() {
|
|
|
165
145
|
}
|
|
166
146
|
};
|
|
167
147
|
|
|
168
|
-
const
|
|
148
|
+
const resume = () => {
|
|
169
149
|
if (!authVerificationUri) {
|
|
170
150
|
return;
|
|
171
151
|
}
|
|
172
152
|
window.open(authVerificationUri, '_blank', 'noopener,noreferrer');
|
|
173
153
|
};
|
|
174
154
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
155
|
+
return {
|
|
156
|
+
authExpiresAt,
|
|
157
|
+
authSessionId,
|
|
158
|
+
authStatusMessage,
|
|
159
|
+
authVerificationUri,
|
|
160
|
+
resume,
|
|
161
|
+
start
|
|
162
|
+
};
|
|
163
|
+
}
|
|
178
164
|
|
|
165
|
+
function RemoteOverviewCard(props: {
|
|
166
|
+
status: RemoteAccessView | undefined;
|
|
167
|
+
runtimeStatus: { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string };
|
|
168
|
+
serviceStatus: { status: 'active' | 'inactive' | 'ready' | 'setup' | 'warning'; label: string };
|
|
169
|
+
}) {
|
|
179
170
|
return (
|
|
180
|
-
<
|
|
181
|
-
<
|
|
171
|
+
<Card>
|
|
172
|
+
<CardHeader>
|
|
173
|
+
<CardTitle className="flex items-center gap-2">
|
|
174
|
+
<Activity className="h-4 w-4 text-primary" />
|
|
175
|
+
{t('remoteOverviewTitle')}
|
|
176
|
+
</CardTitle>
|
|
177
|
+
<CardDescription>{t('remoteOverviewDescription')}</CardDescription>
|
|
178
|
+
</CardHeader>
|
|
179
|
+
<CardContent className="space-y-5">
|
|
180
|
+
<div className="flex flex-wrap gap-2">
|
|
181
|
+
<StatusDot status={props.status?.account.loggedIn ? 'ready' : 'inactive'} label={props.status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
|
|
182
|
+
<StatusDot status={props.serviceStatus.status} label={props.serviceStatus.label} />
|
|
183
|
+
<StatusDot status={props.runtimeStatus.status} label={props.runtimeStatus.label} />
|
|
184
|
+
</div>
|
|
182
185
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
186
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
187
|
+
<KeyValueRow label={t('remoteLocalOrigin')} value={props.status?.localOrigin} />
|
|
188
|
+
<KeyValueRow label={t('remotePublicPlatform')} value={props.status?.platformBase ?? props.status?.account.platformBase} />
|
|
189
|
+
<KeyValueRow label={t('remoteDeviceId')} value={props.status?.runtime?.deviceId} muted />
|
|
190
|
+
<KeyValueRow label={t('remoteLastConnectedAt')} value={props.status?.runtime?.lastConnectedAt ? formatDateTime(props.status.runtime.lastConnectedAt) : '-'} muted />
|
|
191
|
+
<KeyValueRow label={t('remoteLastError')} value={props.status?.runtime?.lastError} muted />
|
|
192
|
+
</div>
|
|
193
|
+
</CardContent>
|
|
194
|
+
</Card>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function RemoteDeviceCard(props: {
|
|
199
|
+
enabled: boolean;
|
|
200
|
+
setEnabled: (value: boolean) => void;
|
|
201
|
+
deviceName: string;
|
|
202
|
+
setDeviceName: (value: string) => void;
|
|
203
|
+
platformApiBase: string;
|
|
204
|
+
setPlatformApiBase: (value: string) => void;
|
|
205
|
+
settingsMutation: ReturnType<typeof useRemoteSettings>;
|
|
206
|
+
serviceMutation: ReturnType<typeof useRemoteServiceControl>;
|
|
207
|
+
}) {
|
|
208
|
+
return (
|
|
209
|
+
<Card>
|
|
210
|
+
<CardHeader>
|
|
211
|
+
<CardTitle className="flex items-center gap-2">
|
|
212
|
+
<Laptop className="h-4 w-4 text-primary" />
|
|
213
|
+
{t('remoteDeviceTitle')}
|
|
214
|
+
</CardTitle>
|
|
215
|
+
<CardDescription>{t('remoteDeviceDescription')}</CardDescription>
|
|
216
|
+
</CardHeader>
|
|
217
|
+
<CardContent className="space-y-4">
|
|
218
|
+
<div className="space-y-2">
|
|
219
|
+
<div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
|
|
220
|
+
<div>
|
|
221
|
+
<p className="text-sm font-medium text-gray-900">{t('remoteEnabled')}</p>
|
|
222
|
+
<p className="mt-1 text-xs text-gray-500">{t('remoteEnabledHelp')}</p>
|
|
197
223
|
</div>
|
|
224
|
+
<Switch checked={props.enabled} onCheckedChange={props.setEnabled} />
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
<div className="space-y-2">
|
|
229
|
+
<Label htmlFor="remote-device-name">{t('remoteDeviceName')}</Label>
|
|
230
|
+
<Input id="remote-device-name" value={props.deviceName} onChange={(event) => props.setDeviceName(event.target.value)} placeholder={t('remoteDeviceNamePlaceholder')} />
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<div className="space-y-2">
|
|
234
|
+
<Label htmlFor="remote-platform-api-base">{t('remotePlatformApiBase')}</Label>
|
|
235
|
+
<Input
|
|
236
|
+
id="remote-platform-api-base"
|
|
237
|
+
value={props.platformApiBase}
|
|
238
|
+
onChange={(event) => props.setPlatformApiBase(event.target.value)}
|
|
239
|
+
placeholder="https://ai-gateway-api.nextclaw.io/v1"
|
|
240
|
+
/>
|
|
241
|
+
<p className="text-xs text-gray-500">{t('remotePlatformApiBaseHelp')}</p>
|
|
242
|
+
</div>
|
|
198
243
|
|
|
244
|
+
<div className="flex flex-wrap gap-3">
|
|
245
|
+
<Button
|
|
246
|
+
onClick={() =>
|
|
247
|
+
props.settingsMutation.mutate({
|
|
248
|
+
enabled: props.enabled,
|
|
249
|
+
deviceName: props.deviceName,
|
|
250
|
+
platformApiBase: props.platformApiBase
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
disabled={props.settingsMutation.isPending}
|
|
254
|
+
>
|
|
255
|
+
{props.settingsMutation.isPending ? t('saving') : t('remoteSaveSettings')}
|
|
256
|
+
</Button>
|
|
257
|
+
<Button variant="outline" onClick={() => props.serviceMutation.mutate('restart')} disabled={props.serviceMutation.isPending}>
|
|
258
|
+
<RefreshCcw className="mr-2 h-4 w-4" />
|
|
259
|
+
{t('remoteRestartService')}
|
|
260
|
+
</Button>
|
|
261
|
+
</div>
|
|
262
|
+
<p className="text-xs text-gray-500">{t('remoteSaveHint')}</p>
|
|
263
|
+
</CardContent>
|
|
264
|
+
</Card>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function RemoteAccountCard(props: {
|
|
269
|
+
status: RemoteAccessView | undefined;
|
|
270
|
+
platformApiBase: string;
|
|
271
|
+
browserAuthStartMutation: ReturnType<typeof useRemoteBrowserAuthStart>;
|
|
272
|
+
logoutMutation: ReturnType<typeof useRemoteLogout>;
|
|
273
|
+
authSessionId: string | null;
|
|
274
|
+
authExpiresAt: string | null;
|
|
275
|
+
authStatusMessage: string;
|
|
276
|
+
authVerificationUri: string | null;
|
|
277
|
+
onStartBrowserAuth: () => Promise<void>;
|
|
278
|
+
onResumeBrowserAuth: () => void;
|
|
279
|
+
}) {
|
|
280
|
+
return (
|
|
281
|
+
<Card>
|
|
282
|
+
<CardHeader>
|
|
283
|
+
<CardTitle className="flex items-center gap-2">
|
|
284
|
+
<KeyRound className="h-4 w-4 text-primary" />
|
|
285
|
+
{t('remoteAccountTitle')}
|
|
286
|
+
</CardTitle>
|
|
287
|
+
<CardDescription>{t('remoteAccountDescription')}</CardDescription>
|
|
288
|
+
</CardHeader>
|
|
289
|
+
<CardContent className="space-y-4">
|
|
290
|
+
{props.status?.account.loggedIn ? (
|
|
291
|
+
<>
|
|
199
292
|
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
200
|
-
<KeyValueRow label={t('
|
|
201
|
-
<KeyValueRow label={t('
|
|
202
|
-
<KeyValueRow label={t('
|
|
203
|
-
<KeyValueRow label={t('remoteLastConnectedAt')} value={status?.runtime?.lastConnectedAt ? formatDateTime(status.runtime.lastConnectedAt) : '-'} muted />
|
|
204
|
-
<KeyValueRow label={t('remoteLastError')} value={status?.runtime?.lastError} muted />
|
|
293
|
+
<KeyValueRow label={t('remoteAccountEmail')} value={props.status.account.email} />
|
|
294
|
+
<KeyValueRow label={t('remoteAccountRole')} value={props.status.account.role} />
|
|
295
|
+
<KeyValueRow label={t('remoteApiBase')} value={props.status.account.apiBase} />
|
|
205
296
|
</div>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
{t('
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
<div className="flex items-center justify-between rounded-2xl border border-gray-200/70 px-4 py-3">
|
|
220
|
-
<div>
|
|
221
|
-
<p className="text-sm font-medium text-gray-900">{t('remoteEnabled')}</p>
|
|
222
|
-
<p className="mt-1 text-xs text-gray-500">{t('remoteEnabledHelp')}</p>
|
|
223
|
-
</div>
|
|
224
|
-
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
|
297
|
+
<Button variant="outline" onClick={() => props.logoutMutation.mutate()} disabled={props.logoutMutation.isPending}>
|
|
298
|
+
{props.logoutMutation.isPending ? t('remoteLoggingOut') : t('remoteLogout')}
|
|
299
|
+
</Button>
|
|
300
|
+
</>
|
|
301
|
+
) : (
|
|
302
|
+
<>
|
|
303
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
304
|
+
<p className="text-sm font-medium text-gray-900">{t('remoteBrowserAuthTitle')}</p>
|
|
305
|
+
<p className="mt-1 text-sm text-gray-600">{t('remoteBrowserAuthDescription')}</p>
|
|
306
|
+
<div className="mt-3 border-t border-white/80 pt-3">
|
|
307
|
+
<KeyValueRow label={t('remoteApiBase')} value={props.platformApiBase || props.status?.settings.platformApiBase || props.status?.account.apiBase} muted />
|
|
308
|
+
<KeyValueRow label={t('remoteBrowserAuthSession')} value={props.authSessionId} muted />
|
|
309
|
+
<KeyValueRow label={t('remoteBrowserAuthExpiresAt')} value={props.authExpiresAt ? formatDateTime(props.authExpiresAt) : '-'} muted />
|
|
225
310
|
</div>
|
|
226
311
|
</div>
|
|
227
|
-
|
|
228
|
-
<div className="
|
|
229
|
-
<
|
|
230
|
-
|
|
312
|
+
{props.authStatusMessage ? <p className="text-sm text-gray-600">{props.authStatusMessage}</p> : null}
|
|
313
|
+
<div className="flex flex-wrap gap-3">
|
|
314
|
+
<Button onClick={props.onStartBrowserAuth} disabled={props.browserAuthStartMutation.isPending || !!props.authSessionId}>
|
|
315
|
+
{props.browserAuthStartMutation.isPending
|
|
316
|
+
? t('remoteBrowserAuthStarting')
|
|
317
|
+
: props.authSessionId
|
|
318
|
+
? t('remoteBrowserAuthAuthorizing')
|
|
319
|
+
: t('remoteBrowserAuthAction')}
|
|
320
|
+
</Button>
|
|
321
|
+
{props.authVerificationUri ? (
|
|
322
|
+
<Button variant="outline" onClick={props.onResumeBrowserAuth}>
|
|
323
|
+
{t('remoteBrowserAuthResume')}
|
|
324
|
+
</Button>
|
|
325
|
+
) : null}
|
|
231
326
|
</div>
|
|
327
|
+
<p className="text-xs text-gray-500">{t('remoteBrowserAuthHint')}</p>
|
|
328
|
+
</>
|
|
329
|
+
)}
|
|
330
|
+
</CardContent>
|
|
331
|
+
</Card>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
232
334
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
335
|
+
function RemoteServiceCard(props: {
|
|
336
|
+
status: RemoteAccessView | undefined;
|
|
337
|
+
serviceMutation: ReturnType<typeof useRemoteServiceControl>;
|
|
338
|
+
}) {
|
|
339
|
+
return (
|
|
340
|
+
<Card>
|
|
341
|
+
<CardHeader>
|
|
342
|
+
<CardTitle className="flex items-center gap-2">
|
|
343
|
+
<ServerCog className="h-4 w-4 text-primary" />
|
|
344
|
+
{t('remoteServiceTitle')}
|
|
345
|
+
</CardTitle>
|
|
346
|
+
<CardDescription>{t('remoteServiceDescription')}</CardDescription>
|
|
347
|
+
</CardHeader>
|
|
348
|
+
<CardContent className="space-y-4">
|
|
349
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
350
|
+
<KeyValueRow label={t('remoteServicePid')} value={props.status?.service.pid} />
|
|
351
|
+
<KeyValueRow label={t('remoteServiceUiUrl')} value={props.status?.service.uiUrl} />
|
|
352
|
+
<KeyValueRow label={t('remoteServiceCurrentProcess')} value={props.status?.service.currentProcess ? t('yes') : t('no')} />
|
|
353
|
+
</div>
|
|
354
|
+
<div className="flex flex-wrap gap-3">
|
|
355
|
+
<Button variant="primary" onClick={() => props.serviceMutation.mutate('start')} disabled={props.serviceMutation.isPending}>
|
|
356
|
+
{t('remoteStartService')}
|
|
357
|
+
</Button>
|
|
358
|
+
<Button variant="outline" onClick={() => props.serviceMutation.mutate('restart')} disabled={props.serviceMutation.isPending}>
|
|
359
|
+
{t('remoteRestartService')}
|
|
360
|
+
</Button>
|
|
361
|
+
<Button variant="outline" onClick={() => props.serviceMutation.mutate('stop')} disabled={props.serviceMutation.isPending}>
|
|
362
|
+
{t('remoteStopService')}
|
|
363
|
+
</Button>
|
|
364
|
+
</div>
|
|
365
|
+
<p className="text-xs text-gray-500">{t('remoteServiceHint')}</p>
|
|
366
|
+
</CardContent>
|
|
367
|
+
</Card>
|
|
368
|
+
);
|
|
369
|
+
}
|
|
243
370
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
371
|
+
function RemoteDoctorCard(props: {
|
|
372
|
+
doctorMutation: ReturnType<typeof useRemoteDoctor>;
|
|
373
|
+
}) {
|
|
374
|
+
return (
|
|
375
|
+
<Card>
|
|
376
|
+
<CardHeader>
|
|
377
|
+
<CardTitle className="flex items-center gap-2">
|
|
378
|
+
<ShieldCheck className="h-4 w-4 text-primary" />
|
|
379
|
+
{t('remoteDoctorTitle')}
|
|
380
|
+
</CardTitle>
|
|
381
|
+
<CardDescription>{t('remoteDoctorDescription')}</CardDescription>
|
|
382
|
+
</CardHeader>
|
|
383
|
+
<CardContent className="space-y-4">
|
|
384
|
+
<div className="flex flex-wrap gap-3">
|
|
385
|
+
<Button variant="outline" onClick={() => props.doctorMutation.mutate()} disabled={props.doctorMutation.isPending}>
|
|
386
|
+
<SquareTerminal className="mr-2 h-4 w-4" />
|
|
387
|
+
{props.doctorMutation.isPending ? t('remoteDoctorRunning') : t('remoteRunDoctor')}
|
|
388
|
+
</Button>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
{props.doctorMutation.data ? <DoctorResultPanel doctor={props.doctorMutation.data} /> : <p className="text-sm text-gray-500">{t('remoteDoctorEmpty')}</p>}
|
|
392
|
+
</CardContent>
|
|
393
|
+
</Card>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function DoctorResultPanel(props: { doctor: RemoteDoctorView }) {
|
|
398
|
+
return (
|
|
399
|
+
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
400
|
+
<KeyValueRow label={t('remoteDoctorGeneratedAt')} value={formatDateTime(props.doctor.generatedAt)} muted />
|
|
401
|
+
<div className="mt-3 space-y-2">
|
|
402
|
+
{props.doctor.checks.map((check) => (
|
|
403
|
+
<div key={check.name} className="rounded-xl border border-white bg-white px-3 py-3">
|
|
404
|
+
<div className="flex items-center justify-between gap-3">
|
|
405
|
+
<span className="text-sm font-medium text-gray-900">{check.name}</span>
|
|
406
|
+
<StatusDot status={check.ok ? 'ready' : 'warning'} label={check.ok ? t('remoteCheckPassed') : t('remoteCheckFailed')} />
|
|
265
407
|
</div>
|
|
266
|
-
<p className="text-
|
|
267
|
-
</
|
|
268
|
-
|
|
408
|
+
<p className="mt-2 text-sm text-gray-600">{check.detail}</p>
|
|
409
|
+
</div>
|
|
410
|
+
))}
|
|
269
411
|
</div>
|
|
412
|
+
</div>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
270
415
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
</>
|
|
324
|
-
)}
|
|
325
|
-
</CardContent>
|
|
326
|
-
</Card>
|
|
327
|
-
|
|
328
|
-
<Card>
|
|
329
|
-
<CardHeader>
|
|
330
|
-
<CardTitle className="flex items-center gap-2">
|
|
331
|
-
<ServerCog className="h-4 w-4 text-primary" />
|
|
332
|
-
{t('remoteServiceTitle')}
|
|
333
|
-
</CardTitle>
|
|
334
|
-
<CardDescription>{t('remoteServiceDescription')}</CardDescription>
|
|
335
|
-
</CardHeader>
|
|
336
|
-
<CardContent className="space-y-4">
|
|
337
|
-
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
338
|
-
<KeyValueRow label={t('remoteServicePid')} value={status?.service.pid} />
|
|
339
|
-
<KeyValueRow label={t('remoteServiceUiUrl')} value={status?.service.uiUrl} />
|
|
340
|
-
<KeyValueRow label={t('remoteServiceCurrentProcess')} value={status?.service.currentProcess ? t('yes') : t('no')} />
|
|
341
|
-
</div>
|
|
342
|
-
<div className="flex flex-wrap gap-3">
|
|
343
|
-
<Button variant="primary" onClick={() => serviceMutation.mutate('start')} disabled={serviceMutation.isPending}>
|
|
344
|
-
{t('remoteStartService')}
|
|
345
|
-
</Button>
|
|
346
|
-
<Button variant="outline" onClick={() => serviceMutation.mutate('restart')} disabled={serviceMutation.isPending}>
|
|
347
|
-
{t('remoteRestartService')}
|
|
348
|
-
</Button>
|
|
349
|
-
<Button variant="outline" onClick={() => serviceMutation.mutate('stop')} disabled={serviceMutation.isPending}>
|
|
350
|
-
{t('remoteStopService')}
|
|
351
|
-
</Button>
|
|
352
|
-
</div>
|
|
353
|
-
<p className="text-xs text-gray-500">{t('remoteServiceHint')}</p>
|
|
354
|
-
</CardContent>
|
|
355
|
-
</Card>
|
|
416
|
+
export function RemoteAccessPage() {
|
|
417
|
+
const remoteStatus = useRemoteStatus();
|
|
418
|
+
const browserAuthStartMutation = useRemoteBrowserAuthStart();
|
|
419
|
+
const browserAuthPollMutation = useRemoteBrowserAuthPoll();
|
|
420
|
+
const logoutMutation = useRemoteLogout();
|
|
421
|
+
const settingsMutation = useRemoteSettings();
|
|
422
|
+
const doctorMutation = useRemoteDoctor();
|
|
423
|
+
const serviceMutation = useRemoteServiceControl();
|
|
424
|
+
|
|
425
|
+
const status = remoteStatus.data;
|
|
426
|
+
const runtimeStatus = useMemo(() => getRuntimeStatus(status?.runtime ?? null), [status?.runtime]);
|
|
427
|
+
const serviceStatus = useMemo(() => getServiceStatus(status?.service ?? { running: false, currentProcess: false }), [status?.service]);
|
|
428
|
+
|
|
429
|
+
const [enabled, setEnabled] = useState(false);
|
|
430
|
+
const [deviceName, setDeviceName] = useState('');
|
|
431
|
+
const [platformApiBase, setPlatformApiBase] = useState('');
|
|
432
|
+
const browserAuth = useRemoteBrowserAuthFlow({
|
|
433
|
+
status,
|
|
434
|
+
platformApiBase,
|
|
435
|
+
startBrowserAuth: browserAuthStartMutation,
|
|
436
|
+
pollBrowserAuth: browserAuthPollMutation
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (!status) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
setEnabled(status.settings.enabled);
|
|
444
|
+
setDeviceName(status.settings.deviceName);
|
|
445
|
+
setPlatformApiBase(status.settings.platformApiBase);
|
|
446
|
+
}, [status]);
|
|
447
|
+
|
|
448
|
+
if (remoteStatus.isLoading && !status) {
|
|
449
|
+
return <div className="p-8 text-gray-400">{t('remoteLoading')}</div>;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<PageLayout className="space-y-6">
|
|
454
|
+
<PageHeader title={t('remotePageTitle')} description={t('remotePageDescription')} />
|
|
455
|
+
|
|
456
|
+
<div className="grid gap-6 xl:grid-cols-[1.35fr_0.95fr]">
|
|
457
|
+
<RemoteOverviewCard status={status} runtimeStatus={runtimeStatus} serviceStatus={serviceStatus} />
|
|
458
|
+
<RemoteDeviceCard
|
|
459
|
+
enabled={enabled}
|
|
460
|
+
setEnabled={setEnabled}
|
|
461
|
+
deviceName={deviceName}
|
|
462
|
+
setDeviceName={setDeviceName}
|
|
463
|
+
platformApiBase={platformApiBase}
|
|
464
|
+
setPlatformApiBase={setPlatformApiBase}
|
|
465
|
+
settingsMutation={settingsMutation}
|
|
466
|
+
serviceMutation={serviceMutation}
|
|
467
|
+
/>
|
|
356
468
|
</div>
|
|
357
469
|
|
|
358
|
-
<
|
|
359
|
-
<
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
470
|
+
<div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
|
|
471
|
+
<RemoteAccountCard
|
|
472
|
+
status={status}
|
|
473
|
+
platformApiBase={platformApiBase}
|
|
474
|
+
browserAuthStartMutation={browserAuthStartMutation}
|
|
475
|
+
logoutMutation={logoutMutation}
|
|
476
|
+
authSessionId={browserAuth.authSessionId}
|
|
477
|
+
authExpiresAt={browserAuth.authExpiresAt}
|
|
478
|
+
authStatusMessage={browserAuth.authStatusMessage}
|
|
479
|
+
authVerificationUri={browserAuth.authVerificationUri}
|
|
480
|
+
onStartBrowserAuth={browserAuth.start}
|
|
481
|
+
onResumeBrowserAuth={browserAuth.resume}
|
|
482
|
+
/>
|
|
483
|
+
<RemoteServiceCard status={status} serviceMutation={serviceMutation} />
|
|
484
|
+
</div>
|
|
373
485
|
|
|
374
|
-
|
|
375
|
-
<div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
|
|
376
|
-
<KeyValueRow label={t('remoteDoctorGeneratedAt')} value={formatDateTime(doctorMutation.data.generatedAt)} muted />
|
|
377
|
-
<div className="mt-3 space-y-2">
|
|
378
|
-
{doctorMutation.data.checks.map((check) => (
|
|
379
|
-
<div key={check.name} className="rounded-xl border border-white bg-white px-3 py-3">
|
|
380
|
-
<div className="flex items-center justify-between gap-3">
|
|
381
|
-
<span className="text-sm font-medium text-gray-900">{check.name}</span>
|
|
382
|
-
<StatusDot status={check.ok ? 'ready' : 'warning'} label={check.ok ? t('remoteCheckPassed') : t('remoteCheckFailed')} />
|
|
383
|
-
</div>
|
|
384
|
-
<p className="mt-2 text-sm text-gray-600">{check.detail}</p>
|
|
385
|
-
</div>
|
|
386
|
-
))}
|
|
387
|
-
</div>
|
|
388
|
-
</div>
|
|
389
|
-
) : (
|
|
390
|
-
<p className="text-sm text-gray-500">{t('remoteDoctorEmpty')}</p>
|
|
391
|
-
)}
|
|
392
|
-
</CardContent>
|
|
393
|
-
</Card>
|
|
486
|
+
<RemoteDoctorCard doctorMutation={doctorMutation} />
|
|
394
487
|
</PageLayout>
|
|
395
488
|
);
|
|
396
489
|
}
|