dsh-zen-remote 2.3.11 → 2.4.1
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 +45 -78
- package/README.md +94 -84
- package/README.zh-CN.md +90 -81
- package/bin/zen-remote.mjs +5 -0
- package/docs/interface.md +6 -6
- package/docs/remote-access.en.md +6 -4
- package/docs/remote-access.md +6 -4
- package/dsh-push.mjs +96 -10
- package/lib/adapters/dsh-better-sidebar/index.js +7 -3
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/admin-routes.js +10 -2
- package/lib/admin-routes.js.map +1 -1
- package/lib/cli/args.js +251 -0
- package/lib/cli/args.js.map +1 -0
- package/lib/cli/bootstrap.js +449 -0
- package/lib/cli/bootstrap.js.map +1 -0
- package/lib/cli/command-setup.js +484 -0
- package/lib/cli/command-setup.js.map +1 -0
- package/lib/cli/commands-client.js +359 -0
- package/lib/cli/commands-client.js.map +1 -0
- package/lib/cli/commands-host.js +1108 -0
- package/lib/cli/commands-host.js.map +1 -0
- package/lib/cli/commands-status.js +279 -0
- package/lib/cli/commands-status.js.map +1 -0
- package/lib/cli/config-writer.js +334 -0
- package/lib/cli/config-writer.js.map +1 -0
- package/lib/cli/context.js +10 -0
- package/lib/cli/context.js.map +1 -0
- package/lib/cli/i18n.js +620 -0
- package/lib/cli/i18n.js.map +1 -0
- package/lib/cli/ipc-client.js +160 -0
- package/lib/cli/ipc-client.js.map +1 -0
- package/lib/cli/ipc-server.js +464 -0
- package/lib/cli/ipc-server.js.map +1 -0
- package/lib/cli/main.js +245 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/cli/qr-terminal.js +48 -0
- package/lib/cli/qr-terminal.js.map +1 -0
- package/lib/client-data/qr.js +286 -0
- package/lib/client-data/qr.js.map +1 -0
- package/lib/client-routes.js +308 -25
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +1013 -229
- package/lib/config.js +50 -12
- package/lib/config.js.map +1 -1
- package/lib/debug-report.js +28 -1
- package/lib/debug-report.js.map +1 -1
- package/lib/fetch-route-intercept.js +334 -26
- package/lib/fetch-route-intercept.js.map +1 -1
- package/lib/index.js +608 -375
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +1833 -280
- package/lib/intercept.js.map +1 -1
- package/lib/lan-gate-server.cjs +36 -10
- package/lib/merge-streams.js +103 -11
- package/lib/merge-streams.js.map +1 -1
- package/lib/plugin-route-intercept.js +93 -63
- package/lib/plugin-route-intercept.js.map +1 -1
- package/lib/relay-access.js +8 -1
- package/lib/relay-access.js.map +1 -1
- package/lib/relay-client.js +133 -4
- package/lib/relay-client.js.map +1 -1
- package/lib/relay-pool.js +275 -0
- package/lib/relay-pool.js.map +1 -0
- package/lib/relay-server.js +269 -17
- package/lib/relay-server.js.map +1 -1
- package/lib/restart-fields.js +6 -4
- package/lib/restart-fields.js.map +1 -1
- package/lib/share-ops.js +68 -0
- package/lib/share-ops.js.map +1 -1
- package/lib/share-store.js +65 -27
- package/lib/share-store.js.map +1 -1
- package/lib/state-store.js +400 -0
- package/lib/state-store.js.map +1 -0
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +3 -2
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/cli/args.d.ts +78 -0
- package/lib/types/cli/args.d.ts.map +1 -0
- package/lib/types/cli/bootstrap.d.ts +170 -0
- package/lib/types/cli/bootstrap.d.ts.map +1 -0
- package/lib/types/cli/command-setup.d.ts +72 -0
- package/lib/types/cli/command-setup.d.ts.map +1 -0
- package/lib/types/cli/commands-client.d.ts +36 -0
- package/lib/types/cli/commands-client.d.ts.map +1 -0
- package/lib/types/cli/commands-host.d.ts +82 -0
- package/lib/types/cli/commands-host.d.ts.map +1 -0
- package/lib/types/cli/commands-status.d.ts +46 -0
- package/lib/types/cli/commands-status.d.ts.map +1 -0
- package/lib/types/cli/config-writer.d.ts +92 -0
- package/lib/types/cli/config-writer.d.ts.map +1 -0
- package/lib/types/cli/context.d.ts +39 -0
- package/lib/types/cli/context.d.ts.map +1 -0
- package/lib/types/cli/i18n.d.ts +32 -0
- package/lib/types/cli/i18n.d.ts.map +1 -0
- package/lib/types/cli/ipc-client.d.ts +83 -0
- package/lib/types/cli/ipc-client.d.ts.map +1 -0
- package/lib/types/cli/ipc-server.d.ts +113 -0
- package/lib/types/cli/ipc-server.d.ts.map +1 -0
- package/lib/types/cli/main.d.ts +57 -0
- package/lib/types/cli/main.d.ts.map +1 -0
- package/lib/types/cli/qr-terminal.d.ts +17 -0
- package/lib/types/cli/qr-terminal.d.ts.map +1 -0
- package/lib/types/client/RemoteStatusIcon.d.ts.map +1 -1
- package/lib/types/client/current-session-report.d.ts +4 -0
- package/lib/types/client/current-session-report.d.ts.map +1 -1
- package/lib/types/client/effects/model-group-side.d.ts.map +1 -1
- package/lib/types/client/effects/remote-session.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +47 -3
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/settings/SettingsSection.d.ts.map +1 -1
- package/lib/types/client/settings/settings-css.d.ts +1 -1
- package/lib/types/client/settings/settings-css.d.ts.map +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts +1 -1
- package/lib/types/client/styles/remote-session.css.d.ts.map +1 -1
- package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -1
- package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -1
- package/lib/types/client/wizard/wizard-css.d.ts +1 -1
- package/lib/types/client/wizard/wizard-css.d.ts.map +1 -1
- package/lib/types/client-data/model-group-side.d.ts +23 -2
- package/lib/types/client-data/model-group-side.d.ts.map +1 -1
- package/lib/types/client-data/remote-status.d.ts +47 -1
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +85 -1
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-data/wizard.d.ts +56 -1
- package/lib/types/client-data/wizard.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +25 -1
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +40 -14
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/debug-report.d.ts.map +1 -1
- package/lib/types/fetch-route-intercept.d.ts +66 -8
- package/lib/types/fetch-route-intercept.d.ts.map +1 -1
- package/lib/types/index.d.ts +9 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts +72 -10
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/merge-streams.d.ts +79 -1
- package/lib/types/merge-streams.d.ts.map +1 -1
- package/lib/types/plugin-route-intercept.d.ts +25 -9
- package/lib/types/plugin-route-intercept.d.ts.map +1 -1
- package/lib/types/relay-access.d.ts +3 -1
- package/lib/types/relay-access.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts +23 -2
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/relay-pool.d.ts +148 -0
- package/lib/types/relay-pool.d.ts.map +1 -0
- package/lib/types/relay-server.d.ts +30 -3
- package/lib/types/relay-server.d.ts.map +1 -1
- package/lib/types/restart-fields.d.ts +5 -5
- package/lib/types/restart-fields.d.ts.map +1 -1
- package/lib/types/share-ops.d.ts +47 -1
- package/lib/types/share-ops.d.ts.map +1 -1
- package/lib/types/share-store.d.ts.map +1 -1
- package/lib/types/state-store.d.ts +179 -0
- package/lib/types/state-store.d.ts.map +1 -0
- package/lib/types/upload-limits.d.ts +15 -0
- package/lib/types/upload-limits.d.ts.map +1 -1
- package/lib/types/virtual-id.d.ts +21 -0
- package/lib/types/virtual-id.d.ts.map +1 -1
- package/lib/types/wizard/probe.d.ts.map +1 -1
- package/lib/types/wizard/proxy-snippets.d.ts +3 -0
- package/lib/types/wizard/proxy-snippets.d.ts.map +1 -1
- package/lib/types/wizard/routes.d.ts +1 -0
- package/lib/types/wizard/routes.d.ts.map +1 -1
- package/lib/types/wizard/status.d.ts +9 -0
- package/lib/types/wizard/status.d.ts.map +1 -1
- package/lib/types/wizard/verify.d.ts +3 -0
- package/lib/types/wizard/verify.d.ts.map +1 -1
- package/lib/upload-limits.js +15 -0
- package/lib/upload-limits.js.map +1 -1
- package/lib/virtual-id.js +60 -0
- package/lib/virtual-id.js.map +1 -1
- package/lib/wizard/probe.js +46 -1
- package/lib/wizard/probe.js.map +1 -1
- package/lib/wizard/proxy-snippets.js +14 -1
- package/lib/wizard/proxy-snippets.js.map +1 -1
- package/lib/wizard/routes.js +23 -4
- package/lib/wizard/routes.js.map +1 -1
- package/lib/wizard/status.js +21 -0
- package/lib/wizard/status.js.map +1 -1
- package/lib/wizard/verify.js +31 -8
- package/lib/wizard/verify.js.map +1 -1
- package/package.json +8 -4
- package/pwa/icons/badge-96.png +0 -0
- package/pwa/icons/badge.svg +1 -0
- package/pwa/manifest.json +2 -1
- package/pwa/sw.js +3 -1
- package/scripts/check-debug-report.mjs +41 -1
- package/scripts/check-settings-form.mjs +14 -3
- package/scripts/check-settings-view.mjs +247 -0
- package/scripts/check-wizard-ui.mjs +37 -0
- package/src/adapters/dsh-better-sidebar/index.ts +7 -3
- package/src/admin-routes.ts +10 -2
- package/src/cli/args.ts +289 -0
- package/src/cli/bootstrap.ts +569 -0
- package/src/cli/command-setup.ts +540 -0
- package/src/cli/commands-client.ts +369 -0
- package/src/cli/commands-host.ts +1127 -0
- package/src/cli/commands-status.ts +296 -0
- package/src/cli/config-writer.ts +354 -0
- package/src/cli/context.ts +38 -0
- package/src/cli/i18n.ts +645 -0
- package/src/cli/ipc-client.ts +216 -0
- package/src/cli/ipc-server.ts +531 -0
- package/src/cli/main.ts +285 -0
- package/src/cli/qr-terminal.ts +49 -0
- package/src/client/RemoteStatusIcon.tsx +4 -1
- package/src/client/current-session-report.ts +28 -4
- package/src/client/effects/model-group-side.ts +19 -5
- package/src/client/effects/remote-session.ts +26 -6
- package/src/client/locales.ts +96 -6
- package/src/client/settings/SettingsSection.tsx +287 -92
- package/src/client/settings/settings-css.ts +18 -0
- package/src/client/styles/remote-session.css.ts +15 -1
- package/src/client/wizard/GatewayWizard.tsx +470 -109
- package/src/client/wizard/cards/ProxyCard.tsx +2 -1
- package/src/client/wizard/wizard-css.ts +23 -0
- package/src/client-data/model-group-side.ts +37 -10
- package/src/client-data/remote-status.ts +112 -17
- package/src/client-data/settings-form.ts +164 -8
- package/src/client-data/wizard.ts +112 -2
- package/src/client-routes.ts +334 -26
- package/src/config.ts +62 -16
- package/src/debug-report.ts +27 -1
- package/src/fetch-route-intercept.ts +381 -32
- package/src/index.ts +608 -380
- package/src/intercept.ts +1866 -286
- package/src/merge-streams.ts +155 -10
- package/src/plugin-route-intercept.ts +120 -71
- package/src/relay-access.ts +14 -3
- package/src/relay-client.ts +163 -5
- package/src/relay-pool.ts +387 -0
- package/src/relay-server.ts +312 -16
- package/src/restart-fields.ts +6 -4
- package/src/share-ops.ts +82 -1
- package/src/share-store.ts +59 -26
- package/src/state-store.ts +428 -0
- package/src/upload-limits.ts +16 -0
- package/src/virtual-id.ts +57 -0
- package/src/wizard/probe.ts +54 -2
- package/src/wizard/proxy-snippets.ts +17 -2
- package/src/wizard/routes.ts +24 -5
- package/src/wizard/status.ts +28 -0
- package/src/wizard/verify.ts +33 -11
- package/AGENTS.md +0 -185
- package/docs/adr/0001-plugin-bridge-forwards-plugin-routes.md +0 -15
- package/docs/adr/0002-relay-emulates-websocket.md +0 -8
|
@@ -48,7 +48,10 @@ import {
|
|
|
48
48
|
CLIENT_DEBUG_REPORT_ROUTE,
|
|
49
49
|
CLIENT_RECONNECT_ROUTE,
|
|
50
50
|
CLIENT_STATUS_ROUTE,
|
|
51
|
+
CLIENT_UNPAIR_ROUTE,
|
|
52
|
+
MAX_PAIRED_SERVERS,
|
|
51
53
|
awaitClientStatusReady,
|
|
54
|
+
canSubmitClientPairing,
|
|
52
55
|
claimDeviceNameOf,
|
|
53
56
|
createLatestGate,
|
|
54
57
|
clientStatusLineOf,
|
|
@@ -56,7 +59,6 @@ import {
|
|
|
56
59
|
deriveSettingsView,
|
|
57
60
|
localOpsAllowed,
|
|
58
61
|
normalizePairingCode,
|
|
59
|
-
shouldFollowServerDeviceName,
|
|
60
62
|
shouldReloadAfterClientActivation,
|
|
61
63
|
} from '../../client-data/settings-form.ts'
|
|
62
64
|
import { bridgeSectionState, togglePluginConsent } from '../../client-data/plugin-bridge.ts'
|
|
@@ -68,6 +70,7 @@ import type {
|
|
|
68
70
|
ClaimRouteBody,
|
|
69
71
|
ClientConfigBody,
|
|
70
72
|
ClientConnectionView,
|
|
73
|
+
ClientServerView,
|
|
71
74
|
ClientStatusBody,
|
|
72
75
|
SettingsDeviceView,
|
|
73
76
|
SettingsFieldState,
|
|
@@ -242,10 +245,40 @@ function pairFailText(fail: PairFail, t: SectionT): string {
|
|
|
242
245
|
}
|
|
243
246
|
case 'unreachable': return t('settings.client.failUnreachable')
|
|
244
247
|
case 'role-mismatch': return t('settings.client.failRoleMismatch')
|
|
248
|
+
case 'limit-reached': return t('settings.client.failLimitReached')
|
|
245
249
|
default: return t('settings.client.failUnexpected')
|
|
246
250
|
}
|
|
247
251
|
}
|
|
248
252
|
|
|
253
|
+
/** Whether one paired server has anything for the per-server diagnostics
|
|
254
|
+
* group (MS-7): a last error, a compat block, or bridge verdicts. */
|
|
255
|
+
function serverHasDiag(server: ClientServerView): boolean {
|
|
256
|
+
return server.lastError !== '' || server.compat !== undefined || server.pluginBridge.length > 0
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** One paired server's state badge (MS-7): copy plus the tone the badge
|
|
260
|
+
* wears — `live` green for connected, `warn` for an offline row (with the
|
|
261
|
+
* retry countdown when one is armed), `invalid` red for a verdict only a
|
|
262
|
+
* re-pair or an upgrade fixes. */
|
|
263
|
+
function serverStateBadge(server: ClientServerView, now: number, t: SectionT): { text: string, tone: 'live' | 'warn' | 'invalid' | undefined } {
|
|
264
|
+
switch (server.state) {
|
|
265
|
+
case 'connected': return { text: t('settings.client.serverStateConnected'), tone: 'live' }
|
|
266
|
+
case 'unreachable':
|
|
267
|
+
case 'unexpected': {
|
|
268
|
+
if (server.nextRetryAt === undefined) return { text: t('settings.client.serverStateOffline'), tone: 'warn' }
|
|
269
|
+
const seconds = Math.max(0, Math.ceil((server.nextRetryAt - now) / 1000))
|
|
270
|
+
return {
|
|
271
|
+
text: seconds > 0 ? t('settings.client.serverStateRetry', { seconds }) : t('settings.client.serverStateRetrySoon'),
|
|
272
|
+
tone: 'warn',
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
case 'revoked': return { text: t('settings.client.serverStateRevoked'), tone: 'invalid' }
|
|
276
|
+
case 'incompatible': return { text: t('settings.client.serverStateIncompatible'), tone: 'invalid' }
|
|
277
|
+
case 'invalid-url': return { text: t('settings.client.serverStateInvalidUrl'), tone: 'invalid' }
|
|
278
|
+
default: return { text: t('settings.client.serverStateUnpaired'), tone: undefined }
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
249
282
|
function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
250
283
|
const form = useSyncExternalStore(
|
|
251
284
|
useCallback((cb: () => void) => config.subscribe(cb), [config]),
|
|
@@ -284,11 +317,16 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
284
317
|
const [pairCode, setPairCode] = useState('')
|
|
285
318
|
const [claimBusy, setClaimBusy] = useState(false)
|
|
286
319
|
const [claimFail, setClaimFail] = useState<PairFail | null>(null)
|
|
287
|
-
const [claimWriteFailed, setClaimWriteFailed] = useState(false)
|
|
288
320
|
const [unpairBusy, setUnpairBusy] = useState(false)
|
|
289
321
|
const [unpairDone, setUnpairDone] = useState(false)
|
|
290
322
|
// T43: the 立即重连 button's in-flight marker.
|
|
291
323
|
const [reconnectBusy, setReconnectBusy] = useState(false)
|
|
324
|
+
// MS-7: the pool list's in-flight row action — the entry id, or 'all' for
|
|
325
|
+
// the remove-all — and whether the last one failed.
|
|
326
|
+
const [entryBusy, setEntryBusy] = useState<string | null>(null)
|
|
327
|
+
const [entryActionFailed, setEntryActionFailed] = useState(false)
|
|
328
|
+
// MS-7: the 重新配对 action focuses the code box.
|
|
329
|
+
const pairCodeRef = useRef<HTMLInputElement | null>(null)
|
|
292
330
|
|
|
293
331
|
const loadStatus = useCallback(() => {
|
|
294
332
|
const ticket = statusGate.next()
|
|
@@ -320,24 +358,9 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
320
358
|
})
|
|
321
359
|
}, [statusGate])
|
|
322
360
|
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
// update reaches the backend with both sides now EQUAL, so the push path
|
|
327
|
-
// stays silent and the two ends cannot bounce the name back and forth.
|
|
328
|
-
// T59-fix: the decision runs HERE, on each FRESH answer, judging the row
|
|
329
|
-
// and the draft AS THEY ARE at that moment — never off a stale
|
|
330
|
-
// `clientView` racing a row change (an effect keyed on the row value
|
|
331
|
-
// would fire on every save with the PREVIOUS poll's name and write the
|
|
332
|
-
// old one straight back over the user's fresh save).
|
|
333
|
-
const followServerDeviceName = useCallback((view: ClientConnectionView): void => {
|
|
334
|
-
if (!config.savedRoleIsClient()) return
|
|
335
|
-
if (view.state !== 'connected') return
|
|
336
|
-
const rowName = typeof config.rowValue('serverName') === 'string' ? config.rowValue('serverName') as string : ''
|
|
337
|
-
if (!shouldFollowServerDeviceName(view.deviceName, rowName, config.hasDraft('serverName'))) return
|
|
338
|
-
void config.writeDeviceName(view.deviceName)
|
|
339
|
-
}, [config])
|
|
340
|
-
|
|
361
|
+
// MS-7: the device name flows ONE way — a saved row serverName is
|
|
362
|
+
// broadcast to every paired server by the backend pool; a server-side
|
|
363
|
+
// record of this device's name never writes back into the row.
|
|
341
364
|
const loadClientStatus = useCallback(() => {
|
|
342
365
|
const ticket = clientGate.next()
|
|
343
366
|
fetch(CLIENT_STATUS_ROUTE)
|
|
@@ -347,9 +370,7 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
347
370
|
})
|
|
348
371
|
.then((body) => {
|
|
349
372
|
if (!clientGate.isLatest(ticket)) return
|
|
350
|
-
|
|
351
|
-
setClientLoad({ state: 'ready', view })
|
|
352
|
-
followServerDeviceName(view)
|
|
373
|
+
setClientLoad({ state: 'ready', view: deriveClientStatusView(body) })
|
|
353
374
|
})
|
|
354
375
|
.catch(() => {
|
|
355
376
|
if (!clientGate.isLatest(ticket)) return
|
|
@@ -357,18 +378,39 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
357
378
|
? { state: 'stale', view: prev.view }
|
|
358
379
|
: { state: 'error' })
|
|
359
380
|
})
|
|
360
|
-
}, [clientGate
|
|
381
|
+
}, [clientGate])
|
|
361
382
|
|
|
362
383
|
// The client group's rendered view, hoisted for the countdown ticker and
|
|
363
384
|
// the retry-follow refresh below (T43).
|
|
364
385
|
const clientView = clientLoad.state === 'ready' || clientLoad.state === 'stale' ? clientLoad.view : undefined
|
|
386
|
+
// MS-7: the paired-server pool, in pairing order (empty on a legacy body),
|
|
387
|
+
// and the cap the counters and the red alert read.
|
|
388
|
+
const serverRows: readonly ClientServerView[] = clientView?.servers ?? []
|
|
389
|
+
const maxServers = clientView?.maxServers ?? MAX_PAIRED_SERVERS
|
|
365
390
|
// RD-3: the remote drop guard's browser-side counters, read at render
|
|
366
391
|
// time — no snapshot state of its own: the initial mount, the 15s
|
|
367
392
|
// connected-status poll and the countdown ticker all re-render this page,
|
|
368
393
|
// and each render takes the values as they are right then.
|
|
369
394
|
const dropDiag = remoteDropDiagnostics()
|
|
370
|
-
|
|
395
|
+
// MS-7: the SOONEST armed retry across the top level and every pool row —
|
|
396
|
+
// each offline row counts down in its own badge, and the follow refresh
|
|
397
|
+
// below fires after whichever retry comes due first.
|
|
398
|
+
const clientRetryAt = [clientView?.nextRetryAt, ...serverRows.map((server) => server.nextRetryAt)]
|
|
399
|
+
.reduce<number | undefined>((soonest, at) => (at !== undefined && (soonest === undefined || at < soonest) ? at : soonest), undefined)
|
|
371
400
|
const clientCountingDown = clientRetryAt !== undefined
|
|
401
|
+
// MS-7 diagnostics gates: the per-server group, the pre-pool fallback to
|
|
402
|
+
// the top-level mirrors, and whether the section has any line at all.
|
|
403
|
+
const legacyTopDiag = clientView !== undefined && serverRows.length === 0
|
|
404
|
+
const hasServerDiag = serverRows.some(serverHasDiag)
|
|
405
|
+
const diagVisible = clientView !== undefined && (
|
|
406
|
+
clientView.intercept !== undefined
|
|
407
|
+
|| clientView.pluginRouteIntercept !== undefined
|
|
408
|
+
|| clientView.uploadProbe !== undefined
|
|
409
|
+
|| dropDiag.installed
|
|
410
|
+
|| dropDiag.notInstalledReason !== undefined
|
|
411
|
+
|| hasServerDiag
|
|
412
|
+
|| (legacyTopDiag && (clientView.lastError !== '' || clientView.compat !== undefined || (clientView.pluginBridge?.length ?? 0) > 0))
|
|
413
|
+
)
|
|
372
414
|
|
|
373
415
|
// Offline with a pending retry: refresh the status shortly after the
|
|
374
416
|
// retry comes due, so a server that came back flips the line to connected
|
|
@@ -670,24 +712,30 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
670
712
|
}, [pairUrlTouched, pairUrl, rowServerUrl])
|
|
671
713
|
|
|
672
714
|
const codeNormalized = normalizePairingCode(pairCode)
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
|
|
715
|
+
// MS-7: the whole gate is the MS-6 pure truth table — an address and a
|
|
716
|
+
// complete code, a quiet page, a valid device-name draft (T60: a red-boxed
|
|
717
|
+
// name would silently pair under the fallback copy), and the 8-server cap,
|
|
718
|
+
// which blocks only a NEW address: re-entering an already-paired one
|
|
719
|
+
// re-pairs it in place, at 8/8 too.
|
|
720
|
+
const claimVerdict = canSubmitClientPairing({
|
|
721
|
+
pairUrl,
|
|
722
|
+
pairCode: codeNormalized,
|
|
723
|
+
servers: serverRows,
|
|
724
|
+
maxServers,
|
|
725
|
+
busy: claimBusy,
|
|
726
|
+
deviceNameInvalid: form.serverName.invalid,
|
|
727
|
+
})
|
|
728
|
+
const claimAllowed = form.available && form.writable && claimVerdict.allowed
|
|
678
729
|
|
|
679
730
|
const runClaim = async (): Promise<void> => {
|
|
680
731
|
setClaimBusy(true)
|
|
681
732
|
setClaimFail(null)
|
|
682
|
-
setClaimWriteFailed(false)
|
|
683
733
|
// UX-1: captured BEFORE the pairing — whether the page ran as client, and
|
|
684
734
|
// whether the backend's stream interceptor was already active per the
|
|
685
735
|
// last client/status diagnostics.
|
|
686
736
|
const wasEffectiveClient = clientRole
|
|
687
737
|
const streamInterceptAlreadyActive = clientView?.intercept?.installed === true
|
|
688
|
-
// T60:
|
|
689
|
-
// ({@link claimDeviceNameOf}), sent to the server AND written into the
|
|
690
|
-
// row when the claim lands.
|
|
738
|
+
// T60: the name registers trimmed and legal ({@link claimDeviceNameOf}).
|
|
691
739
|
const claimName = claimDeviceNameOf(form.serverName.text, t('settings.client.deviceNameDefault'))
|
|
692
740
|
try {
|
|
693
741
|
const res = await fetch(CLIENT_CLAIM_ROUTE, {
|
|
@@ -695,8 +743,7 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
695
743
|
headers: { 'Content-Type': 'application/json' },
|
|
696
744
|
// T57: the name registers under the shared 设备名称 field's current
|
|
697
745
|
// displayed value (a staged draft counts), falling back to the
|
|
698
|
-
// default copy when the field is empty
|
|
699
|
-
// admin baseline (T55), so an unstored name displays empty here.
|
|
746
|
+
// default copy when the field is empty.
|
|
700
747
|
body: JSON.stringify({
|
|
701
748
|
serverUrl: pairUrl.trim(),
|
|
702
749
|
code: codeNormalized,
|
|
@@ -704,49 +751,30 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
704
751
|
}),
|
|
705
752
|
})
|
|
706
753
|
const body = await res.json().catch(() => ({})) as ClaimRouteBody
|
|
707
|
-
if (res.ok && body.ok === true
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
|
|
713
|
-
// actually sent (T60) — either way the row now matches the server.
|
|
714
|
-
const urlToWrite = typeof body.serverUrl === 'string' && body.serverUrl !== '' ? body.serverUrl : pairUrl.trim()
|
|
715
|
-
// T59-fix: the echo is judged by the SAME rules as our own draft
|
|
716
|
-
// (claimDeviceNameOf trims and validates) — a server answer we
|
|
717
|
-
// could not store falls back to the name we actually sent.
|
|
718
|
-
const echoed = typeof body.deviceName === 'string' && body.deviceName !== '' ? claimDeviceNameOf(body.deviceName, claimName) : claimName
|
|
719
|
-
const landed = await config.writeClientPairing(urlToWrite, body.token, echoed)
|
|
720
|
-
if (landed && shouldReloadAfterClientActivation({
|
|
754
|
+
if (res.ok && body.ok === true) {
|
|
755
|
+
// MS-5/MS-7: the backend landed the pairing straight into
|
|
756
|
+
// zen-remote-state.json (the token never reaches the browser), so
|
|
757
|
+
// there is nothing to write here — only the FIRST activation still
|
|
758
|
+
// reloads, so the page's resident global streams reopen wrapped.
|
|
759
|
+
if (shouldReloadAfterClientActivation({
|
|
721
760
|
wasEffectiveClient,
|
|
722
761
|
nowClientRole: true,
|
|
723
762
|
savedOrPaired: true,
|
|
724
763
|
streamInterceptAlreadyActive,
|
|
725
764
|
})) {
|
|
726
|
-
// UX-1: FIRST pairing on a page whose resident global streams were
|
|
727
|
-
// opened unwrapped (or with the interceptor inactive) — wait out the
|
|
728
|
-
// backend's row-reload window, then reload so the streams reopen
|
|
729
|
-
// wrapped and the server's sessions enter the sidebar.
|
|
730
765
|
setClaimBusy(false)
|
|
731
766
|
void awaitClientStatusReady().then(() => window.location.reload())
|
|
732
767
|
return
|
|
733
768
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
setPairCode('')
|
|
739
|
-
setPairUrl(urlToWrite)
|
|
740
|
-
setUnpairDone(false)
|
|
741
|
-
loadClientStatus()
|
|
742
|
-
} else {
|
|
743
|
-
setClaimWriteFailed(true)
|
|
744
|
-
}
|
|
769
|
+
setPairCode('')
|
|
770
|
+
if (typeof body.serverUrl === 'string' && body.serverUrl !== '') setPairUrl(body.serverUrl)
|
|
771
|
+
setUnpairDone(false)
|
|
772
|
+
loadClientStatus()
|
|
745
773
|
} else {
|
|
746
|
-
// Every other shape is a failure (T16-fix)
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
774
|
+
// Every other shape is a failure (T16-fix), shown with the classified
|
|
775
|
+
// code when the route sent one (MS-5 adds `limit-reached`). The
|
|
776
|
+
// body's `message` is ignored: the wording is this build's
|
|
777
|
+
// dictionary (pairFailText).
|
|
750
778
|
const fail: PairFail = { code: typeof body.code === 'string' && body.code !== '' ? body.code : 'unexpected' }
|
|
751
779
|
if (typeof body.retryAfterMs === 'number') fail.retryAfterMs = body.retryAfterMs
|
|
752
780
|
setClaimFail(fail)
|
|
@@ -768,6 +796,67 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
768
796
|
}
|
|
769
797
|
}
|
|
770
798
|
|
|
799
|
+
/** MS-7: one POST of the pool list's row actions — whether it answered ok. */
|
|
800
|
+
const postClientRoute = async (route: string, payload: string): Promise<boolean> => {
|
|
801
|
+
try {
|
|
802
|
+
const res = await fetch(route, {
|
|
803
|
+
method: 'POST',
|
|
804
|
+
headers: { 'Content-Type': 'application/json' },
|
|
805
|
+
body: payload,
|
|
806
|
+
})
|
|
807
|
+
return res.ok
|
|
808
|
+
} catch {
|
|
809
|
+
return false
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// MS-7 立即重连 for ONE server: refresh now (the state is at least
|
|
814
|
+
// "connecting") and once more after the attempt can have settled — the
|
|
815
|
+
// T43 top-level button's cadence, per entry.
|
|
816
|
+
const runEntryReconnect = async (entryId: string): Promise<void> => {
|
|
817
|
+
setEntryBusy(entryId)
|
|
818
|
+
setEntryActionFailed(false)
|
|
819
|
+
if (await postClientRoute(CLIENT_RECONNECT_ROUTE, JSON.stringify({ entryId }))) {
|
|
820
|
+
loadClientStatus()
|
|
821
|
+
refreshTimers.current.push(window.setTimeout(() => { loadClientStatus() }, 1500))
|
|
822
|
+
} else {
|
|
823
|
+
setEntryActionFailed(true)
|
|
824
|
+
}
|
|
825
|
+
setEntryBusy(null)
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// MS-7 移除: forget one paired server (the server's own device record is
|
|
829
|
+
// untouched — the confirm copy says so).
|
|
830
|
+
const runUnpairEntry = async (entryId: string): Promise<void> => {
|
|
831
|
+
if (!window.confirm(t('settings.client.unpairServerConfirm'))) return
|
|
832
|
+
setEntryBusy(entryId)
|
|
833
|
+
setEntryActionFailed(false)
|
|
834
|
+
if (await postClientRoute(CLIENT_UNPAIR_ROUTE, JSON.stringify({ entryId }))) loadClientStatus()
|
|
835
|
+
else setEntryActionFailed(true)
|
|
836
|
+
setEntryBusy(null)
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// MS-7 全部移除: forget every paired server at once.
|
|
840
|
+
const runUnpairAllServers = async (): Promise<void> => {
|
|
841
|
+
if (!window.confirm(t('settings.client.unpairAllServersConfirm'))) return
|
|
842
|
+
setEntryBusy('all')
|
|
843
|
+
setEntryActionFailed(false)
|
|
844
|
+
if (await postClientRoute(CLIENT_UNPAIR_ROUTE, JSON.stringify({ all: true }))) loadClientStatus()
|
|
845
|
+
else setEntryActionFailed(true)
|
|
846
|
+
setEntryBusy(null)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// MS-7 重新配对: point the pairing form at this server — its address in the
|
|
850
|
+
// box (an already-paired address passes the cap gate), a blank code, the
|
|
851
|
+
// focus on the code box so the user only types the fresh code.
|
|
852
|
+
const repurposeServer = (server: ClientServerView): void => {
|
|
853
|
+
setPairUrl(server.serverUrl)
|
|
854
|
+
setPairUrlTouched(true)
|
|
855
|
+
setPairCode('')
|
|
856
|
+
setClaimFail(null)
|
|
857
|
+
pairCodeRef.current?.focus()
|
|
858
|
+
}
|
|
859
|
+
|
|
771
860
|
const formDisabled = !form.available || !form.writable
|
|
772
861
|
|
|
773
862
|
// T17: saving any restart-required field (the role among them) moves the
|
|
@@ -867,29 +956,27 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
867
956
|
)
|
|
868
957
|
|
|
869
958
|
// Boolean control (the shared fields have no checkbox): label row + switch.
|
|
870
|
-
const bool = (state: SettingsFieldState, field: string, label: string, hint: string) => (
|
|
871
|
-
<div className="zr-settings-field" key={field}>
|
|
959
|
+
const bool = (state: SettingsFieldState, field: string, label: string, hint: string, parentDisabled = false) => (
|
|
960
|
+
<div className="zr-settings-field" key={field} data-disabled={parentDisabled ? 'true' : undefined}>
|
|
872
961
|
<div className="zr-settings-head">
|
|
873
|
-
<
|
|
962
|
+
<span className="zr-settings-head-title">{label}</span>
|
|
874
963
|
{state.overridden && <span className="zr-settings-badge">{t('settings.overridden')}</span>}
|
|
875
964
|
{state.overridden && (
|
|
876
|
-
<button type="button" className="zr-settings-reset" disabled={formDisabled || state.locked} onClick={() => { config.resetField(field) }}>{t('settings.reset')}</button>
|
|
965
|
+
<button type="button" className="zr-settings-reset" disabled={formDisabled || state.locked || parentDisabled} onClick={() => { config.resetField(field) }}>{t('settings.reset')}</button>
|
|
877
966
|
)}
|
|
878
|
-
<
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
onChange={(e) => { config.stage(field, e.currentTarget.checked ? 'true' : 'false') }}
|
|
885
|
-
/>
|
|
886
|
-
</span>
|
|
967
|
+
<Switch
|
|
968
|
+
disabled={formDisabled || state.locked || parentDisabled}
|
|
969
|
+
checked={!parentDisabled && state.text === 'true'}
|
|
970
|
+
label={label}
|
|
971
|
+
onChange={(checked) => { config.stage(field, checked ? 'true' : 'false') }}
|
|
972
|
+
/>
|
|
887
973
|
</div>
|
|
888
974
|
<p className="zr-settings-hint">{hint}</p>
|
|
889
975
|
{clearPreviewNote(state)}
|
|
890
976
|
{annotations(field)}
|
|
891
977
|
</div>
|
|
892
978
|
)
|
|
979
|
+
const pushOff = form.pushEnabled.text !== 'true'
|
|
893
980
|
|
|
894
981
|
const deviceRow = (device: SettingsDeviceView) => (
|
|
895
982
|
<div className="zr-settings-device" key={device.id}>
|
|
@@ -1064,9 +1151,14 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1064
1151
|
{text(form.vapidSubject, 'vapidSubject', t('settings.fieldVapidSubject'), t('settings.fieldVapidSubjectHint'), '')}
|
|
1065
1152
|
|
|
1066
1153
|
<h3 className="zr-settings-card-title">{t('settings.pushTitle')}</h3>
|
|
1067
|
-
{
|
|
1068
|
-
|
|
1069
|
-
|
|
1154
|
+
{/* P55-1: the master switch leads the group; when off, the
|
|
1155
|
+
dependent push switches below are disabled and visually
|
|
1156
|
+
switched off, and apply() wires nothing until it is back on. */}
|
|
1157
|
+
{bool(form.pushEnabled, 'pushEnabled', t('settings.fieldPushEnabled'), t('settings.fieldPushEnabledHint'))}
|
|
1158
|
+
{bool(form.pushSessionName, 'pushSessionName', t('settings.fieldPushSessionName'), t('settings.fieldPushSessionNameHint'), pushOff)}
|
|
1159
|
+
{bool(form.pushSummary, 'pushSummary', t('settings.fieldPushSummary'), t('settings.fieldPushSummaryHint'), pushOff)}
|
|
1160
|
+
{bool(form.pushTurnEnd, 'pushTurnEnd', t('settings.fieldPushTurnEnd'), t('settings.fieldPushTurnEndHint'), pushOff)}
|
|
1161
|
+
{bool(form.pushTool, 'pushTool', t('settings.fieldPushTool'), t('settings.fieldPushToolHint'), pushOff)}
|
|
1070
1162
|
{text(form.pushDebounceMs, 'pushDebounceMs', t('settings.fieldPushDebounceMs'), t('settings.fieldPushDebounceMsHint'), t('settings.invalidNonNegativeInt'), true)}
|
|
1071
1163
|
{select(form.lang, 'lang', t('settings.fieldLang'), t('settings.fieldLangHint'), ['auto', 'zh', 'en'], (option) => (
|
|
1072
1164
|
option === 'auto' ? t('settings.langAuto') : option === 'zh' ? t('settings.langZh') : t('settings.langEn')
|
|
@@ -1209,7 +1301,19 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1209
1301
|
operations, so they render even while the namespace is read-only. */}
|
|
1210
1302
|
{clientRole && (
|
|
1211
1303
|
<div className="zr-settings-card">
|
|
1212
|
-
<h3 className="zr-settings-card-title">
|
|
1304
|
+
<h3 className="zr-settings-card-title">
|
|
1305
|
+
{t('settings.client.connectTitle')}
|
|
1306
|
+
<span className="zr-settings-badge" data-invalid={clientView?.limitReached === true ? 'true' : undefined}>{t('settings.client.poolCount', { count: serverRows.length, max: maxServers })}</span>
|
|
1307
|
+
</h3>
|
|
1308
|
+
<p className="zr-settings-hint" style={{ paddingTop: 4 }}>{t('settings.client.poolCapNote', { max: maxServers })}</p>
|
|
1309
|
+
{/* MS-7: the full pool's alert — loud red bold on purpose: a new
|
|
1310
|
+
address is refused until a server is removed, while re-entering
|
|
1311
|
+
an already-paired address still re-pairs it in place. */}
|
|
1312
|
+
{clientView?.limitReached === true && (
|
|
1313
|
+
<p className="zr-settings-limit-alert" data-invalid="true">
|
|
1314
|
+
{t('settings.client.limitReached', { count: serverRows.length, max: maxServers })}
|
|
1315
|
+
</p>
|
|
1316
|
+
)}
|
|
1213
1317
|
<div className="zr-settings-field">
|
|
1214
1318
|
<div className="zr-settings-head">
|
|
1215
1319
|
<label htmlFor="zr-settings-client-server">{t('settings.client.serverUrl')}</label>
|
|
@@ -1231,6 +1335,7 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1231
1335
|
</div>
|
|
1232
1336
|
<input
|
|
1233
1337
|
id="zr-settings-client-code"
|
|
1338
|
+
ref={pairCodeRef}
|
|
1234
1339
|
className="zr-settings-input zr-settings-code-input"
|
|
1235
1340
|
type="text"
|
|
1236
1341
|
autoComplete="off"
|
|
@@ -1252,9 +1357,59 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1252
1357
|
draft is invalid — this line says why. */}
|
|
1253
1358
|
{form.serverName.invalid && <span className="zr-settings-hint" data-invalid="true">{t('settings.client.fixDeviceName')}</span>}
|
|
1254
1359
|
{claimFail !== null && <span className="zr-settings-hint" data-invalid="true">{pairFailText(claimFail, t)}</span>}
|
|
1255
|
-
{claimWriteFailed && <span className="zr-settings-hint" data-invalid="true">{t('settings.client.failWrite')}</span>}
|
|
1256
1360
|
</div>
|
|
1257
1361
|
|
|
1362
|
+
{/* MS-7: the paired main servers, in pairing order — one row per
|
|
1363
|
+
pool entry with its own state badge and actions. The cap only
|
|
1364
|
+
ever gates NEW addresses, so 重新配对 stays usable at 8/8. */}
|
|
1365
|
+
<h3 className="zr-settings-card-title">{t('settings.client.serversTitle')}</h3>
|
|
1366
|
+
{serverRows.length === 0 && (
|
|
1367
|
+
<p className="zr-settings-hint" style={{ padding: '8px 0 12px' }}>{t('settings.client.serversEmpty', { max: maxServers })}</p>
|
|
1368
|
+
)}
|
|
1369
|
+
{serverRows.map((server) => {
|
|
1370
|
+
const badge = serverStateBadge(server, now, t)
|
|
1371
|
+
return (
|
|
1372
|
+
<div className="zr-settings-device" key={server.entryId}>
|
|
1373
|
+
<div className="zr-settings-device-head">
|
|
1374
|
+
<span className="zr-settings-device-name">{server.displayName !== '' ? server.displayName : server.serverUrl}</span>
|
|
1375
|
+
<span
|
|
1376
|
+
className="zr-settings-badge"
|
|
1377
|
+
data-live={badge.tone === 'live' ? 'true' : undefined}
|
|
1378
|
+
data-warn={badge.tone === 'warn' ? 'true' : undefined}
|
|
1379
|
+
data-invalid={badge.tone === 'invalid' ? 'true' : undefined}
|
|
1380
|
+
>
|
|
1381
|
+
{badge.text}
|
|
1382
|
+
</span>
|
|
1383
|
+
<span className="zr-settings-hint">{server.serverUrl}</span>
|
|
1384
|
+
</div>
|
|
1385
|
+
<div className="zr-settings-device-actions">
|
|
1386
|
+
<Button
|
|
1387
|
+
variant="ghost"
|
|
1388
|
+
size="sm"
|
|
1389
|
+
disabled={entryBusy !== null || server.nextRetryAt === undefined}
|
|
1390
|
+
onClick={() => { void runEntryReconnect(server.entryId) }}
|
|
1391
|
+
>
|
|
1392
|
+
{entryBusy === server.entryId ? t('settings.client.reconnectBusy') : t('settings.client.reconnect')}
|
|
1393
|
+
</Button>
|
|
1394
|
+
<Button variant="ghost" size="sm" disabled={entryBusy !== null} onClick={() => { repurposeServer(server) }}>
|
|
1395
|
+
{t('settings.client.repair')}
|
|
1396
|
+
</Button>
|
|
1397
|
+
<Button variant="ghost" size="sm" disabled={entryBusy !== null} onClick={() => { void runUnpairEntry(server.entryId) }}>
|
|
1398
|
+
{t('settings.client.serverRemove')}
|
|
1399
|
+
</Button>
|
|
1400
|
+
</div>
|
|
1401
|
+
</div>
|
|
1402
|
+
)
|
|
1403
|
+
})}
|
|
1404
|
+
{entryActionFailed && <p className="zr-settings-hint" data-invalid="true">{t('settings.actionFail')}</p>}
|
|
1405
|
+
{serverRows.length > 0 && (
|
|
1406
|
+
<div className="zr-settings-row" style={{ paddingBottom: 12 }}>
|
|
1407
|
+
<Button variant="ghost" size="sm" disabled={entryBusy !== null} onClick={() => { void runUnpairAllServers() }}>
|
|
1408
|
+
{t('settings.client.unpairAllServers')}
|
|
1409
|
+
</Button>
|
|
1410
|
+
</div>
|
|
1411
|
+
)}
|
|
1412
|
+
|
|
1258
1413
|
<h3 className="zr-settings-card-title">{t('settings.client.statusTitle')}</h3>
|
|
1259
1414
|
<div className="zr-settings-row" style={{ paddingBottom: 4 }}>
|
|
1260
1415
|
<p className="zr-settings-status-line" style={{ flex: 1 }} data-down={clientView !== undefined && (clientView.state === 'unreachable' || clientView.state === 'unexpected' || clientView.state === 'revoked' || clientView.state === 'invalid-url' || clientView.state === 'incompatible') ? 'true' : undefined}>
|
|
@@ -1291,11 +1446,16 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1291
1446
|
groups render only when the response carried their fields —
|
|
1292
1447
|
earlier servers answer neither, and the section itself only
|
|
1293
1448
|
exists when there is at least one line to show. PB-5 adds the
|
|
1294
|
-
bridge rows and the rerouting note on the same presence gate.
|
|
1295
|
-
|
|
1449
|
+
bridge rows and the rerouting note on the same presence gate.
|
|
1450
|
+
MS-7: the interceptor, the attachment-entry guard and the
|
|
1451
|
+
upload redirect are GLOBAL (one per machine); the last error,
|
|
1452
|
+
the fingerprint groups and the bridge verdicts are PER SERVER
|
|
1453
|
+
(the pool group at the bottom). A legacy body without servers[]
|
|
1454
|
+
keeps the top-level mirrors (legacyTopDiag). */}
|
|
1455
|
+
{diagVisible && clientView !== undefined && (
|
|
1296
1456
|
<>
|
|
1297
1457
|
<h3 className="zr-settings-card-title">{t('settings.client.diagTitle')}</h3>
|
|
1298
|
-
{clientView.lastError !== '' && (
|
|
1458
|
+
{legacyTopDiag && clientView.lastError !== '' && (
|
|
1299
1459
|
<p className="zr-settings-hint" data-invalid={clientView.state === 'unreachable' ? 'true' : undefined}>
|
|
1300
1460
|
{t('settings.client.diagLastError', { code: clientView.lastError })}
|
|
1301
1461
|
</p>
|
|
@@ -1396,7 +1556,7 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1396
1556
|
</div>
|
|
1397
1557
|
)
|
|
1398
1558
|
})()}
|
|
1399
|
-
{clientView.compat !== undefined && (
|
|
1559
|
+
{legacyTopDiag && clientView.compat !== undefined && (
|
|
1400
1560
|
<>
|
|
1401
1561
|
{clientView.compat.mismatchedGroups.length > 0 && (
|
|
1402
1562
|
<p className="zr-settings-hint" data-invalid="true">
|
|
@@ -1417,7 +1577,7 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1417
1577
|
)}
|
|
1418
1578
|
</>
|
|
1419
1579
|
)}
|
|
1420
|
-
{(clientView.pluginBridge?.length ?? 0) > 0 && clientView.pluginBridge !== undefined && (
|
|
1580
|
+
{legacyTopDiag && (clientView.pluginBridge?.length ?? 0) > 0 && clientView.pluginBridge !== undefined && (
|
|
1421
1581
|
<div className="zr-settings-field">
|
|
1422
1582
|
<div className="zr-settings-head">
|
|
1423
1583
|
<label>{t('settings.bridge.title')}</label>
|
|
@@ -1437,6 +1597,41 @@ function SettingsSectionPage({ config, shares, t }: SettingsSectionProps) {
|
|
|
1437
1597
|
{t('settings.bridge.interceptOff', { reason: clientView.pluginRouteIntercept.reasons.join('; ') || '—' })}
|
|
1438
1598
|
</p>
|
|
1439
1599
|
)}
|
|
1600
|
+
{hasServerDiag && (
|
|
1601
|
+
<div className="zr-settings-field">
|
|
1602
|
+
<div className="zr-settings-head">
|
|
1603
|
+
<label>{t('settings.client.serverDiagTitle')}</label>
|
|
1604
|
+
</div>
|
|
1605
|
+
{serverRows.filter(serverHasDiag).map((server) => (
|
|
1606
|
+
<div className="zr-settings-device" key={server.entryId}>
|
|
1607
|
+
<span className="zr-settings-device-name">{server.displayName !== '' ? server.displayName : server.serverUrl}</span>
|
|
1608
|
+
{server.lastError !== '' && (
|
|
1609
|
+
<p className="zr-settings-hint" data-invalid={server.state === 'unreachable' ? 'true' : undefined}>
|
|
1610
|
+
{t('settings.client.diagLastError', { code: server.lastError })}
|
|
1611
|
+
</p>
|
|
1612
|
+
)}
|
|
1613
|
+
{server.compat !== undefined && server.compat.mismatchedGroups.length > 0 && (
|
|
1614
|
+
<p className="zr-settings-hint" data-invalid="true">
|
|
1615
|
+
{t('settings.client.diagCompatGroups', { groups: server.compat.mismatchedGroups.join(', ') })}
|
|
1616
|
+
</p>
|
|
1617
|
+
)}
|
|
1618
|
+
{server.compat?.recentCalls.map((failure, index) => (
|
|
1619
|
+
<p key={index} className="zr-settings-hint">
|
|
1620
|
+
{t('settings.client.diagCompatCalls')} · {new Date(failure.time).toLocaleString()} · {failure.method !== '' ? failure.method : '—'} · {failure.code}
|
|
1621
|
+
</p>
|
|
1622
|
+
))}
|
|
1623
|
+
{server.pluginBridge.map((row: PluginBridgeStatusView) => (
|
|
1624
|
+
<p key={row.plugin} className="zr-settings-hint">
|
|
1625
|
+
{t('settings.bridge.title')} · {row.plugin} · {row.localVersion !== '' ? `v${row.localVersion}` : '—'} ·{' '}
|
|
1626
|
+
{row.stateKey === 'settings.bridge.stateVersionMismatch'
|
|
1627
|
+
? t(row.stateKey, { version: row.serverVersion ?? '?' })
|
|
1628
|
+
: t(row.stateKey)}
|
|
1629
|
+
</p>
|
|
1630
|
+
))}
|
|
1631
|
+
</div>
|
|
1632
|
+
))}
|
|
1633
|
+
</div>
|
|
1634
|
+
)}
|
|
1440
1635
|
</>
|
|
1441
1636
|
)}
|
|
1442
1637
|
</div>
|
|
@@ -52,6 +52,9 @@ export const SETTINGS_CSS = `
|
|
|
52
52
|
[data-zen-remote="settings"] .zr-settings-field + .zr-settings-field {
|
|
53
53
|
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
54
54
|
}
|
|
55
|
+
[data-zen-remote="settings"] .zr-settings-field[data-disabled="true"] {
|
|
56
|
+
opacity: 0.5;
|
|
57
|
+
}
|
|
55
58
|
[data-zen-remote="settings"] .zr-settings-head {
|
|
56
59
|
display: flex;
|
|
57
60
|
align-items: center;
|
|
@@ -227,6 +230,21 @@ export const SETTINGS_CSS = `
|
|
|
227
230
|
[data-zen-remote="settings"] .zr-settings-badge[data-live="true"] {
|
|
228
231
|
color: var(--dsw-alias-state-success-primary, #16a34a);
|
|
229
232
|
}
|
|
233
|
+
[data-zen-remote="settings"] .zr-settings-badge[data-invalid="true"] {
|
|
234
|
+
color: var(--dsw-alias-state-error-primary, #d5304f);
|
|
235
|
+
}
|
|
236
|
+
/* MS-7: the full-pool alert must be impossible to miss — red, bold, framed. */
|
|
237
|
+
[data-zen-remote="settings"] .zr-settings-limit-alert,
|
|
238
|
+
[data-zen-remote="settings"] .zr-settings-limit-alert[data-invalid="true"] {
|
|
239
|
+
font-size: 13px;
|
|
240
|
+
font-weight: 700;
|
|
241
|
+
line-height: 1.5;
|
|
242
|
+
color: var(--dsw-alias-state-error-primary, #d5304f);
|
|
243
|
+
border: 1px solid var(--dsw-alias-state-error-primary, #d5304f);
|
|
244
|
+
border-radius: 8px;
|
|
245
|
+
padding: 8px 12px;
|
|
246
|
+
margin: 8px 0 4px;
|
|
247
|
+
}
|
|
230
248
|
[data-zen-remote="settings"] .zr-settings-footer {
|
|
231
249
|
display: flex;
|
|
232
250
|
align-items: center;
|
|
@@ -245,13 +245,27 @@ html[data-zr-remote-session="1"]:not([data-zr-bridge-dsh-better-sidebar="1"]) [d
|
|
|
245
245
|
|
|
246
246
|
KNOWN BOUNDARY (accepted, not fixed): in a remote session, searching the
|
|
247
247
|
model menu and pressing Enter can commit a HIDDEN local model — the
|
|
248
|
-
interceptor refuses it with its usual message.
|
|
248
|
+
interceptor refuses it with its usual message.
|
|
249
|
+
|
|
250
|
+
MS-4, several paired servers: each remote group heading also carries its
|
|
251
|
+
server id (zero-width bits after the mark, virtual-id.ts
|
|
252
|
+
encodeServerIdZeroWidth), and the effect reads the open session's server
|
|
253
|
+
off html[data-zr-remote-server] (effects/remote-session.ts). A remote
|
|
254
|
+
group of a DIFFERENT server is stamped "remote-other": hidden inside a
|
|
255
|
+
remote session (only the open session's own server's models can run
|
|
256
|
+
there) and, like every remote group, outside one. */
|
|
249
257
|
html[data-zr-remote-session="1"] section[role="group"][data-zr-group="local"] {
|
|
250
258
|
display: none !important;
|
|
251
259
|
}
|
|
260
|
+
html[data-zr-remote-session="1"] section[role="group"][data-zr-group="remote-other"] {
|
|
261
|
+
display: none !important;
|
|
262
|
+
}
|
|
252
263
|
html:not([data-zr-remote-session]) section[role="group"][data-zr-group="remote"] {
|
|
253
264
|
display: none !important;
|
|
254
265
|
}
|
|
266
|
+
html:not([data-zr-remote-session]) section[role="group"][data-zr-group="remote-other"] {
|
|
267
|
+
display: none !important;
|
|
268
|
+
}
|
|
255
269
|
|
|
256
270
|
/* ---------- T52-fix: hide the virtual groups in NATIVE <option> pickers ----
|
|
257
271
|
The model menu above is React DOM, but dsh-vision-router's settings render
|