dsh-zen-remote 2.3.5 → 2.3.7
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/AGENTS.md +15 -5
- package/CHANGELOG.md +71 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/interface.md +12 -6
- package/docs/plugin-bridge.md +2 -2
- package/docs/remote-access.en.md +9 -3
- package/docs/remote-access.md +9 -3
- package/lan-gate.mjs +7 -0
- package/lib/adapters/dsh-better-sidebar/index.js +31 -2
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/client-pairing.js +10 -0
- package/lib/client-pairing.js.map +1 -1
- package/lib/client-routes.js +183 -94
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +589 -97
- package/lib/debug-redact.js +253 -0
- package/lib/debug-redact.js.map +1 -0
- package/lib/debug-report.js +60 -0
- package/lib/debug-report.js.map +1 -0
- package/lib/fetch-route-intercept.js +23 -0
- package/lib/fetch-route-intercept.js.map +1 -1
- package/lib/index.js +306 -45
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +43 -1
- package/lib/intercept.js.map +1 -1
- package/lib/log-ring.js +76 -0
- package/lib/log-ring.js.map +1 -0
- package/lib/plugin-adapter-core.js +30 -1
- package/lib/plugin-adapter-core.js.map +1 -1
- package/lib/plugin-route-intercept.js +506 -11
- package/lib/plugin-route-intercept.js.map +1 -1
- package/lib/plugin-tunnel.js +210 -18
- package/lib/plugin-tunnel.js.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +5 -2
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/client/MobileSessionInfo.d.ts.map +1 -1
- package/lib/types/client/effects/remote-drop.d.ts +20 -8
- package/lib/types/client/effects/remote-drop.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +10 -1
- 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/styles/content.css.d.ts +1 -1
- package/lib/types/client/styles/content.css.d.ts.map +1 -1
- package/lib/types/client/styles/layout.css.d.ts +1 -1
- package/lib/types/client/styles/layout.css.d.ts.map +1 -1
- package/lib/types/client/styles/misc.css.d.ts +1 -1
- package/lib/types/client/styles/misc.css.d.ts.map +1 -1
- package/lib/types/client-data/remote-drop.d.ts +64 -0
- package/lib/types/client-data/remote-drop.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +3 -0
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-pairing.d.ts +8 -0
- package/lib/types/client-pairing.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +23 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/debug-redact.d.ts +68 -0
- package/lib/types/debug-redact.d.ts.map +1 -0
- package/lib/types/debug-report.d.ts +58 -0
- package/lib/types/debug-report.d.ts.map +1 -0
- package/lib/types/fetch-route-intercept.d.ts +26 -0
- package/lib/types/fetch-route-intercept.d.ts.map +1 -1
- package/lib/types/index.d.ts +7 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts +6 -0
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/log-ring.d.ts +57 -0
- package/lib/types/log-ring.d.ts.map +1 -0
- package/lib/types/plugin-adapter-core.d.ts.map +1 -1
- package/lib/types/plugin-route-intercept.d.ts +8 -0
- package/lib/types/plugin-route-intercept.d.ts.map +1 -1
- package/lib/types/plugin-tunnel.d.ts +37 -2
- package/lib/types/plugin-tunnel.d.ts.map +1 -1
- package/package.json +3 -2
- package/pwa/app.css +13 -2
- package/scripts/check-debug-report.mjs +277 -0
- package/scripts/check-info-sheet-stacking.mjs +85 -0
- package/scripts/check-remote-drop.mjs +130 -0
- package/scripts/check-tablet-safe-area.mjs +49 -0
- package/src/adapters/dsh-better-sidebar/index.ts +31 -2
- package/src/client/MobileSessionInfo.tsx +29 -2
- package/src/client/effects/remote-drop.ts +271 -24
- package/src/client/locales.ts +24 -4
- package/src/client/settings/SettingsSection.tsx +120 -3
- package/src/client/styles/content.css.ts +44 -14
- package/src/client/styles/layout.css.ts +9 -3
- package/src/client/styles/misc.css.ts +38 -0
- package/src/client-data/remote-drop.ts +103 -0
- package/src/client-data/settings-form.ts +3 -0
- package/src/client-pairing.ts +11 -0
- package/src/client-routes.ts +196 -90
- package/src/debug-redact.ts +290 -0
- package/src/debug-report.ts +93 -0
- package/src/fetch-route-intercept.ts +29 -0
- package/src/index.ts +295 -43
- package/src/intercept.ts +46 -0
- package/src/log-ring.ts +101 -0
- package/src/plugin-adapter-core.ts +29 -2
- package/src/plugin-route-intercept.ts +516 -12
- package/src/plugin-tunnel.ts +230 -20
package/src/index.ts
CHANGED
|
@@ -59,7 +59,7 @@ import { runSelfCheck, installIntercept } from './intercept.js'
|
|
|
59
59
|
import type { CurrentSessionRead, InterceptDiagnostics, InterceptHandle, SessionInventorySlot } from './intercept.js'
|
|
60
60
|
import { checkGatewayShape } from './intercept-shape.js'
|
|
61
61
|
import type { GatewayShapeCheck } from './intercept-shape.js'
|
|
62
|
-
import { checkFetchRouteShape, installFetchRouteIntercept } from './fetch-route-intercept.js'
|
|
62
|
+
import { checkFetchRouteShape, installFetchRouteIntercept, shouldFallbackInstall } from './fetch-route-intercept.js'
|
|
63
63
|
import type { FetchRouteInterceptDiagnostics, FetchRouteInterceptHandle, FetchRouteShapeCheck } from './fetch-route-intercept.js'
|
|
64
64
|
import { checkWebServerShape, installPluginRouteIntercept } from './plugin-route-intercept.js'
|
|
65
65
|
import type { PluginRouteInterceptDiagnostics, PluginRouteInterceptHandle, WebServerShapeCheck } from './plugin-route-intercept.js'
|
|
@@ -87,6 +87,7 @@ import { createVerificationManager } from './wizard/verify.js'
|
|
|
87
87
|
import type { BridgeDeclaration, BridgeRegistry, BridgeStatus, ServerPluginEntry, ZenRemoteService } from './plugin-bridge.js'
|
|
88
88
|
import { createPluginLoopback } from './plugin-loopback.js'
|
|
89
89
|
import { createPluginTunnel } from './plugin-tunnel.js'
|
|
90
|
+
import { createLogRing, createPluginLog } from './log-ring.js'
|
|
90
91
|
import { DEFAULT_MAX_UPLOAD_BYTES } from './upload-limits.js'
|
|
91
92
|
|
|
92
93
|
// The plugin bridge's public types (PB-1), re-exported so another plugin's
|
|
@@ -112,6 +113,19 @@ import * as push from '../dsh-push.mjs'
|
|
|
112
113
|
/** Exact route the phone composer POSTs one file body to. */
|
|
113
114
|
export const UPLOAD_ROUTE = '/_dsh/mobile-nav/upload'
|
|
114
115
|
|
|
116
|
+
/**
|
|
117
|
+
* This package's own version, read once off the package root (`../package.json`
|
|
118
|
+
* resolves there both from src/ under the type-stripping scripts and from the
|
|
119
|
+
* built lib/ in production). The debug report's `_meta` plate carries it —
|
|
120
|
+
* issue triage starts at "which build is this" — and the row-apply log line
|
|
121
|
+
* stamps it. Unreadable file degrades to `'unknown'`, never throws.
|
|
122
|
+
*/
|
|
123
|
+
let pluginVersion = 'unknown'
|
|
124
|
+
try {
|
|
125
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version?: unknown }
|
|
126
|
+
if (typeof pkg?.version === 'string' && pkg.version !== '') pluginVersion = pkg.version
|
|
127
|
+
} catch { /* keep 'unknown' */ }
|
|
128
|
+
|
|
115
129
|
/**
|
|
116
130
|
* The one piece of cordis the typert interception needs at RUNTIME: the
|
|
117
131
|
* registered symbol under which every cordis proxy answers with its raw
|
|
@@ -457,6 +471,12 @@ async function writeUpload(
|
|
|
457
471
|
* request must ask the connection service itself, its refusal relayed
|
|
458
472
|
* verbatim — same first wall as the admin and client routes. Optional only
|
|
459
473
|
* for the direct-drive check scripts; production always passes it.
|
|
474
|
+
* @param warn - where the rejection line goes. apply() passes its double-
|
|
475
|
+
* writer (context logger + the CU-1 log ring), so a rejected upload — the
|
|
476
|
+
* very path issue #49 is triaged on — shows up in the debug report. The
|
|
477
|
+
* ring is per-apply on purpose (a row reload clears it), which is why it
|
|
478
|
+
* arrives as a parameter instead of a module singleton. Defaults to the
|
|
479
|
+
* context logger for the direct-drive check scripts.
|
|
460
480
|
*/
|
|
461
481
|
export async function handleUpload(
|
|
462
482
|
ctx: Context,
|
|
@@ -464,6 +484,7 @@ export async function handleUpload(
|
|
|
464
484
|
req: IncomingMessage,
|
|
465
485
|
res: ServerResponse,
|
|
466
486
|
admit?: (req: IncomingMessage) => AdminAdmission,
|
|
487
|
+
warn?: (format: string, ...args: unknown[]) => void,
|
|
467
488
|
): Promise<void> {
|
|
468
489
|
if (admit !== undefined) {
|
|
469
490
|
const admission = admit(req)
|
|
@@ -502,7 +523,8 @@ export async function handleUpload(
|
|
|
502
523
|
} catch (error) {
|
|
503
524
|
const status = error instanceof UploadError ? error.status : 400
|
|
504
525
|
const code = error instanceof UploadError ? error.code : 'upload-rejected'
|
|
505
|
-
|
|
526
|
+
if (warn !== undefined) warn('dsh-mobile-nav upload rejected: %s', message(error))
|
|
527
|
+
else ctx.logger.warn('dsh-mobile-nav upload rejected: %s', message(error))
|
|
506
528
|
responseJson(res, status, { ok: false, error: { code, message: message(error) } })
|
|
507
529
|
}
|
|
508
530
|
}
|
|
@@ -529,7 +551,20 @@ export async function handleUpload(
|
|
|
529
551
|
* request.
|
|
530
552
|
*/
|
|
531
553
|
export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
554
|
+
// CU-1: the plugin's own log ring (SPEC 方案 C) — rebuilt per apply, so a
|
|
555
|
+
// row reload clears it (expected: the report describes THIS process
|
|
556
|
+
// generation). `logWarn` / `logInfo` are the branch-shared double-writers:
|
|
557
|
+
// every line below reaches the context logger exactly as before and the
|
|
558
|
+
// ring besides, so the debug report (CU-2) can answer "what did the plugin
|
|
559
|
+
// actually log" without a log file (the desktop app writes none).
|
|
560
|
+
const ring = createLogRing()
|
|
561
|
+
const pluginLog = createPluginLog(ring, ctx.logger as Parameters<typeof createPluginLog>[1])
|
|
562
|
+
const logWarn = pluginLog.warn
|
|
563
|
+
const logInfo = pluginLog.info
|
|
532
564
|
const effective = resolveConfig(config, readFileConfig(), process.env)
|
|
565
|
+
// Lifecycle: the row came up. Role and both versions are the first things
|
|
566
|
+
// an issue report needs.
|
|
567
|
+
ring.push('info', 'row apply: role=%s plugin=%s dsh=%s', effective.values.role, pluginVersion, resolveDshVersion())
|
|
533
568
|
// RD-6 probe fact: whether the fetch-route inject callback (below, in the
|
|
534
569
|
// client-half wiring) ever fired. On a composition where `connection` or
|
|
535
570
|
// `fileUploads` never becomes available the callback stays silently
|
|
@@ -537,30 +572,49 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
537
572
|
// diagnostic stays undefined — this flag is the one honest signal of that
|
|
538
573
|
// state. Declared this early because probes on BOTH role branches read it.
|
|
539
574
|
let fetchRouteInjectSeen = false
|
|
575
|
+
/** BU-1: how the fetch-route wrap got installed — `'inject'` via the
|
|
576
|
+
* primary `ctx.inject` callback, `'fallback'` via the bounded lazy timer
|
|
577
|
+
* below, `undefined` while not installed. Read live by the service probe. */
|
|
578
|
+
let fetchRouteInstalledVia: 'inject' | 'fallback' | undefined
|
|
579
|
+
/** BU-1: ISO timestamp of the moment a fallback attempt FAILED (shape
|
|
580
|
+
* refused, original unresolvable) — the tick stops after one, so this is
|
|
581
|
+
* also "the fallback gave up here". An install success does not set it. */
|
|
582
|
+
let fetchRouteFallbackTriedAt: string | undefined
|
|
540
583
|
/** The client branch's fetch-route diagnostics getter, assigned once that
|
|
541
584
|
* branch declares it; the probe reads it lazily through this slot so both
|
|
542
585
|
* role mounts can share one probe factory. */
|
|
543
586
|
let fetchRouteDiagnosticsRef: (() => unknown) | undefined
|
|
544
587
|
/** RD-6: build the lazy service probe against a context whose own inject
|
|
545
588
|
* already resolved (the client-routes mounts qualify on both roles). One
|
|
546
|
-
* reflection per process
|
|
547
|
-
*
|
|
589
|
+
* reflection per process for the SERVICE PRESENCE facts (below); the
|
|
590
|
+
* fetch-route facts are re-read live on every call — they change while the
|
|
591
|
+
* page is open (the inject may fire, or the fallback may step in, long
|
|
592
|
+
* after the first probe). */
|
|
548
593
|
const uploadServiceProbe = (probeCtx: Context): (() => Record<string, unknown>) => {
|
|
549
|
-
let cached:
|
|
594
|
+
let cached: {
|
|
595
|
+
connectionService: string
|
|
596
|
+
fileUploadsService: string
|
|
597
|
+
connectionShape?: FetchRouteShapeCheck
|
|
598
|
+
} | undefined
|
|
550
599
|
return () => {
|
|
551
|
-
if (cached
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
600
|
+
if (cached === undefined) {
|
|
601
|
+
const reflect = (probeCtx as Context & { reflect?: { get(name: string): unknown } }).reflect
|
|
602
|
+
const connection = reflect?.get('connection')
|
|
603
|
+
const fileUploads = reflect?.get('fileUploads')
|
|
604
|
+
cached = {
|
|
605
|
+
connectionService: connection === undefined ? 'absent' : 'present',
|
|
606
|
+
fileUploadsService: fileUploads === undefined ? 'absent' : 'present',
|
|
607
|
+
...(connection === undefined ? {} : { connectionShape: checkFetchRouteShape(connection) }),
|
|
608
|
+
}
|
|
609
|
+
}
|
|
555
610
|
const fetchRoute = fetchRouteDiagnosticsRef?.()
|
|
556
|
-
|
|
611
|
+
return {
|
|
557
612
|
uploadInjectSeen: fetchRouteInjectSeen,
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
...
|
|
613
|
+
installedVia: fetchRouteInstalledVia,
|
|
614
|
+
...(fetchRouteFallbackTriedAt === undefined ? {} : { fallbackTriedAt: fetchRouteFallbackTriedAt }),
|
|
615
|
+
...cached,
|
|
561
616
|
...(fetchRoute === undefined ? {} : { fetchRoute }),
|
|
562
617
|
}
|
|
563
|
-
return cached
|
|
564
618
|
}
|
|
565
619
|
}
|
|
566
620
|
// The plugin bridge's zenRemote service (PB-1), provided on BOTH roles
|
|
@@ -580,7 +634,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
580
634
|
// failing the row over — the bridge stays off, everything else keeps
|
|
581
635
|
// working — so it degrades to a warning like the session feeds do.
|
|
582
636
|
const registry = createBridgeRegistry({
|
|
583
|
-
warn: (text) => {
|
|
637
|
+
warn: (text) => { logWarn('dsh-zen-remote plugin bridge: %s', text) },
|
|
584
638
|
})
|
|
585
639
|
bridgeRegistry = registry
|
|
586
640
|
try {
|
|
@@ -589,7 +643,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
589
643
|
}
|
|
590
644
|
ctx.provide('zenRemote', service)
|
|
591
645
|
} catch (error) {
|
|
592
|
-
|
|
646
|
+
logWarn('dsh-zen-remote: cannot provide the "zenRemote" service: %s', message(error))
|
|
593
647
|
}
|
|
594
648
|
ctx.effect(() => () => {
|
|
595
649
|
if (bridgeRegistry === registry) bridgeRegistry = undefined
|
|
@@ -613,7 +667,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
613
667
|
// halves.
|
|
614
668
|
const adapters: NormalizedAdapter[] = []
|
|
615
669
|
{
|
|
616
|
-
const adapterWarn = (text: string): void => {
|
|
670
|
+
const adapterWarn = (text: string): void => { logWarn('dsh-zen-remote adapter: %s', text) }
|
|
617
671
|
for (const adapter of ADAPTERS) {
|
|
618
672
|
try {
|
|
619
673
|
const holder = ctx as unknown as { get?: (name: string) => unknown; baseUrl?: string }
|
|
@@ -660,15 +714,27 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
660
714
|
registry.registerNormalized(normalized.declaration)
|
|
661
715
|
adapters.push(normalized)
|
|
662
716
|
if (unlisted && surfaceDigest !== undefined) {
|
|
663
|
-
|
|
717
|
+
logInfo('dsh-zen-remote adapter: %s bridged by surface digest %s (version %s unlisted)', normalized.plugin, surfaceDigest, installedVersion)
|
|
664
718
|
} else {
|
|
665
|
-
|
|
719
|
+
logInfo('dsh-zen-remote adapter: %s adapted at version %s', normalized.plugin, installedVersion)
|
|
666
720
|
}
|
|
667
721
|
} catch (error) {
|
|
668
722
|
adapterWarn(`${adapter.plugin} refused: ${error instanceof Error ? error.message : String(error)}`)
|
|
669
723
|
}
|
|
670
724
|
}
|
|
671
725
|
}
|
|
726
|
+
// CU-3: the debug-report route's profile fact — read per call through the
|
|
727
|
+
// reflection layer's store lookup (a composition without profileContext
|
|
728
|
+
// answers undefined, and a throwing read degrades to undefined too).
|
|
729
|
+
const profileName = (): string | undefined => {
|
|
730
|
+
try {
|
|
731
|
+
const holder = ctx as unknown as { get?: (name: string) => unknown }
|
|
732
|
+
const profile = holder.get?.('profileContext') as { name?: unknown } | undefined
|
|
733
|
+
return typeof profile?.name === 'string' && profile.name !== '' ? profile.name : undefined
|
|
734
|
+
} catch {
|
|
735
|
+
return undefined
|
|
736
|
+
}
|
|
737
|
+
}
|
|
672
738
|
if (effective.values.role === 'host') {
|
|
673
739
|
// A row that switched from client to host must not leave a stale relay
|
|
674
740
|
// client where getRelayClient() can find it (T23a-fix).
|
|
@@ -677,7 +743,24 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
677
743
|
// gateway child through LAN_GATE_RELAY_SECRET and kept here for the
|
|
678
744
|
// relay routes below. The push half never sees it.
|
|
679
745
|
const relaySecret = randomBytes(32).toString('hex')
|
|
680
|
-
|
|
746
|
+
// CU-1 lifecycle: the gateway child's spawn/exit, reported back through
|
|
747
|
+
// this hook (lan-gate.mjs reads it off the config object it is handed).
|
|
748
|
+
// The child runs in a subprocess whose console the ring cannot see, so
|
|
749
|
+
// these two lines are the debug report's only record of it. Either call
|
|
750
|
+
// must never break the gateway half — lan-gate.mjs guards its side, we
|
|
751
|
+
// guard ours. The hook rides as a NON-enumerable property: it is wiring,
|
|
752
|
+
// not configuration — the gateway's config stays exactly "resolved values
|
|
753
|
+
// + relaySecret" for every spread, deep compare and serialization (the
|
|
754
|
+
// role-wiring test pins that shape), while a direct read still finds it.
|
|
755
|
+
const gatewayEvent = (event: 'spawn' | 'exit', detail: Record<string, unknown>): void => {
|
|
756
|
+
try {
|
|
757
|
+
if (event === 'spawn') ring.push('info', 'gateway spawned (port=%s)', String(detail.port ?? '?'))
|
|
758
|
+
else ring.push('warn', 'gateway exited (code=%s signal=%s error=%s)', String(detail.code ?? '—'), String(detail.signal ?? '—'), String(detail.error ?? '—'))
|
|
759
|
+
} catch { /* the ring must never break the wiring */ }
|
|
760
|
+
}
|
|
761
|
+
const gatewayConfig: Record<string, unknown> = { ...effective.values, relaySecret }
|
|
762
|
+
Object.defineProperty(gatewayConfig, 'gatewayEvent', { value: gatewayEvent, enumerable: false })
|
|
763
|
+
ctx.plugin(gateway, gatewayConfig)
|
|
681
764
|
ctx.plugin(push, effective.values)
|
|
682
765
|
// The shared-session table backing the relay's access control. The relay
|
|
683
766
|
// route consumes it below; the activity tracker and idle sweeper (T22c)
|
|
@@ -720,7 +803,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
720
803
|
tracker.onEvent(sessionId, event.type)
|
|
721
804
|
})
|
|
722
805
|
} catch (error) {
|
|
723
|
-
|
|
806
|
+
logWarn('dsh-zen-remote cannot listen on "session/event": %s', message(error))
|
|
724
807
|
}
|
|
725
808
|
// Auto-share and fork-following (T33a), off `agent/created` — the only
|
|
726
809
|
// host event that fires EXACTLY ONCE per NEW session. Its `source` is
|
|
@@ -743,6 +826,14 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
743
826
|
// best-effort bookkeeping — it must never be able to fail the
|
|
744
827
|
// creation that triggered it.
|
|
745
828
|
try {
|
|
829
|
+
// PB-26a: record the subagent→parent link the moment the child
|
|
830
|
+
// exists — for EVERY source. A sidechat child (better-sidebar's
|
|
831
|
+
// sidechat.start → agents.create) rides no workspace and writes
|
|
832
|
+
// no catalog; until its first session/event the index knew
|
|
833
|
+
// nothing of it, so the relay answered not-shared for a session
|
|
834
|
+
// whose share it inherits through its parent, and the forwarded
|
|
835
|
+
// status frames the client needs to announce the row never left.
|
|
836
|
+
parentIndex.observe(agent.session)
|
|
746
837
|
if (source !== 'startup') return
|
|
747
838
|
onSessionCreated(agent.session.header, {
|
|
748
839
|
store,
|
|
@@ -750,11 +841,11 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
750
841
|
parentOf: parentIndex.parentOf,
|
|
751
842
|
})
|
|
752
843
|
} catch (error) {
|
|
753
|
-
|
|
844
|
+
logWarn('dsh-zen-remote cannot share new session "%s": %s', agent.id, message(error))
|
|
754
845
|
}
|
|
755
846
|
})
|
|
756
847
|
} catch (error) {
|
|
757
|
-
|
|
848
|
+
logWarn('dsh-zen-remote cannot listen on "agent/created": %s', message(error))
|
|
758
849
|
}
|
|
759
850
|
// Busy restore (T33a): `busy` is never persisted, so right after a row
|
|
760
851
|
// restart every shared session believes itself idle — including one whose
|
|
@@ -777,19 +868,19 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
777
868
|
sessionPersistence?: { list(): Promise<readonly PersistedSessionSnapshot[]> } | undefined
|
|
778
869
|
}).sessionPersistence
|
|
779
870
|
if (persistence === undefined) {
|
|
780
|
-
|
|
871
|
+
logWarn('dsh-zen-remote: no sessionPersistence service — pre-restart subagent sessions stay not-shared until they emit an event')
|
|
781
872
|
return
|
|
782
873
|
}
|
|
783
874
|
void persistence
|
|
784
875
|
.list()
|
|
785
876
|
.then((snapshots) => {
|
|
786
877
|
const recorded = seedParentIndexFromSnapshots(parentIndex, snapshots)
|
|
787
|
-
if (recorded > 0)
|
|
878
|
+
if (recorded > 0) logInfo('dsh-zen-remote: restored %d persisted parent links', recorded)
|
|
788
879
|
})
|
|
789
|
-
.catch((error: unknown) =>
|
|
880
|
+
.catch((error: unknown) => logWarn('dsh-zen-remote cannot seed parent links from persisted sessions: %s', message(error)))
|
|
790
881
|
})
|
|
791
882
|
} catch (error) {
|
|
792
|
-
|
|
883
|
+
logWarn('dsh-zen-remote cannot listen on "sessionPersistence": %s', message(error))
|
|
793
884
|
}
|
|
794
885
|
// Settings-surface admin routes (T14). The browser cannot call the
|
|
795
886
|
// gateway's loopback-only admin API from DSH's origin, so the host
|
|
@@ -1213,6 +1304,35 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1213
1304
|
}
|
|
1214
1305
|
}, 'dsh-zen-remote: relay route')
|
|
1215
1306
|
})
|
|
1307
|
+
// CU-3 (host mount): the gateway child's summary plate for the debug
|
|
1308
|
+
// report — one best-effort probe of the gateway's own status endpoint
|
|
1309
|
+
// (the same seam the wizard's runtime probe reads) reduced to liveness,
|
|
1310
|
+
// port and COUNTS. The endpoint's device rows carry no tokens and we
|
|
1311
|
+
// keep none of their fields anyway; the share table's size is read off
|
|
1312
|
+
// the store this apply owns. The gateway being down IS the summary.
|
|
1313
|
+
const gatewaySummary = async (): Promise<Record<string, unknown>> => {
|
|
1314
|
+
let reachable = false
|
|
1315
|
+
let port: number | undefined
|
|
1316
|
+
let deviceCount: number | undefined
|
|
1317
|
+
let pushSubscriptions: number | undefined
|
|
1318
|
+
try {
|
|
1319
|
+
const response = await fetch(`http://127.0.0.1:${effective.values.port}/lan-gate/status`, { signal: AbortSignal.timeout(3000) })
|
|
1320
|
+
reachable = response.ok
|
|
1321
|
+
if (reachable) {
|
|
1322
|
+
const body = await response.json() as { port?: unknown; devices?: unknown; pushSubscriptions?: unknown }
|
|
1323
|
+
if (typeof body?.port === 'number') port = body.port
|
|
1324
|
+
if (Array.isArray(body?.devices)) deviceCount = body.devices.length
|
|
1325
|
+
if (typeof body?.pushSubscriptions === 'number') pushSubscriptions = body.pushSubscriptions
|
|
1326
|
+
}
|
|
1327
|
+
} catch { /* unreachable is an honest answer */ }
|
|
1328
|
+
return {
|
|
1329
|
+
reachable,
|
|
1330
|
+
...(port === undefined ? {} : { port }),
|
|
1331
|
+
...(deviceCount === undefined ? {} : { deviceCount }),
|
|
1332
|
+
...(pushSubscriptions === undefined ? {} : { pushSubscriptions }),
|
|
1333
|
+
shareCount: store.list().length,
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1216
1336
|
// The remote-status route lives on BOTH roles (T34-fix): it is what the
|
|
1217
1337
|
// client-half parts poll, and a desktop app can switch roles without a
|
|
1218
1338
|
// code path change — on a host it reads the empty
|
|
@@ -1231,6 +1351,10 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1231
1351
|
getRowConfig: () => config,
|
|
1232
1352
|
remoteStatusOnly: true,
|
|
1233
1353
|
getServiceProbe: uploadServiceProbe(clientCtx),
|
|
1354
|
+
getLogSnapshot: () => ring.snapshot(),
|
|
1355
|
+
getGatewaySummary: gatewaySummary,
|
|
1356
|
+
getProfileName: profileName,
|
|
1357
|
+
getPluginVersion: () => pluginVersion,
|
|
1234
1358
|
}),
|
|
1235
1359
|
}), 'dsh-zen-remote: client routes (host)')
|
|
1236
1360
|
})
|
|
@@ -1272,6 +1396,22 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1272
1396
|
// fresh handshake or a link flip shows up at the very next read.
|
|
1273
1397
|
const pluginBridgeStatus = (): BridgeStatus[] =>
|
|
1274
1398
|
compareBridges(registry.list(), relayClient.handshakeInfo?.plugins, relayClient.state === 'online')
|
|
1399
|
+
// CU-1 lifecycle: the relay link's flips enter the ring — `online` with
|
|
1400
|
+
// the server id it handshook, `offline` with the mapped failure code
|
|
1401
|
+
// (relayClient.lastError is a short code by contract, never free text).
|
|
1402
|
+
// subscribe re-announces the CURRENT state when only the handshake
|
|
1403
|
+
// identity moved (the T59 heartbeat), so the dedupe below keeps the
|
|
1404
|
+
// heartbeats out of the ring: one line per actual state change.
|
|
1405
|
+
let lastRelayRingState: string | undefined
|
|
1406
|
+
const offRelayRing = relayClient.subscribe((state) => {
|
|
1407
|
+
if (state === lastRelayRingState) return
|
|
1408
|
+
lastRelayRingState = state
|
|
1409
|
+
if (state === 'online') {
|
|
1410
|
+
ring.push('info', 'relay link online (server=%s)', relayClient.handshakeInfo?.serverId ?? '?')
|
|
1411
|
+
} else if (state === 'offline') {
|
|
1412
|
+
ring.push('warn', 'relay link offline (code=%s)', relayClient.lastError ?? '?')
|
|
1413
|
+
}
|
|
1414
|
+
})
|
|
1275
1415
|
// A stopped or reloaded row must not leave a stale instance where the
|
|
1276
1416
|
// module getter can hand it out (T23a-fix), and its reconnect machinery
|
|
1277
1417
|
// (T43) must not outlive the row either: stop() cancels the armed retry
|
|
@@ -1279,6 +1419,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1279
1419
|
// dispose from clearing a NEWER row's instance that a reload already
|
|
1280
1420
|
// installed.
|
|
1281
1421
|
ctx.effect(() => () => {
|
|
1422
|
+
offRelayRing()
|
|
1282
1423
|
relayClient.stop()
|
|
1283
1424
|
if (clientRelayClient === relayClient) clientRelayClient = undefined
|
|
1284
1425
|
}, 'dsh-zen-remote: relay client disposal')
|
|
@@ -1314,7 +1455,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1314
1455
|
off()
|
|
1315
1456
|
}
|
|
1316
1457
|
} catch (error) {
|
|
1317
|
-
|
|
1458
|
+
logWarn('dsh-zen-remote: cannot listen on "loader/volatile-update" for relay credentials: %s', message(error))
|
|
1318
1459
|
return () => {}
|
|
1319
1460
|
}
|
|
1320
1461
|
}, 'dsh-zen-remote: relay credentials watcher')
|
|
@@ -1325,7 +1466,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1325
1466
|
void relayClient.connect().catch((error) => {
|
|
1326
1467
|
if (error instanceof RelayError && error.code === 'unpaired') return
|
|
1327
1468
|
const code = error instanceof RelayError ? error.code : 'error'
|
|
1328
|
-
|
|
1469
|
+
logWarn('dsh-zen-remote relay client initial connect failed (%s): %s', code, message(error))
|
|
1329
1470
|
})
|
|
1330
1471
|
// The typertGateway interception (T23b-1): remote-session calls ride the
|
|
1331
1472
|
// relay, everything else reaches the local gateway untouched. The live
|
|
@@ -1389,9 +1530,6 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1389
1530
|
return undefined
|
|
1390
1531
|
}
|
|
1391
1532
|
ctx.inject(['typertGateway'], (gatewayCtx) => {
|
|
1392
|
-
// Degraded logging: the real cordis context always carries a logger,
|
|
1393
|
-
// but a row fake without one must not crash apply.
|
|
1394
|
-
const logWarn = (...args: Parameters<Context['logger']['warn']>): void => { ctx.logger?.warn(...args) }
|
|
1395
1533
|
const proxy = (gatewayCtx as Context & { typertGateway?: unknown }).typertGateway
|
|
1396
1534
|
const raw = proxy !== null && typeof proxy === 'object'
|
|
1397
1535
|
? (proxy as Record<PropertyKey, unknown>)[symbols.original]
|
|
@@ -1417,7 +1555,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1417
1555
|
// PB-25b: each merged stream reports its real-session inventory
|
|
1418
1556
|
// share into the union above.
|
|
1419
1557
|
createSessionInventorySlot,
|
|
1420
|
-
log: (format, ...args) => {
|
|
1558
|
+
log: (format, ...args) => { logInfo(`dsh-zen-remote ${format}`, ...args) },
|
|
1421
1559
|
})
|
|
1422
1560
|
interceptHandle = handle
|
|
1423
1561
|
gatewayCtx.effect(() => () => {
|
|
@@ -1439,7 +1577,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1439
1577
|
void runSelfCheck(raw, {
|
|
1440
1578
|
handle,
|
|
1441
1579
|
log: logWarn,
|
|
1442
|
-
info: (format, ...args) => {
|
|
1580
|
+
info: (format, ...args) => { logInfo(`dsh-zen-remote ${format}`, ...args) },
|
|
1443
1581
|
getPeer: createOwnerPeerSource(() => gatewayCtx.reflect.get('connection')),
|
|
1444
1582
|
})
|
|
1445
1583
|
})
|
|
@@ -1482,8 +1620,12 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1482
1620
|
fetchRouteDiagnosticsRef = fetchRouteDiagnostics
|
|
1483
1621
|
ctx.inject(['connection', 'fileUploads'], (connectionCtx) => {
|
|
1484
1622
|
fetchRouteInjectSeen = true
|
|
1485
|
-
|
|
1486
|
-
|
|
1623
|
+
// BU-1 double-install guard: the fallback timer may have installed
|
|
1624
|
+
// while this callback sat PENDING — or recorded a refusal. Either way
|
|
1625
|
+
// this primary path has nothing left to do; installing again would
|
|
1626
|
+
// chain a second wrap over the first (behaviorally right, structurally
|
|
1627
|
+
// dirty) and overwrite the diagnostics' installedVia.
|
|
1628
|
+
if (fetchRouteHandle !== undefined || fetchRouteRefused !== undefined) return
|
|
1487
1629
|
const proxy = (connectionCtx as Context & { connection?: unknown }).connection
|
|
1488
1630
|
const raw = proxy !== null && typeof proxy === 'object'
|
|
1489
1631
|
? (proxy as unknown as Record<PropertyKey, unknown>)[symbols.original]
|
|
@@ -1509,11 +1651,109 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1509
1651
|
log: (format, ...args) => { logInfo(`dsh-zen-remote ${format}`, ...args) },
|
|
1510
1652
|
})
|
|
1511
1653
|
fetchRouteHandle = handle
|
|
1654
|
+
fetchRouteInstalledVia = 'inject'
|
|
1655
|
+
// CU-1 lifecycle: a successful install logs nothing today (the wrap's
|
|
1656
|
+
// own log lines only fire per call), so the ring gets one explicit
|
|
1657
|
+
// record — the debug report's "did the redirect actually attach" fact.
|
|
1658
|
+
// The refused and fallback paths funnel through logWarn/logInfo below
|
|
1659
|
+
// and enter the ring that way.
|
|
1660
|
+
ring.push('info', 'fetch-route intercept attached (via=inject)')
|
|
1512
1661
|
connectionCtx.effect(() => () => {
|
|
1513
1662
|
handle.uninstall()
|
|
1514
|
-
if (fetchRouteHandle === handle)
|
|
1663
|
+
if (fetchRouteHandle === handle) {
|
|
1664
|
+
fetchRouteHandle = undefined
|
|
1665
|
+
fetchRouteInstalledVia = undefined
|
|
1666
|
+
}
|
|
1515
1667
|
}, 'dsh-zen-remote: fetch-route intercept')
|
|
1516
1668
|
})
|
|
1669
|
+
// BU-1: the fetch-route FALLBACK install (SPEC §2). RD-6 proved the
|
|
1670
|
+
// machine-local failure mode: on one Windows client the
|
|
1671
|
+
// `['connection','fileUploads']` inject above sat silently PENDING (a
|
|
1672
|
+
// stuck row keeps cordis waiting forever, printing nothing), the wrap
|
|
1673
|
+
// never installed, and every remote upload fell through to the local
|
|
1674
|
+
// backend. The inject stays the FIRST choice — this timer only acts when
|
|
1675
|
+
// it demonstrably has not fired while BOTH services are present in the
|
|
1676
|
+
// reflection layer, then installs through the same
|
|
1677
|
+
// checkFetchRouteShape → symbols.original → installFetchRouteIntercept
|
|
1678
|
+
// path, sharing the handle/refused slots (which is also the
|
|
1679
|
+
// double-install guard: whichever path runs first, the other sees the
|
|
1680
|
+
// slots set and stands down). Bounded: a 3 s tick, at most 20 (a minute)
|
|
1681
|
+
// — a composition that never ships the services stops probing; unref'd
|
|
1682
|
+
// and cleared with the row.
|
|
1683
|
+
let fallbackTimer: ReturnType<typeof setInterval> | undefined
|
|
1684
|
+
let fallbackAttempts = 0
|
|
1685
|
+
const stopFallbackTimer = (): void => {
|
|
1686
|
+
if (fallbackTimer !== undefined) {
|
|
1687
|
+
clearInterval(fallbackTimer)
|
|
1688
|
+
fallbackTimer = undefined
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
fallbackTimer = setInterval(() => {
|
|
1692
|
+
// The tick budget is checked BEFORE acting but does not cancel the
|
|
1693
|
+
// current tick: attempt #20 still runs, no tick #21 ever comes.
|
|
1694
|
+
fallbackAttempts += 1
|
|
1695
|
+
if (fallbackAttempts >= 20) stopFallbackTimer()
|
|
1696
|
+
// Same reflection the service probe reads: both services must be
|
|
1697
|
+
// PRESENT — their absence means a legitimate startup wait (or a
|
|
1698
|
+
// composition without them), where the inject must stay first choice.
|
|
1699
|
+
const reflect = (ctx as Context & { reflect?: { get(name: string): unknown } }).reflect
|
|
1700
|
+
const connection = reflect?.get('connection')
|
|
1701
|
+
const fileUploads = reflect?.get('fileUploads')
|
|
1702
|
+
if (!shouldFallbackInstall({
|
|
1703
|
+
injectSeen: fetchRouteInjectSeen,
|
|
1704
|
+
handleSet: fetchRouteHandle !== undefined,
|
|
1705
|
+
refused: fetchRouteRefused !== undefined,
|
|
1706
|
+
servicesPresent: connection !== undefined && fileUploads !== undefined,
|
|
1707
|
+
})) {
|
|
1708
|
+
// The services being absent is a WAIT (next tick re-judges); the
|
|
1709
|
+
// primary path having advanced is a VERDICT — the fallback has no
|
|
1710
|
+
// business left, stop probing.
|
|
1711
|
+
if (fetchRouteInjectSeen || fetchRouteHandle !== undefined || fetchRouteRefused !== undefined) stopFallbackTimer()
|
|
1712
|
+
return
|
|
1713
|
+
}
|
|
1714
|
+
// Shape against the proxy first (fetchRoutes reads through it); the
|
|
1715
|
+
// raw instance comes off the same symbols slot the inject path uses.
|
|
1716
|
+
const shape = checkFetchRouteShape(connection)
|
|
1717
|
+
if (!shape.ok) {
|
|
1718
|
+
// Structural mismatch is a verdict, not a wait: record the attempt
|
|
1719
|
+
// and stop. A later inject trigger may still install (it checks the
|
|
1720
|
+
// RAW instance, which can differ from what the proxy exposed).
|
|
1721
|
+
fetchRouteFallbackTriedAt = new Date().toISOString()
|
|
1722
|
+
stopFallbackTimer()
|
|
1723
|
+
logWarn('dsh-zen-remote fetch-route fallback declined: %s', shape.reasons.join('; '))
|
|
1724
|
+
return
|
|
1725
|
+
}
|
|
1726
|
+
const raw = connection !== null && typeof connection === 'object'
|
|
1727
|
+
? (connection as unknown as Record<PropertyKey, unknown>)[symbols.original]
|
|
1728
|
+
: undefined
|
|
1729
|
+
if (raw === null || typeof raw !== 'object') {
|
|
1730
|
+
fetchRouteFallbackTriedAt = new Date().toISOString()
|
|
1731
|
+
stopFallbackTimer()
|
|
1732
|
+
logWarn('dsh-zen-remote fetch-route fallback declined: %s', 'original: connection[symbols.original] is not an object')
|
|
1733
|
+
return
|
|
1734
|
+
}
|
|
1735
|
+
const handle = installFetchRouteIntercept({
|
|
1736
|
+
connection: raw,
|
|
1737
|
+
relay: relayClient,
|
|
1738
|
+
getServerId: () => relayClient.handshakeInfo?.serverId,
|
|
1739
|
+
log: (format, ...args) => { logInfo(`dsh-zen-remote ${format}`, ...args) },
|
|
1740
|
+
})
|
|
1741
|
+
fetchRouteHandle = handle
|
|
1742
|
+
fetchRouteInstalledVia = 'fallback'
|
|
1743
|
+
stopFallbackTimer()
|
|
1744
|
+
logInfo('dsh-zen-remote fetch-route intercept installed via the fallback timer (inject never fired)')
|
|
1745
|
+
// Same disposal contract as the inject path: the wrap dies with the
|
|
1746
|
+
// row (the fallback has no service context of its own to hang on).
|
|
1747
|
+
ctx.effect(() => () => {
|
|
1748
|
+
handle.uninstall()
|
|
1749
|
+
if (fetchRouteHandle === handle) {
|
|
1750
|
+
fetchRouteHandle = undefined
|
|
1751
|
+
fetchRouteInstalledVia = undefined
|
|
1752
|
+
}
|
|
1753
|
+
}, 'dsh-zen-remote: fetch-route intercept (fallback)')
|
|
1754
|
+
}, 3_000)
|
|
1755
|
+
if (typeof fallbackTimer.unref === 'function') fallbackTimer.unref()
|
|
1756
|
+
ctx.effect(() => () => { stopFallbackTimer() }, 'dsh-zen-remote: fetch-route fallback timer')
|
|
1517
1757
|
// The plugin bridge's route interception (PB-4): the local webServer's
|
|
1518
1758
|
// `match` wrap forwarding bridged-plugin requests for REMOTE sessions
|
|
1519
1759
|
// through the relay. Injected on `webServer` alone — the only service
|
|
@@ -1544,7 +1784,6 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1544
1784
|
return undefined
|
|
1545
1785
|
}
|
|
1546
1786
|
ctx.inject(['webServer'], (wsCtx) => {
|
|
1547
|
-
const logWarn = (...args: Parameters<Context['logger']['warn']>): void => { ctx.logger?.warn(...args) }
|
|
1548
1787
|
const proxy = (wsCtx as Context & { webServer?: unknown }).webServer
|
|
1549
1788
|
const raw = proxy !== null && typeof proxy === 'object'
|
|
1550
1789
|
? (proxy as unknown as Record<PropertyKey, unknown>)[symbols.original]
|
|
@@ -1578,7 +1817,12 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1578
1817
|
// answer — the rewriteIds deep scan rewrites an id that already sits
|
|
1579
1818
|
// in the browser's view even when this request never carried it.
|
|
1580
1819
|
getKnownRealIds: () => knownRealIds,
|
|
1581
|
-
|
|
1820
|
+
// PB-26a: a response rewrite that minted a virtual id (a sidechat
|
|
1821
|
+
// child from `rewriteIds.fields` or an adapter's `virtualize`) is the
|
|
1822
|
+
// client's only early notice of a row that rides no workspace/follow
|
|
1823
|
+
// frame — hand it to the summary sync so the sidebar row appears.
|
|
1824
|
+
onMintedVirtualIds: (ids) => { interceptHandle?.nudgeSummarySync(ids) },
|
|
1825
|
+
log: (format, ...args) => { logInfo(`dsh-zen-remote ${format}`, ...args) },
|
|
1582
1826
|
})
|
|
1583
1827
|
pluginRouteHandle = handle
|
|
1584
1828
|
wsCtx.effect(() => () => {
|
|
@@ -1600,6 +1844,9 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1600
1844
|
getPluginRouteIntercept: pluginRouteDiagnostics,
|
|
1601
1845
|
getPluginBridgeStatus: pluginBridgeStatus,
|
|
1602
1846
|
onCurrentSession,
|
|
1847
|
+
getLogSnapshot: () => ring.snapshot(),
|
|
1848
|
+
getProfileName: profileName,
|
|
1849
|
+
getPluginVersion: () => pluginVersion,
|
|
1603
1850
|
}),
|
|
1604
1851
|
}), 'dsh-zen-remote: client routes')
|
|
1605
1852
|
})
|
|
@@ -1626,17 +1873,21 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1626
1873
|
// dispose-and-reload entry. A fake context without a fiber (the route
|
|
1627
1874
|
// tests) only ever reaches the warn.
|
|
1628
1875
|
const reload = (): void => {
|
|
1876
|
+
// CU-1 lifecycle: the row is about to reload — the ring dies with this
|
|
1877
|
+
// apply right after, so this line is the reload's only witness in the
|
|
1878
|
+
// NEXT generation's report (the fresh apply's row-apply line follows).
|
|
1879
|
+
ring.push('info', 'row reload: a restart-required setting changed, reloading the plugin row')
|
|
1629
1880
|
const fiber = (ctx as { fiber?: { restart?: () => Promise<unknown> } }).fiber
|
|
1630
1881
|
if (fiber?.restart === undefined) {
|
|
1631
|
-
|
|
1882
|
+
logWarn('dsh-zen-remote: a restart-required setting changed, but no fiber is available to reload the plugin row')
|
|
1632
1883
|
return
|
|
1633
1884
|
}
|
|
1634
1885
|
try {
|
|
1635
1886
|
Promise.resolve(fiber.restart()).catch((error) => {
|
|
1636
|
-
|
|
1887
|
+
logWarn('dsh-zen-remote: plugin row reload failed: %s', message(error))
|
|
1637
1888
|
})
|
|
1638
1889
|
} catch (error) {
|
|
1639
|
-
|
|
1890
|
+
logWarn('dsh-zen-remote: plugin row reload failed: %s', message(error))
|
|
1640
1891
|
}
|
|
1641
1892
|
}
|
|
1642
1893
|
ctx.effect(() => {
|
|
@@ -1649,7 +1900,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1649
1900
|
try {
|
|
1650
1901
|
ctx.on('loader/volatile-update', () => { watcher.checkNow() })
|
|
1651
1902
|
} catch (error) {
|
|
1652
|
-
|
|
1903
|
+
logWarn('dsh-zen-remote: cannot listen on "loader/volatile-update": %s', message(error))
|
|
1653
1904
|
}
|
|
1654
1905
|
return watcher.stop
|
|
1655
1906
|
}, 'dsh-zen-remote: restart watcher')
|
|
@@ -1681,6 +1932,7 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
1681
1932
|
req,
|
|
1682
1933
|
res,
|
|
1683
1934
|
(admitReq) => webCtx.connection.admit(admitReq),
|
|
1935
|
+
logWarn,
|
|
1684
1936
|
)
|
|
1685
1937
|
},
|
|
1686
1938
|
}), 'dsh-mobile-nav: upload route')
|