dsh-deeppilot 0.9.1 → 0.9.2

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/lib/client.js CHANGED
@@ -3667,7 +3667,330 @@ window.__ModuleLoader__.load({
3667
3667
  return typeof value === "number" && Number.isInteger(value) && value >= 1024 && value <= 65535 ? value : DEFAULT_LOCAL_PORT;
3668
3668
  }
3669
3669
  //#endregion
3670
+ //#region src/client/settings-props.ts
3671
+ const MISSING_REPORT_HOOK = () => ({
3672
+ status: "loading",
3673
+ report: null,
3674
+ message: ""
3675
+ });
3676
+ const MISSING_ENABLED_HOOK = () => ({
3677
+ status: "unavailable",
3678
+ enabled: true
3679
+ });
3680
+ const MISSING_LOCAL_ENABLED_HOOK = () => ({
3681
+ status: "unavailable",
3682
+ enabled: true
3683
+ });
3684
+ const MISSING_LOCAL_PORT_HOOK = () => ({
3685
+ status: "unavailable",
3686
+ value: DEFAULT_LOCAL_PORT
3687
+ });
3688
+ const MISSING_REMOTE_ENABLED_HOOK = () => ({
3689
+ status: "unavailable",
3690
+ enabled: false
3691
+ });
3692
+ const MISSING_REMOTE_LIMIT_HOOK = () => ({
3693
+ status: "unavailable",
3694
+ value: 8
3695
+ });
3696
+ const MISSING_DEBUG_HOOK = () => ({
3697
+ status: "unavailable",
3698
+ enabled: false
3699
+ });
3700
+ /** 诊断行的 key,集中在此便于对账(tests/settings-props.test.ts 逐条断言)。 */
3701
+ const DIAG_KEY = {
3702
+ reportHook: "diag.missingReportHook",
3703
+ enabledHook: "diag.missingEnabledHook",
3704
+ localEnabledHook: "diag.missingLocalEnabledHook",
3705
+ localPortHook: "diag.missingLocalPortHook",
3706
+ remoteEnabledHook: "diag.missingRemoteEnabledHook",
3707
+ remoteLimitHook: "diag.missingRemoteLimitHook",
3708
+ debugHook: "diag.missingDebugHook",
3709
+ refresh: "diag.missingRefresh",
3710
+ reveal: "diag.missingReveal",
3711
+ rotate: "diag.missingRotate",
3712
+ rename: "diag.missingRename",
3713
+ testRelay: "diag.missingTestRelay",
3714
+ testPush: "diag.missingTestPush",
3715
+ setEnabled: "diag.missingSetEnabled",
3716
+ setLocal: "diag.missingSetLocal",
3717
+ setLocalPort: "diag.missingSetLocalPort",
3718
+ setRemote: "diag.missingSetRemote",
3719
+ setRemoteLimit: "diag.missingSetRemoteLimit",
3720
+ setDebug: "diag.missingSetDebug",
3721
+ settingsUnavailable: "diag.settingsUnavailable"
3722
+ };
3723
+ const CAPABILITY_BY_PROP = [
3724
+ ["refresh", "refresh"],
3725
+ ["beginPairing", "beginPairing"],
3726
+ ["revokeDevice", "revokeDevice"],
3727
+ ["setDeviceName", "setDeviceName"],
3728
+ ["testRelay", "testRelay"],
3729
+ ["testPush", "testPush"],
3730
+ ["setEnabled", "setDeepPilotEnabled"],
3731
+ ["setLocalEnabled", "setDeepPilotLocalEnabled"],
3732
+ ["setLocalPort", "setDeepPilotLocalPort"],
3733
+ ["setRemoteEnabled", "setDeepPilotRemoteEnabled"],
3734
+ ["setRemoteLimit", "setDeepPilotRemoteConnectionLimit"],
3735
+ ["setDebug", "setDeepPilotDebug"]
3736
+ ];
3737
+ const isFn = (value) => typeof value === "function";
3738
+ /**
3739
+ * 把宿主注入的原始对象归一化成页面可消费的面。
3740
+ *
3741
+ * 逐项复刻迁移前页面内的行为:hook 缺失换 fallback 并记诊断;函数缺失则该
3742
+ * 能力不进 `can` 并记诊断;诊断顺序与原先的 `diag[]` 完全一致(宿主过期时
3743
+ * 用户看到的技术诊断因此一字不差)。
3744
+ */
3745
+ function normalizeSettingsPageProps(raw) {
3746
+ const source = raw ?? {};
3747
+ const t = typeof source.t === "function" ? source.t : ((key) => key);
3748
+ const diagnostics = [];
3749
+ const can = /* @__PURE__ */ new Set();
3750
+ const useReport = isFn(source.useDeepPilotReport) ? source.useDeepPilotReport : MISSING_REPORT_HOOK;
3751
+ const useEnabled = isFn(source.useDeepPilotEnabled) ? source.useDeepPilotEnabled : MISSING_ENABLED_HOOK;
3752
+ const useLocalEnabled = isFn(source.useDeepPilotLocalEnabled) ? source.useDeepPilotLocalEnabled : MISSING_LOCAL_ENABLED_HOOK;
3753
+ const useLocalPort = isFn(source.useDeepPilotLocalPort) ? source.useDeepPilotLocalPort : MISSING_LOCAL_PORT_HOOK;
3754
+ const useRemoteEnabled = isFn(source.useDeepPilotRemoteEnabled) ? source.useDeepPilotRemoteEnabled : MISSING_REMOTE_ENABLED_HOOK;
3755
+ const useRemoteLimit = isFn(source.useDeepPilotRemoteConnectionLimit) ? source.useDeepPilotRemoteConnectionLimit : MISSING_REMOTE_LIMIT_HOOK;
3756
+ const useDebug = isFn(source.useDeepPilotDebug) ? source.useDeepPilotDebug : MISSING_DEBUG_HOOK;
3757
+ if (!isFn(source.useDeepPilotReport)) diagnostics.push(t(DIAG_KEY.reportHook));
3758
+ if (!isFn(source.useDeepPilotEnabled)) diagnostics.push(t(DIAG_KEY.enabledHook));
3759
+ if (!isFn(source.refresh)) diagnostics.push(t(DIAG_KEY.refresh));
3760
+ if (!isFn(source.beginPairing)) diagnostics.push(t(DIAG_KEY.reveal));
3761
+ if (!isFn(source.revokeDevice)) diagnostics.push(t(DIAG_KEY.rotate));
3762
+ if (!isFn(source.setDeviceName)) diagnostics.push(t(DIAG_KEY.rename));
3763
+ if (!isFn(source.testRelay)) diagnostics.push(t(DIAG_KEY.testRelay));
3764
+ if (!isFn(source.testPush)) diagnostics.push(t(DIAG_KEY.testPush));
3765
+ if (!isFn(source.setDeepPilotEnabled)) diagnostics.push(t(DIAG_KEY.setEnabled));
3766
+ if (!isFn(source.useDeepPilotLocalEnabled)) diagnostics.push(t(DIAG_KEY.localEnabledHook));
3767
+ if (!isFn(source.useDeepPilotLocalPort)) diagnostics.push(t(DIAG_KEY.localPortHook));
3768
+ if (!isFn(source.setDeepPilotLocalEnabled)) diagnostics.push(t(DIAG_KEY.setLocal));
3769
+ if (!isFn(source.setDeepPilotLocalPort)) diagnostics.push(t(DIAG_KEY.setLocalPort));
3770
+ if (!isFn(source.useDeepPilotRemoteEnabled)) diagnostics.push(t(DIAG_KEY.remoteEnabledHook));
3771
+ if (!isFn(source.setDeepPilotRemoteEnabled)) diagnostics.push(t(DIAG_KEY.setRemote));
3772
+ if (!isFn(source.useDeepPilotRemoteConnectionLimit)) diagnostics.push(t(DIAG_KEY.remoteLimitHook));
3773
+ if (!isFn(source.setDeepPilotRemoteConnectionLimit)) diagnostics.push(t(DIAG_KEY.setRemoteLimit));
3774
+ if (!isFn(source.useDeepPilotDebug)) diagnostics.push(t(DIAG_KEY.debugHook));
3775
+ if (!isFn(source.setDeepPilotDebug)) diagnostics.push(t(DIAG_KEY.setDebug));
3776
+ for (const [capability, prop] of CAPABILITY_BY_PROP) if (isFn(source[prop])) can.add(capability);
3777
+ return {
3778
+ props: {
3779
+ useDeepPilotReport: useReport,
3780
+ useDeepPilotEnabled: useEnabled,
3781
+ useDeepPilotLocalEnabled: useLocalEnabled,
3782
+ useDeepPilotLocalPort: useLocalPort,
3783
+ useDeepPilotRemoteEnabled: useRemoteEnabled,
3784
+ useDeepPilotRemoteConnectionLimit: useRemoteLimit,
3785
+ useDeepPilotDebug: useDebug,
3786
+ t,
3787
+ refresh: isFn(source.refresh) ? source.refresh : void 0,
3788
+ beginPairing: isFn(source.beginPairing) ? source.beginPairing : void 0,
3789
+ revokeDevice: isFn(source.revokeDevice) ? source.revokeDevice : void 0,
3790
+ setDeviceName: isFn(source.setDeviceName) ? source.setDeviceName : void 0,
3791
+ testRelay: isFn(source.testRelay) ? source.testRelay : void 0,
3792
+ testPush: isFn(source.testPush) ? source.testPush : void 0,
3793
+ setDeepPilotEnabled: isFn(source.setDeepPilotEnabled) ? source.setDeepPilotEnabled : void 0,
3794
+ setDeepPilotLocalEnabled: isFn(source.setDeepPilotLocalEnabled) ? source.setDeepPilotLocalEnabled : void 0,
3795
+ setDeepPilotLocalPort: isFn(source.setDeepPilotLocalPort) ? source.setDeepPilotLocalPort : void 0,
3796
+ setDeepPilotRemoteEnabled: isFn(source.setDeepPilotRemoteEnabled) ? source.setDeepPilotRemoteEnabled : void 0,
3797
+ setDeepPilotRemoteConnectionLimit: isFn(source.setDeepPilotRemoteConnectionLimit) ? source.setDeepPilotRemoteConnectionLimit : void 0,
3798
+ setDeepPilotDebug: isFn(source.setDeepPilotDebug) ? source.setDeepPilotDebug : void 0
3799
+ },
3800
+ can,
3801
+ diagnostics,
3802
+ reportHookMissing: !isFn(source.useDeepPilotReport)
3803
+ };
3804
+ }
3805
+ //#endregion
3806
+ //#region src/client/settings-view-models.ts
3807
+ /**
3808
+ * 设置页的 view model:把原先住在渲染函数里的三块逻辑变成可单测的纯模块。
3809
+ *
3810
+ * 迁移前 `settings-page.ts` 是一个 1059 行的组件,funnel phase → {dot, label}、
3811
+ 端口/连接数草稿的编辑与合法性、serviceStatus 派生全在渲染函数里,没有任何
3812
+ 直接测试。这里按 pairing-panel.ts 的形状把它们拆成 state / action / reduce /
3813
+ view 四件套——shape 与该先例一致,但**不继承它唯一的泄漏**:view model 只返回
3814
+ 语义值,i18n key 的映射留在页面(措辞归页面,测试不跟着措辞漂)。
3815
+ */
3816
+ const REMOTE_PHASE_META = {
3817
+ disabled: {
3818
+ dot: "",
3819
+ label: "disabled"
3820
+ },
3821
+ starting: {
3822
+ dot: " pbb-dotWarn",
3823
+ label: "starting"
3824
+ },
3825
+ login_required: {
3826
+ dot: " pbb-dotWarn",
3827
+ label: "login_required"
3828
+ },
3829
+ online: {
3830
+ dot: " pbb-dotOk",
3831
+ label: "online"
3832
+ },
3833
+ error: {
3834
+ dot: " pbb-dotBad",
3835
+ label: "error"
3836
+ },
3837
+ unavailable: {
3838
+ dot: " pbb-dotBad",
3839
+ label: "unavailable"
3840
+ },
3841
+ stopped: {
3842
+ dot: "",
3843
+ label: "stopped"
3844
+ }
3845
+ };
3846
+ const LOCAL_PHASE_META = {
3847
+ disabled: {
3848
+ dot: "",
3849
+ label: "disabled"
3850
+ },
3851
+ starting: {
3852
+ dot: " pbb-dotWarn",
3853
+ label: "starting"
3854
+ },
3855
+ online: {
3856
+ dot: " pbb-dotOk",
3857
+ label: "online"
3858
+ },
3859
+ error: {
3860
+ dot: " pbb-dotBad",
3861
+ label: "error"
3862
+ },
3863
+ stopped: {
3864
+ dot: "",
3865
+ label: "stopped"
3866
+ }
3867
+ };
3868
+ /** 从点位反推「需要注意 / 已在线」——原先页面用字符串 includes 猜类名。 */
3869
+ function dotState(dot) {
3870
+ return {
3871
+ attention: dot?.includes("pbb-dotBad") === true,
3872
+ online: dot?.includes("pbb-dotOk") === true
3873
+ };
3874
+ }
3875
+ function serviceStatus(input) {
3876
+ const local = dotState(input.localDot);
3877
+ const remote = dotState(input.remoteDot);
3878
+ const attention = local.attention || remote.attention;
3879
+ const online = local.online || remote.online;
3880
+ return {
3881
+ kind: !input.switchReady ? "loading" : !input.enabled ? "off" : attention ? "attention" : online ? "ready" : "loading",
3882
+ dot: !input.enabled ? "" : attention ? " pbb-dotBad" : online ? " pbb-dotOk" : " pbb-dotWarn"
3883
+ };
3884
+ }
3885
+ const LOCAL_PORT_RANGE = {
3886
+ min: MIN_LOCAL_PORT,
3887
+ max: MAX_LOCAL_PORT,
3888
+ fallback: DEFAULT_LOCAL_PORT
3889
+ };
3890
+ const REMOTE_LIMIT_RANGE = {
3891
+ min: 1,
3892
+ max: 16,
3893
+ fallback: 8
3894
+ };
3895
+ function initialNumericDraft(range) {
3896
+ return {
3897
+ draft: String(range.fallback),
3898
+ message: "",
3899
+ failed: false
3900
+ };
3901
+ }
3902
+ function numericDraftReduce(state, action, range) {
3903
+ switch (action.type) {
3904
+ case "edit": return {
3905
+ ...state,
3906
+ draft: action.draft
3907
+ };
3908
+ case "sync": return {
3909
+ ...state,
3910
+ draft: String(action.value),
3911
+ message: "",
3912
+ failed: false
3913
+ };
3914
+ case "applied": return {
3915
+ ...state,
3916
+ message: "",
3917
+ failed: false
3918
+ };
3919
+ case "succeeded": return {
3920
+ ...state,
3921
+ message: action.message,
3922
+ failed: false
3923
+ };
3924
+ case "failed": return {
3925
+ ...state,
3926
+ message: action.message,
3927
+ failed: true
3928
+ };
3929
+ }
3930
+ }
3931
+ function numericDraftView(state, current, range) {
3932
+ const value = Number(state.draft);
3933
+ const valid = Number.isInteger(value) && value >= range.min && value <= range.max;
3934
+ return {
3935
+ value,
3936
+ valid,
3937
+ unchanged: valid && value === current
3938
+ };
3939
+ }
3940
+ /** 已撤销的设备不展示。 */
3941
+ function visibleDevices(report) {
3942
+ return (report?.devices ?? []).filter((device) => device.revokedAt === void 0);
3943
+ }
3944
+ /** 设备展示名:自定义名优先,都没有则空串(兜底文案归页面)。 */
3945
+ function deviceLabel(device) {
3946
+ return (device.customName ?? device.deviceName).trim();
3947
+ }
3948
+ /** 渲染一块连接磁贴。纯数据函数:九参进,元素树出。 */
3949
+ function renderConnectionTile(key, title, description, checked, ready, dot, statusLabel, onToggle, extra = null) {
3950
+ return (0, react.createElement)("div", {
3951
+ className: "pbb-connectionTile",
3952
+ key
3953
+ }, (0, react.createElement)("div", { className: "pbb-connectionTileHeader" }, (0, react.createElement)("span", { className: "pbb-switchTitle pbb-dotRow" }, (0, react.createElement)("span", {
3954
+ className: "pbb-dot" + dot,
3955
+ role: "img",
3956
+ "aria-label": statusLabel,
3957
+ title: statusLabel
3958
+ }), title), (0, react.createElement)("button", {
3959
+ type: "button",
3960
+ role: "switch",
3961
+ "aria-checked": checked,
3962
+ "aria-label": title,
3963
+ disabled: !ready,
3964
+ className: "pbb-switch" + (checked ? " pbb-switchOn" : ""),
3965
+ onClick: onToggle
3966
+ })), (0, react.createElement)("span", { className: "pbb-connectionState" }, statusLabel), (0, react.createElement)("p", { className: "pbb-connectionDescription" }, description), extra);
3967
+ }
3968
+ //#endregion
3670
3969
  //#region src/client/settings-page.ts
3970
+ /** phase 语义标签 → i18n key。措辞归页面,view model 只认语义。 */
3971
+ const REMOTE_LABEL_KEY = {
3972
+ disabled: "phase.disabled",
3973
+ starting: "phase.starting",
3974
+ login_required: "phase.login_required",
3975
+ online: "phase.online",
3976
+ error: "phase.error",
3977
+ unavailable: "phase.unavailable",
3978
+ stopped: "phase.stopped"
3979
+ };
3980
+ const LOCAL_LABEL_KEY = {
3981
+ disabled: "local.phaseDisabled",
3982
+ starting: "local.phaseStarting",
3983
+ online: "local.phaseOnline",
3984
+ error: "local.phaseError",
3985
+ stopped: "local.phaseStopped"
3986
+ };
3987
+ /** 服务状态 → i18n key。 */
3988
+ const SERVICE_STATUS_KEY = {
3989
+ loading: "status.loading",
3990
+ off: "status.off",
3991
+ attention: "status.attention",
3992
+ ready: "status.ready"
3993
+ };
3671
3994
  /** Inline trash icon for the compact destructive row action. Kept as a tiny
3672
3995
  * element (no icon dependency) so the client bundle stays dependency-free
3673
3996
  * apart from React. */
@@ -3721,68 +4044,19 @@ window.__ModuleLoader__.load({
3721
4044
  textarea.remove();
3722
4045
  if (!copied) throw new Error(t("clipboard.rejected"));
3723
4046
  }
3724
- /** Visual state of the embedded Funnel: a colored dot plus its spoken label.
3725
- * The label is a function because the surrounding constant lives at module
3726
- * scope where the locale-bound t() is not in scope; the page resolves the
3727
- * label at render time via t(props.t, ...). */
3728
- const REMOTE_PHASE_META = {
3729
- disabled: {
3730
- dot: "",
3731
- labelKey: "phase.disabled"
3732
- },
3733
- starting: {
3734
- dot: " pbb-dotWarn",
3735
- labelKey: "phase.starting"
3736
- },
3737
- login_required: {
3738
- dot: " pbb-dotWarn",
3739
- labelKey: "phase.login_required"
3740
- },
3741
- online: {
3742
- dot: " pbb-dotOk",
3743
- labelKey: "phase.online"
3744
- },
3745
- error: {
3746
- dot: " pbb-dotBad",
3747
- labelKey: "phase.error"
3748
- },
3749
- unavailable: {
3750
- dot: " pbb-dotBad",
3751
- labelKey: "phase.unavailable"
3752
- },
3753
- stopped: {
3754
- dot: "",
3755
- labelKey: "phase.stopped"
3756
- }
3757
- };
3758
- const LOCAL_PHASE_META = {
3759
- disabled: {
3760
- dot: "",
3761
- labelKey: "local.phaseDisabled"
3762
- },
3763
- starting: {
3764
- dot: " pbb-dotWarn",
3765
- labelKey: "local.phaseStarting"
3766
- },
3767
- online: {
3768
- dot: " pbb-dotOk",
3769
- labelKey: "local.phaseOnline"
3770
- },
3771
- error: {
3772
- dot: " pbb-dotBad",
3773
- labelKey: "local.phaseError"
3774
- },
3775
- stopped: {
3776
- dot: "",
3777
- labelKey: "local.phaseStopped"
3778
- }
3779
- };
3780
- /** Slot component: hooks come from the slot renderer, named use<Key>. */
3781
- function DeepPilotSettingsPage(props) {
3782
- const [remoteLimitDraft, setRemoteLimitDraft] = (0, react.useState)(String(8));
3783
- const [remoteLimitMessage, setRemoteLimitMessage] = (0, react.useState)("");
3784
- const [localPortDraft, setLocalPortDraft] = (0, react.useState)(String(DEFAULT_LOCAL_PORT));
3785
- const [localPortMessage, setLocalPortMessage] = (0, react.useState)("");
4047
+ /**
4048
+ * Slot component: hooks come from the slot renderer, named use<Key>.
4049
+ *
4050
+ * 组件的接口是 `SettingsPageProps`(见 settings-props.ts):宿主注入面与页面
4051
+ * 共享同一份类型。这里只做一次归一化,之后全程消费类型化对象——页面里不再
4052
+ * 出现任何 typeof 守卫,也不再自己拼诊断行。
4053
+ */
4054
+ function DeepPilotSettingsPage(rawProps) {
4055
+ const surface = normalizeSettingsPageProps(rawProps);
4056
+ const props = surface.props;
4057
+ const can = (capability) => surface.can.has(capability);
4058
+ const [remoteLimitDraft, dispatchRemoteLimitDraft] = (0, react.useReducer)((state, action) => numericDraftReduce(state, action, REMOTE_LIMIT_RANGE), initialNumericDraft(REMOTE_LIMIT_RANGE));
4059
+ const [localPortDraft, dispatchLocalPortDraft] = (0, react.useReducer)((state, action) => numericDraftReduce(state, action, LOCAL_PORT_RANGE), initialNumericDraft(LOCAL_PORT_RANGE));
3786
4060
  const [selectedPairingHost, setSelectedPairingHost] = (0, react.useState)(null);
3787
4061
  const [pairingPanel, dispatchPairingPanel] = (0, react.useReducer)(pairingPanelReduce, initialPairingPanelState);
3788
4062
  const qrRequestId = (0, react.useRef)(0);
@@ -3799,14 +4073,15 @@ window.__ModuleLoader__.load({
3799
4073
  const [editingDeviceId, setEditingDeviceId] = (0, react.useState)(null);
3800
4074
  const [deviceNameDraft, setDeviceNameDraft] = (0, react.useState)("");
3801
4075
  (0, react.useEffect)(() => {
3802
- if (typeof props.refresh !== "function") return;
3803
- props.refresh();
3804
- const timer = globalThis.setInterval(() => props.refresh(), 3e3);
4076
+ if (!can("refresh")) return;
4077
+ const refresh = props.refresh;
4078
+ refresh();
4079
+ const timer = globalThis.setInterval(() => refresh(), 3e3);
3805
4080
  return () => globalThis.clearInterval(timer);
3806
4081
  }, [props.refresh]);
3807
4082
  const sendPushTest = () => {
3808
4083
  if (pushTestBusy) return;
3809
- if (typeof props.testPush !== "function") {
4084
+ if (!can("testPush")) {
3810
4085
  setPushTestError(translateWith(props.t, "push.staleHost"));
3811
4086
  return;
3812
4087
  }
@@ -3823,7 +4098,7 @@ window.__ModuleLoader__.load({
3823
4098
  };
3824
4099
  const runRelayTest = () => {
3825
4100
  if (relayTestBusy) return;
3826
- if (typeof props.testRelay !== "function") {
4101
+ if (!can("testRelay")) {
3827
4102
  setRelayTestError(translateWith(props.t, "push.staleHostRelay"));
3828
4103
  return;
3829
4104
  }
@@ -3839,15 +4114,15 @@ window.__ModuleLoader__.load({
3839
4114
  });
3840
4115
  };
3841
4116
  (0, react.useEffect)(() => {
3842
- if (!remoteLimitMessage) return;
3843
- const timer = globalThis.setTimeout(() => setRemoteLimitMessage(""), 4e3);
4117
+ if (remoteLimitDraft.message === "") return;
4118
+ const timer = globalThis.setTimeout(() => dispatchRemoteLimitDraft({ type: "applied" }), 4e3);
3844
4119
  return () => globalThis.clearTimeout(timer);
3845
- }, [remoteLimitMessage]);
4120
+ }, [remoteLimitDraft.message]);
3846
4121
  (0, react.useEffect)(() => {
3847
- if (!localPortMessage) return;
3848
- const timer = globalThis.setTimeout(() => setLocalPortMessage(""), 4e3);
4122
+ if (localPortDraft.message === "") return;
4123
+ const timer = globalThis.setTimeout(() => dispatchLocalPortDraft({ type: "applied" }), 4e3);
3849
4124
  return () => globalThis.clearTimeout(timer);
3850
- }, [localPortMessage]);
4125
+ }, [localPortDraft.message]);
3851
4126
  (0, react.useEffect)(() => {
3852
4127
  if (pairingPanel.grant === null) return;
3853
4128
  const remaining = Math.max(0, pairingPanel.grant.expiresAt - Date.now());
@@ -3864,112 +4139,123 @@ window.__ModuleLoader__.load({
3864
4139
  const timer = globalThis.setTimeout(() => dispatchPairingPanel({ type: "dismissNotice" }), 2500);
3865
4140
  return () => globalThis.clearTimeout(timer);
3866
4141
  }, [pairingPanel.notice]);
3867
- const diag = [];
3868
- let report = null;
3869
- let enabled = true;
3870
- let switchReady = false;
3871
- let localEnabled = true;
3872
- let localSwitchReady = false;
3873
- let localPort = DEFAULT_LOCAL_PORT;
3874
- let localPortReady = false;
3875
- let remoteEnabled = false;
3876
- let remoteSwitchReady = false;
3877
- let remoteConnectionLimit = 8;
3878
- let remoteConnectionLimitReady = false;
3879
- let debugEnabled = false;
3880
- let debugSwitchReady = false;
3881
- let failed = false;
3882
- try {
3883
- if (typeof props.useDeepPilotReport !== "function") {
3884
- diag.push(translateWith(props.t, "diag.missingReportHook"));
4142
+ const diag = surface.diagnostics;
4143
+ let failed = surface.reportHookMissing;
4144
+ const readHooks = () => {
4145
+ const reportState = props.useDeepPilotReport((s) => s);
4146
+ hooks.report = reportState.report;
4147
+ if (reportState.status === "error" && reportState.message) {
4148
+ diag.push(reportState.message);
3885
4149
  failed = true;
3886
- } else {
3887
- const state = props.useDeepPilotReport((s) => s);
3888
- report = state.report;
3889
- if (state.status === "error" && state.message) {
3890
- diag.push(state.message);
3891
- failed = true;
3892
- }
3893
4150
  }
3894
- if (typeof props.useDeepPilotEnabled === "function") {
3895
- const state = props.useDeepPilotEnabled((s) => s);
3896
- enabled = state.enabled;
3897
- switchReady = state.status === "ready";
3898
- if (state.status === "unavailable") diag.push(translateWith(props.t, "diag.settingsUnavailable"));
3899
- } else diag.push(translateWith(props.t, "diag.missingEnabledHook"));
3900
- if (typeof props.refresh !== "function") diag.push(translateWith(props.t, "diag.missingRefresh"));
3901
- if (typeof props.beginPairing !== "function") diag.push(translateWith(props.t, "diag.missingReveal"));
3902
- if (typeof props.revokeDevice !== "function") diag.push(translateWith(props.t, "diag.missingRotate"));
3903
- if (typeof props.setDeviceName !== "function") diag.push(translateWith(props.t, "diag.missingRename"));
3904
- if (typeof props.testRelay !== "function") diag.push(translateWith(props.t, "diag.missingTestRelay"));
3905
- if (typeof props.testPush !== "function") diag.push(translateWith(props.t, "diag.missingTestPush"));
3906
- if (typeof props.setDeepPilotEnabled !== "function") diag.push(translateWith(props.t, "diag.missingSetEnabled"));
3907
- if (typeof props.useDeepPilotLocalEnabled === "function") {
3908
- const state = props.useDeepPilotLocalEnabled((s) => s);
3909
- localEnabled = state.enabled;
3910
- localSwitchReady = state.status === "ready";
3911
- } else diag.push(translateWith(props.t, "diag.missingLocalEnabledHook"));
3912
- if (typeof props.useDeepPilotLocalPort === "function") {
3913
- const state = props.useDeepPilotLocalPort((s) => s);
3914
- localPort = state.value;
3915
- localPortReady = state.status === "ready";
3916
- } else diag.push(translateWith(props.t, "diag.missingLocalPortHook"));
3917
- if (typeof props.setDeepPilotLocalEnabled !== "function") diag.push(translateWith(props.t, "diag.missingSetLocal"));
3918
- if (typeof props.setDeepPilotLocalPort !== "function") diag.push(translateWith(props.t, "diag.missingSetLocalPort"));
3919
- if (typeof props.useDeepPilotRemoteEnabled === "function") {
3920
- const state = props.useDeepPilotRemoteEnabled((s) => s);
3921
- remoteEnabled = state.enabled;
3922
- remoteSwitchReady = state.status === "ready";
3923
- } else diag.push(translateWith(props.t, "diag.missingRemoteEnabledHook"));
3924
- if (typeof props.setDeepPilotRemoteEnabled !== "function") diag.push(translateWith(props.t, "diag.missingSetRemote"));
3925
- if (typeof props.useDeepPilotRemoteConnectionLimit === "function") {
3926
- const state = props.useDeepPilotRemoteConnectionLimit((s) => s);
3927
- remoteConnectionLimit = state.value;
3928
- remoteConnectionLimitReady = state.status === "ready";
3929
- } else diag.push(translateWith(props.t, "diag.missingRemoteLimitHook"));
3930
- if (typeof props.setDeepPilotRemoteConnectionLimit !== "function") diag.push(translateWith(props.t, "diag.missingSetRemoteLimit"));
3931
- if (typeof props.useDeepPilotDebug === "function") {
3932
- const state = props.useDeepPilotDebug((s) => s);
3933
- debugEnabled = state.enabled;
3934
- debugSwitchReady = state.status === "ready";
3935
- } else diag.push(translateWith(props.t, "diag.missingDebugHook"));
3936
- if (typeof props.setDeepPilotDebug !== "function") diag.push(translateWith(props.t, "diag.missingSetDebug"));
4151
+ const enabledState = props.useDeepPilotEnabled((s) => s);
4152
+ hooks.enabled = enabledState.enabled;
4153
+ hooks.switchReady = enabledState.status === "ready";
4154
+ if (enabledState.status === "unavailable") diag.push(translateWith(props.t, "diag.settingsUnavailable"));
4155
+ const localEnabledState = props.useDeepPilotLocalEnabled((s) => s);
4156
+ hooks.localEnabled = localEnabledState.enabled;
4157
+ hooks.localSwitchReady = localEnabledState.status === "ready";
4158
+ const localPortState = props.useDeepPilotLocalPort((s) => s);
4159
+ hooks.localPort = localPortState.value;
4160
+ hooks.localPortReady = localPortState.status === "ready";
4161
+ const remoteEnabledState = props.useDeepPilotRemoteEnabled((s) => s);
4162
+ hooks.remoteEnabled = remoteEnabledState.enabled;
4163
+ hooks.remoteSwitchReady = remoteEnabledState.status === "ready";
4164
+ const remoteLimitState = props.useDeepPilotRemoteConnectionLimit((s) => s);
4165
+ hooks.remoteConnectionLimit = remoteLimitState.value;
4166
+ hooks.remoteConnectionLimitReady = remoteLimitState.status === "ready";
4167
+ const debugState = props.useDeepPilotDebug((s) => s);
4168
+ hooks.debugEnabled = debugState.enabled;
4169
+ hooks.debugSwitchReady = debugState.status === "ready";
4170
+ };
4171
+ const hooks = {
4172
+ report: null,
4173
+ enabled: true,
4174
+ switchReady: false,
4175
+ localEnabled: true,
4176
+ localSwitchReady: false,
4177
+ localPort: DEFAULT_LOCAL_PORT,
4178
+ localPortReady: false,
4179
+ remoteEnabled: false,
4180
+ remoteSwitchReady: false,
4181
+ remoteConnectionLimit: 8,
4182
+ remoteConnectionLimitReady: false,
4183
+ debugEnabled: false,
4184
+ debugSwitchReady: false
4185
+ };
4186
+ try {
4187
+ readHooks();
3937
4188
  } catch (error) {
3938
4189
  diag.push(translateWith(props.t, "diag.renderError") + (error instanceof Error ? error.message : String(error)));
3939
4190
  failed = true;
3940
4191
  }
4192
+ const report = hooks.report;
4193
+ const enabled = hooks.enabled;
4194
+ const switchReady = hooks.switchReady;
4195
+ const localEnabled = hooks.localEnabled;
4196
+ const localSwitchReady = hooks.localSwitchReady;
4197
+ const localPort = hooks.localPort;
4198
+ const localPortReady = hooks.localPortReady;
4199
+ const remoteEnabled = hooks.remoteEnabled;
4200
+ const remoteSwitchReady = hooks.remoteSwitchReady;
4201
+ const remoteConnectionLimit = hooks.remoteConnectionLimit;
4202
+ const remoteConnectionLimitReady = hooks.remoteConnectionLimitReady;
4203
+ const debugEnabled = hooks.debugEnabled;
4204
+ const debugSwitchReady = hooks.debugSwitchReady;
3941
4205
  (0, react.useEffect)(() => {
3942
- setRemoteLimitDraft(String(remoteConnectionLimit));
4206
+ dispatchRemoteLimitDraft({
4207
+ type: "sync",
4208
+ value: remoteConnectionLimit
4209
+ });
3943
4210
  }, [remoteConnectionLimit]);
3944
4211
  (0, react.useEffect)(() => {
3945
- setLocalPortDraft(String(localPort));
4212
+ dispatchLocalPortDraft({
4213
+ type: "sync",
4214
+ value: localPort
4215
+ });
3946
4216
  }, [localPort]);
3947
- const parsedLocalPort = Number(localPortDraft);
3948
- const localPortValid = Number.isInteger(parsedLocalPort) && parsedLocalPort >= 1024 && parsedLocalPort <= 65535;
4217
+ const localPortView = numericDraftView(localPortDraft, localPort, LOCAL_PORT_RANGE);
4218
+ const remoteLimitView = numericDraftView(remoteLimitDraft, remoteConnectionLimit, REMOTE_LIMIT_RANGE);
3949
4219
  const applyLocalPort = () => {
3950
- if (!localPortValid || typeof props.setDeepPilotLocalPort !== "function") {
3951
- setLocalPortMessage(translateWith(props.t, "local.portInvalid"));
4220
+ if (!localPortView.valid || !can("setLocalPort")) {
4221
+ dispatchLocalPortDraft({
4222
+ type: "failed",
4223
+ message: translateWith(props.t, "local.portInvalid")
4224
+ });
3952
4225
  return;
3953
4226
  }
3954
- setLocalPortMessage("");
3955
- props.setDeepPilotLocalPort(parsedLocalPort).then(() => {
3956
- setLocalPortMessage(translateWith(props.t, "local.portApplied"));
4227
+ dispatchLocalPortDraft({ type: "applied" });
4228
+ Promise.resolve(props.setDeepPilotLocalPort(localPortView.value)).then(() => {
4229
+ dispatchLocalPortDraft({
4230
+ type: "succeeded",
4231
+ message: translateWith(props.t, "local.portApplied")
4232
+ });
3957
4233
  }, (error) => {
3958
- setLocalPortMessage(translateWith(props.t, "local.portFailed") + (error instanceof Error ? error.message : String(error)));
4234
+ dispatchLocalPortDraft({
4235
+ type: "failed",
4236
+ message: translateWith(props.t, "local.portFailed") + (error instanceof Error ? error.message : String(error))
4237
+ });
3959
4238
  });
3960
4239
  };
3961
- const parsedRemoteLimit = Number(remoteLimitDraft);
3962
- const remoteLimitValid = Number.isInteger(parsedRemoteLimit) && parsedRemoteLimit >= 1 && parsedRemoteLimit <= 16;
3963
4240
  const applyRemoteLimit = () => {
3964
- if (!remoteLimitValid || typeof props.setDeepPilotRemoteConnectionLimit !== "function") {
3965
- setRemoteLimitMessage(translateWith(props.t, "remote.limitInvalid"));
4241
+ if (!remoteLimitView.valid || !can("setRemoteLimit")) {
4242
+ dispatchRemoteLimitDraft({
4243
+ type: "failed",
4244
+ message: translateWith(props.t, "remote.limitInvalid")
4245
+ });
3966
4246
  return;
3967
4247
  }
3968
- setRemoteLimitMessage("");
3969
- props.setDeepPilotRemoteConnectionLimit(parsedRemoteLimit).then(() => {
3970
- setRemoteLimitMessage(translateWith(props.t, "remote.limitApplied"));
4248
+ dispatchRemoteLimitDraft({ type: "applied" });
4249
+ Promise.resolve(props.setDeepPilotRemoteConnectionLimit(remoteLimitView.value)).then(() => {
4250
+ dispatchRemoteLimitDraft({
4251
+ type: "succeeded",
4252
+ message: translateWith(props.t, "remote.limitApplied")
4253
+ });
3971
4254
  }, (error) => {
3972
- setRemoteLimitMessage(translateWith(props.t, "remote.limitFailed") + (error instanceof Error ? error.message : String(error)));
4255
+ dispatchRemoteLimitDraft({
4256
+ type: "failed",
4257
+ message: translateWith(props.t, "remote.limitFailed") + (error instanceof Error ? error.message : String(error))
4258
+ });
3973
4259
  });
3974
4260
  };
3975
4261
  const pairingTargets = report === null ? [] : selectPairingTargets(report.local, report.remote);
@@ -3984,7 +4270,7 @@ window.__ModuleLoader__.load({
3984
4270
  * switching between LAN and public always mints a fresh one.
3985
4271
  */
3986
4272
  const showPairingQR = (target) => {
3987
- if (typeof props.beginPairing !== "function") return;
4273
+ if (!can("beginPairing")) return;
3988
4274
  const requestId = qrRequestId.current + 1;
3989
4275
  qrRequestId.current = requestId;
3990
4276
  dispatchPairingPanel({
@@ -4074,7 +4360,7 @@ window.__ModuleLoader__.load({
4074
4360
  }, (0, react.createElement)("div", { className: "pbb-row" }, (0, react.createElement)("span", { className: "pbb-label" }, translateWith(props.t, "advanced.bufferMax")), (0, react.createElement)("span", { className: "pbb-value" }, String(report.historyBufferMax) + translateWith(props.t, "advanced.frames")))));
4075
4361
  }
4076
4362
  const revokeDevice = (deviceId, name) => {
4077
- if (typeof props.revokeDevice !== "function") return;
4363
+ if (!can("revokeDevice")) return;
4078
4364
  if (typeof window !== "undefined" && !window.confirm(translateWith(props.t, "devices.revokeConfirm", { name }))) return;
4079
4365
  setDeviceBusy(deviceId);
4080
4366
  setDeviceBusyAction("revoke");
@@ -4088,7 +4374,7 @@ window.__ModuleLoader__.load({
4088
4374
  setDeviceBusyAction(null);
4089
4375
  });
4090
4376
  };
4091
- const deviceLabel = (device) => (device.customName ?? device.deviceName).trim() || translateWith(props.t, "devices.unnamed");
4377
+ const deviceLabel$1 = (device) => deviceLabel(device) || translateWith(props.t, "devices.unnamed");
4092
4378
  const beginDeviceRename = (device) => {
4093
4379
  setEditingDeviceId(device.deviceId);
4094
4380
  setDeviceNameDraft(device.customName ?? device.deviceName);
@@ -4100,7 +4386,7 @@ window.__ModuleLoader__.load({
4100
4386
  setDeviceMessage("");
4101
4387
  };
4102
4388
  const saveDeviceName = (deviceId) => {
4103
- if (typeof props.setDeviceName !== "function") return;
4389
+ if (!can("setDeviceName")) return;
4104
4390
  const normalized = deviceNameDraft.trim();
4105
4391
  if (normalized.length > 64) {
4106
4392
  setDeviceMessage(translateWith(props.t, "devices.nameInvalid"));
@@ -4120,11 +4406,11 @@ window.__ModuleLoader__.load({
4120
4406
  setDeviceBusyAction(null);
4121
4407
  });
4122
4408
  };
4123
- const visibleDevices = report?.devices.filter((device) => device.revokedAt === void 0) ?? [];
4124
- const deviceTable = visibleDevices.length > 0 ? (0, react.createElement)("div", { className: "pbb-tableWrap" }, (0, react.createElement)("table", { className: "pbb-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, translateWith(props.t, "devices.col.name")), (0, react.createElement)("th", null, translateWith(props.t, "devices.col.lastSeen")), (0, react.createElement)("th", { className: "pbb-tableActionHead" }, translateWith(props.t, "devices.col.actions")))), (0, react.createElement)("tbody", null, visibleDevices.map((d) => {
4409
+ const visibleDevices$1 = visibleDevices(report);
4410
+ const deviceTable = visibleDevices$1.length > 0 ? (0, react.createElement)("div", { className: "pbb-tableWrap" }, (0, react.createElement)("table", { className: "pbb-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, (0, react.createElement)("th", null, translateWith(props.t, "devices.col.name")), (0, react.createElement)("th", null, translateWith(props.t, "devices.col.lastSeen")), (0, react.createElement)("th", { className: "pbb-tableActionHead" }, translateWith(props.t, "devices.col.actions")))), (0, react.createElement)("tbody", null, visibleDevices$1.map((d) => {
4125
4411
  const editing = editingDeviceId === d.deviceId;
4126
4412
  const busy = deviceBusy === d.deviceId;
4127
- const label = deviceLabel(d);
4413
+ const label = deviceLabel$1(d);
4128
4414
  return (0, react.createElement)("tr", {
4129
4415
  key: d.deviceId,
4130
4416
  "aria-busy": busy
@@ -4169,10 +4455,10 @@ window.__ModuleLoader__.load({
4169
4455
  title: translateWith(props.t, "devices.nameCancel"),
4170
4456
  "aria-label": translateWith(props.t, "devices.nameCancel"),
4171
4457
  onClick: cancelDeviceRename
4172
- }, CloseIcon()))), deviceNameDraft.length > 64 ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "devices.nameInvalid")) : null) : (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "pbb-deviceNameRow" }, (0, react.createElement)("span", { className: "pbb-deviceName" }, label), d.customName ? (0, react.createElement)("span", { className: "pbb-badge" }, translateWith(props.t, "devices.customBadge")) : null), (0, react.createElement)("span", { className: "pbb-deviceMeta" }, d.appVersion, " · ", (0, react.createElement)("code", null, d.fingerprint.slice(0, 12))))), (0, react.createElement)("td", { className: "pbb-lastSeen" }, new Date(d.lastSeenTs).toLocaleString()), (0, react.createElement)("td", { className: "pbb-tableActionCell" }, !editing ? (0, react.createElement)("button", {
4458
+ }, CloseIcon()))), deviceNameDraft.length > 64 ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "devices.nameInvalid")) : null) : (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "pbb-deviceNameRow" }, (0, react.createElement)("span", { className: "pbb-deviceName" }, label), d.customName ? (0, react.createElement)("span", { className: "pbb-badge" }, translateWith(props.t, "devices.customBadge")) : null), (0, react.createElement)("span", { className: "pbb-deviceMeta" }, d.appVersion, " · ", (0, react.createElement)("code", null, (d.fingerprint ?? "").slice(0, 12))))), (0, react.createElement)("td", { className: "pbb-lastSeen" }, new Date(d.lastSeenTs ?? 0).toLocaleString()), (0, react.createElement)("td", { className: "pbb-tableActionCell" }, !editing ? (0, react.createElement)("button", {
4173
4459
  type: "button",
4174
4460
  className: "pbb-action",
4175
- disabled: busy || editingDeviceId !== null || typeof props.setDeviceName !== "function",
4461
+ disabled: busy || editingDeviceId !== null || !can("setDeviceName"),
4176
4462
  "aria-label": translateWith(props.t, "devices.renameAria", { name: label }),
4177
4463
  onClick: () => beginDeviceRename(d)
4178
4464
  }, translateWith(props.t, "devices.rename")) : null, (0, react.createElement)("button", {
@@ -4187,34 +4473,21 @@ window.__ModuleLoader__.load({
4187
4473
  const switchDesc = switchReady ? enabled ? translateWith(props.t, "master.on") : translateWith(props.t, "master.off") : translateWith(props.t, "master.loading");
4188
4474
  const localMeta = report === null ? null : LOCAL_PHASE_META[report.local.phase];
4189
4475
  const remoteMeta = report === null ? null : REMOTE_PHASE_META[report.remote.phase];
4190
- const connectionNeedsAttention = localMeta?.dot.includes("pbb-dotBad") === true || remoteMeta?.dot.includes("pbb-dotBad") === true;
4191
- const connectionOnline = localMeta?.dot.includes("pbb-dotOk") === true || remoteMeta?.dot.includes("pbb-dotOk") === true;
4192
- const serviceStatus = !switchReady ? translateWith(props.t, "status.loading") : !enabled ? translateWith(props.t, "status.off") : connectionNeedsAttention ? translateWith(props.t, "status.attention") : connectionOnline ? translateWith(props.t, "status.ready") : translateWith(props.t, "status.loading");
4193
- const serviceDot = !enabled ? "" : connectionNeedsAttention ? " pbb-dotBad" : connectionOnline ? " pbb-dotOk" : " pbb-dotWarn";
4194
- const renderConnectionTile = (key, title, description, checked, ready, dot, statusLabel, onToggle, extra = null) => (0, react.createElement)("div", {
4195
- className: "pbb-connectionTile",
4196
- key
4197
- }, (0, react.createElement)("div", { className: "pbb-connectionTileHeader" }, (0, react.createElement)("span", { className: "pbb-switchTitle pbb-dotRow" }, (0, react.createElement)("span", {
4198
- className: "pbb-dot" + dot,
4199
- role: "img",
4200
- "aria-label": statusLabel,
4201
- title: statusLabel
4202
- }), title), (0, react.createElement)("button", {
4203
- type: "button",
4204
- role: "switch",
4205
- "aria-checked": checked,
4206
- "aria-label": title,
4207
- disabled: !ready,
4208
- className: "pbb-switch" + (checked ? " pbb-switchOn" : ""),
4209
- onClick: onToggle
4210
- })), (0, react.createElement)("span", { className: "pbb-connectionState" }, statusLabel), (0, react.createElement)("p", { className: "pbb-connectionDescription" }, description), extra);
4476
+ const service = serviceStatus({
4477
+ enabled,
4478
+ switchReady,
4479
+ localDot: localMeta?.dot,
4480
+ remoteDot: remoteMeta?.dot
4481
+ });
4482
+ const serviceStatusText = translateWith(props.t, SERVICE_STATUS_KEY[service.kind]);
4483
+ const serviceDot = service.dot;
4211
4484
  return (0, react.createElement)("div", { className: "pbb-section" }, (0, react.createElement)("header", { className: "pbb-pageHeader" }, (0, react.createElement)("div", null, (0, react.createElement)("h2", { className: "pbb-title" }, "DeepPilot"), (0, react.createElement)("p", { className: "pbb-intro" }, translateWith(props.t, "meta.intro"))), (0, react.createElement)("button", {
4212
4485
  type: "button",
4213
4486
  className: "pbb-refresh",
4214
4487
  onClick: () => {
4215
- if (typeof props.refresh === "function") props.refresh();
4488
+ if (can("refresh")) props.refresh();
4216
4489
  }
4217
- }, translateWith(props.t, "meta.refresh"))), (0, react.createElement)("div", { className: "pbb-card pbb-primaryCard" }, (0, react.createElement)("div", { className: "pbb-cardHeader" }, (0, react.createElement)("div", null, (0, react.createElement)("h3", { className: "pbb-cardTitle" }, translateWith(props.t, "connection.title")), (0, react.createElement)("p", { className: "pbb-cardDescription" }, translateWith(props.t, "connection.description"))), (0, react.createElement)("span", { className: "pbb-statusBadge" }, (0, react.createElement)("span", { className: "pbb-dot" + serviceDot }), serviceStatus)), (0, react.createElement)("div", { className: "pbb-masterRow" }, (0, react.createElement)("div", { className: "pbb-switchText" }, (0, react.createElement)("span", { className: "pbb-switchTitle" }, switchTitle), (0, react.createElement)("span", { className: "pbb-switchDesc" }, switchDesc)), (0, react.createElement)("button", {
4490
+ }, translateWith(props.t, "meta.refresh"))), (0, react.createElement)("div", { className: "pbb-card pbb-primaryCard" }, (0, react.createElement)("div", { className: "pbb-cardHeader" }, (0, react.createElement)("div", null, (0, react.createElement)("h3", { className: "pbb-cardTitle" }, translateWith(props.t, "connection.title")), (0, react.createElement)("p", { className: "pbb-cardDescription" }, translateWith(props.t, "connection.description"))), (0, react.createElement)("span", { className: "pbb-statusBadge" }, (0, react.createElement)("span", { className: "pbb-dot" + serviceDot }), serviceStatusText)), (0, react.createElement)("div", { className: "pbb-masterRow" }, (0, react.createElement)("div", { className: "pbb-switchText" }, (0, react.createElement)("span", { className: "pbb-switchTitle" }, switchTitle), (0, react.createElement)("span", { className: "pbb-switchDesc" }, switchDesc)), (0, react.createElement)("button", {
4218
4491
  type: "button",
4219
4492
  role: "switch",
4220
4493
  "aria-checked": enabled,
@@ -4222,12 +4495,12 @@ window.__ModuleLoader__.load({
4222
4495
  disabled: !switchReady,
4223
4496
  className: "pbb-switch" + (enabled ? " pbb-switchOn" : ""),
4224
4497
  onClick: () => {
4225
- if (typeof props.setDeepPilotEnabled === "function") props.setDeepPilotEnabled(!enabled);
4498
+ if (can("setEnabled")) props.setDeepPilotEnabled(!enabled);
4226
4499
  }
4227
- })), (0, react.createElement)("div", { className: "pbb-connectionGrid" }, renderConnectionTile("local", translateWith(props.t, "local.title"), localSwitchReady ? localEnabled ? translateWith(props.t, "local.on", { port: localPort }) : translateWith(props.t, "local.off") : translateWith(props.t, "master.loading"), localEnabled, localSwitchReady, localMeta?.dot ?? "", localMeta === null ? translateWith(props.t, "phase.unknown") : translateWith(props.t, localMeta.labelKey), () => {
4228
- if (typeof props.setDeepPilotLocalEnabled === "function") props.setDeepPilotLocalEnabled(!localEnabled);
4229
- }, (0, react.createElement)(react.Fragment, null, report !== null && report.local.message && report.local.phase === "error" ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, report.local.message) : null, report !== null && report.local.tlsIdentityRegenerated === true ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "local.tlsRegenerated")) : null)), renderConnectionTile("remote", translateWith(props.t, "remote.title"), remoteSwitchReady ? remoteEnabled ? translateWith(props.t, "remote.on") : translateWith(props.t, "remote.off") : translateWith(props.t, "master.loading"), remoteEnabled, remoteSwitchReady, remoteMeta?.dot ?? "", remoteMeta === null ? translateWith(props.t, "phase.unknown") : translateWith(props.t, remoteMeta.labelKey), () => {
4230
- if (typeof props.setDeepPilotRemoteEnabled === "function") props.setDeepPilotRemoteEnabled(!remoteEnabled);
4500
+ })), (0, react.createElement)("div", { className: "pbb-connectionGrid" }, renderConnectionTile("local", translateWith(props.t, "local.title"), localSwitchReady ? localEnabled ? translateWith(props.t, "local.on", { port: localPort }) : translateWith(props.t, "local.off") : translateWith(props.t, "master.loading"), localEnabled, localSwitchReady, localMeta?.dot ?? "", localMeta === null ? translateWith(props.t, "phase.unknown") : translateWith(props.t, LOCAL_LABEL_KEY[localMeta.label]), () => {
4501
+ if (can("setLocalEnabled")) props.setDeepPilotLocalEnabled(!localEnabled);
4502
+ }, (0, react.createElement)(react.Fragment, null, report !== null && report.local.message && report.local.phase === "error" ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, report.local.message) : null, report !== null && report.local.tlsIdentityRegenerated === true ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "local.tlsRegenerated")) : null)), renderConnectionTile("remote", translateWith(props.t, "remote.title"), remoteSwitchReady ? remoteEnabled ? translateWith(props.t, "remote.on") : translateWith(props.t, "remote.off") : translateWith(props.t, "master.loading"), remoteEnabled, remoteSwitchReady, remoteMeta?.dot ?? "", remoteMeta === null ? translateWith(props.t, "phase.unknown") : translateWith(props.t, REMOTE_LABEL_KEY[remoteMeta.label]), () => {
4503
+ if (can("setRemoteEnabled")) props.setDeepPilotRemoteEnabled(!remoteEnabled);
4231
4504
  }, (0, react.createElement)(react.Fragment, null, report !== null && report.remote.phase === "login_required" && typeof report.remote.authURL === "string" && report.remote.authURL.startsWith("https://") ? (0, react.createElement)("div", { className: "pbb-rowAction" }, (0, react.createElement)("a", {
4232
4505
  className: "pbb-action pbb-buttonPrimary",
4233
4506
  href: report.remote.authURL,
@@ -4236,18 +4509,21 @@ window.__ModuleLoader__.load({
4236
4509
  }, translateWith(props.t, "remote.openAuth"))) : null, report !== null && report.remote.message && (report.remote.phase === "error" || report.remote.phase === "unavailable") ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, report.remote.message) : null))), report === null ? null : (0, react.createElement)("div", { className: "pbb-statGrid" }, primaryRows), (0, react.createElement)("details", { className: "pbb-inlineDetails" }, (0, react.createElement)("summary", null, translateWith(props.t, "connection.optionsSummary")), (0, react.createElement)("div", { className: "pbb-helpBody" }, (0, react.createElement)("div", { className: "pbb-limitRow pbb-limitNested" }, (0, react.createElement)("div", { className: "pbb-switchText" }, (0, react.createElement)("label", {
4237
4510
  className: "pbb-switchTitle",
4238
4511
  htmlFor: "deeppilot-local-port"
4239
- }, translateWith(props.t, "local.portTitle")), (0, react.createElement)("span", { className: "pbb-switchDesc" }, translateWith(props.t, "local.portDescription")), !localPortValid ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "local.portInvalid")) : localPortMessage ? (0, react.createElement)("p", { className: "pbb-diag" + (localPortMessage.startsWith(translateWith(props.t, "local.portFailed")) ? " pbb-diagBad" : "") }, localPortMessage) : null), (0, react.createElement)("div", { className: "pbb-limitControl" }, (0, react.createElement)("input", {
4512
+ }, translateWith(props.t, "local.portTitle")), (0, react.createElement)("span", { className: "pbb-switchDesc" }, translateWith(props.t, "local.portDescription")), !localPortView.valid ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "local.portInvalid")) : localPortDraft.message ? (0, react.createElement)("p", { className: "pbb-diag" + (localPortDraft.failed ? " pbb-diagBad" : "") }, localPortDraft.message) : null), (0, react.createElement)("div", { className: "pbb-limitControl" }, (0, react.createElement)("input", {
4240
4513
  id: "deeppilot-local-port",
4241
4514
  className: "pbb-numberInput",
4242
4515
  type: "number",
4243
- min: MIN_LOCAL_PORT,
4244
- max: MAX_LOCAL_PORT,
4516
+ min: LOCAL_PORT_RANGE.min,
4517
+ max: LOCAL_PORT_RANGE.max,
4245
4518
  step: 1,
4246
4519
  inputMode: "numeric",
4247
- value: localPortDraft,
4520
+ value: localPortDraft.draft,
4248
4521
  disabled: !localPortReady,
4249
4522
  "aria-label": translateWith(props.t, "local.portTitle"),
4250
- onChange: (event) => setLocalPortDraft(event.currentTarget.value),
4523
+ onChange: (event) => dispatchLocalPortDraft({
4524
+ type: "edit",
4525
+ draft: event.currentTarget.value
4526
+ }),
4251
4527
  onKeyDown: (event) => {
4252
4528
  if (event.key === "Enter") {
4253
4529
  event.preventDefault();
@@ -4257,23 +4533,26 @@ window.__ModuleLoader__.load({
4257
4533
  }), (0, react.createElement)("button", {
4258
4534
  type: "button",
4259
4535
  className: "pbb-action",
4260
- disabled: !localPortReady || !localPortValid || parsedLocalPort === localPort,
4536
+ disabled: !localPortReady || !localPortView.valid || localPortView.unchanged,
4261
4537
  onClick: applyLocalPort
4262
4538
  }, translateWith(props.t, "remote.limitApply")))), (0, react.createElement)("div", { className: "pbb-limitRow pbb-limitNested" }, (0, react.createElement)("div", { className: "pbb-switchText" }, (0, react.createElement)("label", {
4263
4539
  className: "pbb-switchTitle",
4264
4540
  htmlFor: "deeppilot-funnel-source-limit"
4265
- }, translateWith(props.t, "remote.limitTitle")), (0, react.createElement)("span", { className: "pbb-switchDesc" }, translateWith(props.t, "remote.limitDescription")), !remoteLimitValid ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "remote.limitInvalid")) : remoteLimitMessage ? (0, react.createElement)("p", { className: "pbb-diag" + (remoteLimitMessage.startsWith(translateWith(props.t, "remote.limitFailed")) ? " pbb-diagBad" : "") }, remoteLimitMessage) : null), (0, react.createElement)("div", { className: "pbb-limitControl" }, (0, react.createElement)("input", {
4541
+ }, translateWith(props.t, "remote.limitTitle")), (0, react.createElement)("span", { className: "pbb-switchDesc" }, translateWith(props.t, "remote.limitDescription")), !remoteLimitView.valid ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, translateWith(props.t, "remote.limitInvalid")) : remoteLimitDraft.message ? (0, react.createElement)("p", { className: "pbb-diag" + (remoteLimitDraft.failed ? " pbb-diagBad" : "") }, remoteLimitDraft.message) : null), (0, react.createElement)("div", { className: "pbb-limitControl" }, (0, react.createElement)("input", {
4266
4542
  id: "deeppilot-funnel-source-limit",
4267
4543
  className: "pbb-numberInput",
4268
4544
  type: "number",
4269
4545
  min: 1,
4270
- max: 16,
4546
+ max: REMOTE_LIMIT_RANGE.max,
4271
4547
  step: 1,
4272
4548
  inputMode: "numeric",
4273
- value: remoteLimitDraft,
4549
+ value: remoteLimitDraft.draft,
4274
4550
  disabled: !remoteConnectionLimitReady,
4275
4551
  "aria-label": translateWith(props.t, "remote.limitTitle"),
4276
- onChange: (event) => setRemoteLimitDraft(event.currentTarget.value),
4552
+ onChange: (event) => dispatchRemoteLimitDraft({
4553
+ type: "edit",
4554
+ draft: event.currentTarget.value
4555
+ }),
4277
4556
  onKeyDown: (event) => {
4278
4557
  if (event.key === "Enter") {
4279
4558
  event.preventDefault();
@@ -4283,7 +4562,7 @@ window.__ModuleLoader__.load({
4283
4562
  }), (0, react.createElement)("button", {
4284
4563
  type: "button",
4285
4564
  className: "pbb-action",
4286
- disabled: !remoteConnectionLimitReady || !remoteLimitValid || parsedRemoteLimit === remoteConnectionLimit,
4565
+ disabled: !remoteConnectionLimitReady || !remoteLimitView.valid || remoteLimitView.unchanged,
4287
4566
  onClick: applyRemoteLimit
4288
4567
  }, translateWith(props.t, "remote.limitApply")))))), (0, react.createElement)("details", { className: "pbb-help" }, (0, react.createElement)("summary", null, translateWith(props.t, "help.remoteTitle")), (0, react.createElement)("div", { className: "pbb-helpBody" }, (0, react.createElement)("div", { className: "pbb-helpSection" }, (0, react.createElement)("div", { className: "pbb-helpHeading" }, translateWith(props.t, "help.recommended")), (0, react.createElement)("ol", { className: "pbb-helpList" }, (0, react.createElement)("li", null, translateWith(props.t, "help.step1")), (0, react.createElement)("li", null, translateWith(props.t, "help.step2")), (0, react.createElement)("li", null, translateWith(props.t, "help.step3")), (0, react.createElement)("li", null, translateWith(props.t, "help.step4"))), (0, react.createElement)("p", { className: "pbb-helpText" }, translateWith(props.t, "help.funnelHint"))), (0, react.createElement)("div", { className: "pbb-helpSection" }, (0, react.createElement)("div", { className: "pbb-helpHeading" }, translateWith(props.t, "help.httpsTitle")), (0, react.createElement)("ol", { className: "pbb-helpList" }, (0, react.createElement)("li", null, translateWith(props.t, "help.httpsStep1")), (0, react.createElement)("li", null, translateWith(props.t, "help.httpsStep2")), (0, react.createElement)("li", null, translateWith(props.t, "help.httpsStep3"))), (0, react.createElement)("p", { className: "pbb-helpText" }, translateWith(props.t, "help.httpsHint"))), (0, react.createElement)("div", { className: "pbb-helpSection" }, (0, react.createElement)("div", { className: "pbb-helpHeading" }, translateWith(props.t, "help.allowTitle")), (0, react.createElement)("p", { className: "pbb-helpText" }, translateWith(props.t, "help.allowBody")), (0, react.createElement)("code", { className: "pbb-helpCode" }, "\"nodeAttrs\": [\n {\n \"target\": [\"autogroup:member\"],\n \"attr\": [\"funnel\"],\n },\n],"), (0, react.createElement)("p", { className: "pbb-helpText" }, translateWith(props.t, "help.allowHint"))), (0, react.createElement)("div", { className: "pbb-helpSection" }, (0, react.createElement)("div", { className: "pbb-helpHeading" }, translateWith(props.t, "help.faqTitle")), (0, react.createElement)("ul", { className: "pbb-helpList" }, (0, react.createElement)("li", null, translateWith(props.t, "help.faq1")), (0, react.createElement)("li", null, translateWith(props.t, "help.faq2")), (0, react.createElement)("li", null, translateWith(props.t, "help.faq3")), (0, react.createElement)("li", null, translateWith(props.t, "help.faq4")))), (0, react.createElement)("div", { className: "pbb-helpSection" }, (0, react.createElement)("div", { className: "pbb-helpHeading" }, translateWith(props.t, "help.securityTitle")), (0, react.createElement)("ul", { className: "pbb-helpList" }, (0, react.createElement)("li", null, translateWith(props.t, "help.security1")), (0, react.createElement)("li", null, translateWith(props.t, "help.security2")), (0, react.createElement)("li", null, translateWith(props.t, "help.security3")), (0, react.createElement)("li", null, translateWith(props.t, "help.security4")), (0, react.createElement)("li", null, translateWith(props.t, "help.security5"))), (0, react.createElement)("p", { className: "pbb-helpText" }, translateWith(props.t, "help.docsPrefix"), (0, react.createElement)("a", {
4289
4568
  className: "pbb-helpLink",
@@ -4322,7 +4601,7 @@ window.__ModuleLoader__.load({
4322
4601
  className: "pbb-action pbb-buttonPrimary",
4323
4602
  onClick: copyPairingInfo,
4324
4603
  "data-testid": "pairingInfoCopy"
4325
- }, translateWith(props.t, "panel.tokenAction.copy")))), (0, react.createElement)("p", { className: "pbb-qrHint" }, translateWith(props.t, "pair.qrHint", { kind: panel.target?.kind === "public" ? translateWith(props.t, "pair.kind.public") : translateWith(props.t, "pair.kind.lan") }))) : null)), (0, react.createElement)("div", { className: "pbb-card" }, (0, react.createElement)("div", { className: "pbb-cardHeader" }, (0, react.createElement)("div", null, (0, react.createElement)("h3", { className: "pbb-cardTitle" }, translateWith(props.t, "devices.title")), (0, react.createElement)("p", { className: "pbb-cardDescription" }, translateWith(props.t, "devices.description"))), (0, react.createElement)("span", { className: "pbb-countBadge" }, String(visibleDevices.length))), (0, react.createElement)("div", { className: "pbb-cardBody" }, deviceMessage ? (0, react.createElement)("p", { className: "pbb-diag" + (deviceMessage.startsWith(translateWith(props.t, "devices.renameFailed")) || deviceMessage.startsWith(translateWith(props.t, "devices.revokeFailed")) ? " pbb-diagBad" : "") }, deviceMessage) : null, deviceTable)), (0, react.createElement)("details", {
4604
+ }, translateWith(props.t, "panel.tokenAction.copy")))), (0, react.createElement)("p", { className: "pbb-qrHint" }, translateWith(props.t, "pair.qrHint", { kind: panel.target?.kind === "public" ? translateWith(props.t, "pair.kind.public") : translateWith(props.t, "pair.kind.lan") }))) : null)), (0, react.createElement)("div", { className: "pbb-card" }, (0, react.createElement)("div", { className: "pbb-cardHeader" }, (0, react.createElement)("div", null, (0, react.createElement)("h3", { className: "pbb-cardTitle" }, translateWith(props.t, "devices.title")), (0, react.createElement)("p", { className: "pbb-cardDescription" }, translateWith(props.t, "devices.description"))), (0, react.createElement)("span", { className: "pbb-countBadge" }, String(visibleDevices$1.length))), (0, react.createElement)("div", { className: "pbb-cardBody" }, deviceMessage ? (0, react.createElement)("p", { className: "pbb-diag" + (deviceMessage.startsWith(translateWith(props.t, "devices.renameFailed")) || deviceMessage.startsWith(translateWith(props.t, "devices.revokeFailed")) ? " pbb-diagBad" : "") }, deviceMessage) : null, deviceTable)), (0, react.createElement)("details", {
4326
4605
  className: "pbb-card pbb-disclosure",
4327
4606
  open: troubleshootingOpen,
4328
4607
  onToggle: (event) => setTroubleshootingOpen(event.currentTarget.open)
@@ -4344,7 +4623,7 @@ window.__ModuleLoader__.load({
4344
4623
  disabled: !debugSwitchReady,
4345
4624
  className: "pbb-switch" + (debugEnabled ? " pbb-switchOn" : ""),
4346
4625
  onClick: () => {
4347
- if (typeof props.setDeepPilotDebug === "function") props.setDeepPilotDebug(!debugEnabled);
4626
+ if (can("setDebug")) props.setDeepPilotDebug(!debugEnabled);
4348
4627
  }
4349
4628
  }))), relayTestError ? (0, react.createElement)("p", { className: "pbb-diag pbb-diagBad" }, relayTestError) : null, relayTestResult === null ? (0, react.createElement)("p", { className: "pbb-diag" }, translateWith(props.t, "push.relayDefault")) : (0, react.createElement)("div", { className: "pbb-helpBody" }, (0, react.createElement)("div", { className: "pbb-row" }, (0, react.createElement)("span", { className: "pbb-label" }, relayTestResult.url || translateWith(props.t, "push.relayUrlEmpty")), (0, react.createElement)("code", { className: "pbb-token " + (relayTestResult.overall === "ok" ? "pbb-ok" : "pbb-bad") }, relayTestResult.overall === "ok" ? translateWith(props.t, "push.relayOk") : translateWith(props.t, "push.relayBad"))), relayTestResult.steps.map((step, index) => (0, react.createElement)("p", {
4350
4629
  className: "pbb-diag",