@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.
Files changed (29) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/assets/{ChannelsList-DDfZIiJa.js → ChannelsList-Byfj2R01.js} +1 -1
  3. package/dist/assets/{ChatPage-FpRraTxm.js → ChatPage-DM1ewbWf.js} +1 -1
  4. package/dist/assets/{DocBrowser-Kndx8OJj.js → DocBrowser-BLv77lJ0.js} +1 -1
  5. package/dist/assets/{LogoBadge-hKHoLH9n.js → LogoBadge-D7j1al-w.js} +1 -1
  6. package/dist/assets/{MarketplacePage-CZIJyfjK.js → MarketplacePage-DuskLKYh.js} +1 -1
  7. package/dist/assets/{McpMarketplacePage-BGrAMA37.js → McpMarketplacePage-DpMjaD3m.js} +1 -1
  8. package/dist/assets/{ModelConfig-BpKQeGfb.js → ModelConfig-ubaecweS.js} +1 -1
  9. package/dist/assets/{ProvidersList-qfUL6mrW.js → ProvidersList-w8MJH2LI.js} +1 -1
  10. package/dist/assets/RemoteAccessPage-D79_5Kbn.js +1 -0
  11. package/dist/assets/{RuntimeConfig-CVlqNWKO.js → RuntimeConfig-BbX4yFKy.js} +1 -1
  12. package/dist/assets/{SearchConfig-DXFV6Mvx.js → SearchConfig-BmmmeyJd.js} +1 -1
  13. package/dist/assets/{SecretsConfig-BGW9aUqv.js → SecretsConfig-CWG8J01H.js} +1 -1
  14. package/dist/assets/{SessionsConfig-BByfa1ke.js → SessionsConfig-D-vg_Lgv.js} +1 -1
  15. package/dist/assets/{chat-message-ZwnDwDuQ.js → chat-message-CGXiVhyN.js} +1 -1
  16. package/dist/assets/{index-BWvap_iq.js → index-CeRbsQ90.js} +2 -2
  17. package/dist/assets/{label-Bklr3fXc.js → label-CCSffS1D.js} +1 -1
  18. package/dist/assets/{page-layout-sNhcbwtm.js → page-layout-ud8wZ8gX.js} +1 -1
  19. package/dist/assets/{popover-C3rJrJJG.js → popover-Bfoe6YBX.js} +1 -1
  20. package/dist/assets/{security-config-BueosYw1.js → security-config-DJJUCMov.js} +1 -1
  21. package/dist/assets/{skeleton-CiG6msbm.js → skeleton-IOOTmHzP.js} +1 -1
  22. package/dist/assets/{status-dot-CsIV5YrS.js → status-dot-Fz9-eKsl.js} +1 -1
  23. package/dist/assets/{switch-DSdHSIsC.js → switch-B-_SrMSL.js} +1 -1
  24. package/dist/assets/{tabs-custom-BB-VjdL2.js → tabs-custom-6Tm1ZHfS.js} +1 -1
  25. package/dist/assets/{useConfirmDialog-BL5s8KDC.js → useConfirmDialog-BeOW2bOI.js} +1 -1
  26. package/dist/index.html +1 -1
  27. package/package.json +4 -4
  28. package/src/components/remote/RemoteAccessPage.tsx +334 -241
  29. 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
- export function RemoteAccessPage() {
61
- const remoteStatus = useRemoteStatus();
62
- const browserAuthStartMutation = useRemoteBrowserAuthStart();
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
- const [enabled, setEnabled] = useState(false);
74
- const [deviceName, setDeviceName] = useState('');
75
- const [platformApiBase, setPlatformApiBase] = useState('');
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 browserAuthPollMutation.mutateAsync({
97
+ const result = await pollBrowserAuth.mutateAsync({
111
98
  sessionId: authSessionId,
112
- apiBase: platformApiBase.trim() || status?.settings.platformApiBase || status?.account.apiBase || undefined
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
- authPollIntervalMs,
146
- authSessionId,
147
- browserAuthPollMutation,
148
- platformApiBase,
149
- status?.account.apiBase,
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 handleResumeBrowserAuth = () => {
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
- if (remoteStatus.isLoading && !status) {
176
- return <div className="p-8 text-gray-400">{t('remoteLoading')}</div>;
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
- <PageLayout className="space-y-6">
181
- <PageHeader title={t('remotePageTitle')} description={t('remotePageDescription')} />
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
- <div className="grid gap-6 xl:grid-cols-[1.35fr_0.95fr]">
184
- <Card>
185
- <CardHeader>
186
- <CardTitle className="flex items-center gap-2">
187
- <Activity className="h-4 w-4 text-primary" />
188
- {t('remoteOverviewTitle')}
189
- </CardTitle>
190
- <CardDescription>{t('remoteOverviewDescription')}</CardDescription>
191
- </CardHeader>
192
- <CardContent className="space-y-5">
193
- <div className="flex flex-wrap gap-2">
194
- <StatusDot status={status?.account.loggedIn ? 'ready' : 'inactive'} label={status?.account.loggedIn ? t('remoteAccountConnected') : t('remoteAccountNotConnected')} />
195
- <StatusDot status={serviceStatus.status} label={serviceStatus.label} />
196
- <StatusDot status={runtimeStatus.status} label={runtimeStatus.label} />
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('remoteLocalOrigin')} value={status?.localOrigin} />
201
- <KeyValueRow label={t('remotePublicPlatform')} value={status?.platformBase ?? status?.account.platformBase} />
202
- <KeyValueRow label={t('remoteDeviceId')} value={status?.runtime?.deviceId} muted />
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
- </CardContent>
207
- </Card>
208
-
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>
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="space-y-2">
229
- <Label htmlFor="remote-device-name">{t('remoteDeviceName')}</Label>
230
- <Input id="remote-device-name" value={deviceName} onChange={(event) => setDeviceName(event.target.value)} placeholder={t('remoteDeviceNamePlaceholder')} />
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
- <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={platformApiBase}
238
- onChange={(event) => 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>
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
- <div className="flex flex-wrap gap-3">
245
- <Button
246
- onClick={() =>
247
- settingsMutation.mutate({
248
- enabled,
249
- deviceName,
250
- platformApiBase
251
- })
252
- }
253
- disabled={settingsMutation.isPending}
254
- >
255
- {settingsMutation.isPending ? t('saving') : t('remoteSaveSettings')}
256
- </Button>
257
- <Button
258
- variant="outline"
259
- onClick={() => serviceMutation.mutate('restart')}
260
- disabled={serviceMutation.isPending}
261
- >
262
- <RefreshCcw className="mr-2 h-4 w-4" />
263
- {t('remoteRestartService')}
264
- </Button>
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-xs text-gray-500">{t('remoteSaveHint')}</p>
267
- </CardContent>
268
- </Card>
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
- <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
272
- <Card>
273
- <CardHeader>
274
- <CardTitle className="flex items-center gap-2">
275
- <KeyRound className="h-4 w-4 text-primary" />
276
- {t('remoteAccountTitle')}
277
- </CardTitle>
278
- <CardDescription>{t('remoteAccountDescription')}</CardDescription>
279
- </CardHeader>
280
- <CardContent className="space-y-4">
281
- {status?.account.loggedIn ? (
282
- <>
283
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
284
- <KeyValueRow label={t('remoteAccountEmail')} value={status.account.email} />
285
- <KeyValueRow label={t('remoteAccountRole')} value={status.account.role} />
286
- <KeyValueRow label={t('remoteApiBase')} value={status.account.apiBase} />
287
- </div>
288
- <Button variant="outline" onClick={() => logoutMutation.mutate()} disabled={logoutMutation.isPending}>
289
- {logoutMutation.isPending ? t('remoteLoggingOut') : t('remoteLogout')}
290
- </Button>
291
- </>
292
- ) : (
293
- <>
294
- <div className="rounded-2xl border border-gray-200/70 bg-gray-50/70 px-4 py-3">
295
- <p className="text-sm font-medium text-gray-900">{t('remoteBrowserAuthTitle')}</p>
296
- <p className="mt-1 text-sm text-gray-600">{t('remoteBrowserAuthDescription')}</p>
297
- <div className="mt-3 border-t border-white/80 pt-3">
298
- <KeyValueRow label={t('remoteApiBase')} value={platformApiBase || status?.settings.platformApiBase || status?.account.apiBase} muted />
299
- <KeyValueRow label={t('remoteBrowserAuthSession')} value={authSessionId} muted />
300
- <KeyValueRow
301
- label={t('remoteBrowserAuthExpiresAt')}
302
- value={authExpiresAt ? formatDateTime(authExpiresAt) : '-'}
303
- muted
304
- />
305
- </div>
306
- </div>
307
- {authStatusMessage ? <p className="text-sm text-gray-600">{authStatusMessage}</p> : null}
308
- <div className="flex flex-wrap gap-3">
309
- <Button onClick={handleStartBrowserAuth} disabled={browserAuthStartMutation.isPending || !!authSessionId}>
310
- {browserAuthStartMutation.isPending
311
- ? t('remoteBrowserAuthStarting')
312
- : authSessionId
313
- ? t('remoteBrowserAuthAuthorizing')
314
- : t('remoteBrowserAuthAction')}
315
- </Button>
316
- {authVerificationUri ? (
317
- <Button variant="outline" onClick={handleResumeBrowserAuth}>
318
- {t('remoteBrowserAuthResume')}
319
- </Button>
320
- ) : null}
321
- </div>
322
- <p className="text-xs text-gray-500">{t('remoteBrowserAuthHint')}</p>
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
- <Card>
359
- <CardHeader>
360
- <CardTitle className="flex items-center gap-2">
361
- <ShieldCheck className="h-4 w-4 text-primary" />
362
- {t('remoteDoctorTitle')}
363
- </CardTitle>
364
- <CardDescription>{t('remoteDoctorDescription')}</CardDescription>
365
- </CardHeader>
366
- <CardContent className="space-y-4">
367
- <div className="flex flex-wrap gap-3">
368
- <Button variant="outline" onClick={() => doctorMutation.mutate()} disabled={doctorMutation.isPending}>
369
- <SquareTerminal className="mr-2 h-4 w-4" />
370
- {doctorMutation.isPending ? t('remoteDoctorRunning') : t('remoteRunDoctor')}
371
- </Button>
372
- </div>
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
- {doctorMutation.data ? (
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
  }