@arcblock/did-connect-service 4.1.32 → 4.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/_generated/asset-bytes.js +1 -1
  2. package/dist/_generated/asset-manifest.js +4 -4
  3. package/dist/_generated/login-bundle-string.d.ts +1 -1
  4. package/dist/_generated/login-bundle-string.d.ts.map +1 -1
  5. package/dist/_generated/login-bundle-string.js +1 -1
  6. package/dist/_generated/login-bundle-string.js.map +1 -1
  7. package/dist/assets/admin-core.667d2962.js +2094 -0
  8. package/dist/assets/admin-core.69147606.js +2143 -0
  9. package/dist/assets/admin-core.880e42df.js +2090 -0
  10. package/dist/assets/admin-extra.979f5e56.js +2605 -0
  11. package/dist/assets/admin.21a6c77e.css +2324 -0
  12. package/dist/assets/admin.76aa0931.css +2323 -0
  13. package/dist/assets/login.566f858e.js +720 -0
  14. package/dist/handlers/team-handler.d.ts +8 -0
  15. package/dist/handlers/team-handler.d.ts.map +1 -1
  16. package/dist/handlers/team-handler.js +21 -2
  17. package/dist/handlers/team-handler.js.map +1 -1
  18. package/dist/pages/admin/scripts/dialog.d.ts +1 -1
  19. package/dist/pages/admin/scripts/dialog.d.ts.map +1 -1
  20. package/dist/pages/admin/scripts/dialog.js +29 -2
  21. package/dist/pages/admin/scripts/dialog.js.map +1 -1
  22. package/dist/pages/admin/scripts/utils.d.ts +1 -1
  23. package/dist/pages/admin/scripts/utils.d.ts.map +1 -1
  24. package/dist/pages/admin/scripts/utils.js +35 -0
  25. package/dist/pages/admin/scripts/utils.js.map +1 -1
  26. package/dist/pages/admin/styles.d.ts +1 -1
  27. package/dist/pages/admin/styles.d.ts.map +1 -1
  28. package/dist/pages/admin/styles.js +11 -0
  29. package/dist/pages/admin/styles.js.map +1 -1
  30. package/dist/pages/admin/tab-access.d.ts.map +1 -1
  31. package/dist/pages/admin/tab-access.js +6 -0
  32. package/dist/pages/admin/tab-access.js.map +1 -1
  33. package/dist/pages/admin/tab-appearance.d.ts.map +1 -1
  34. package/dist/pages/admin/tab-appearance.js +4 -0
  35. package/dist/pages/admin/tab-appearance.js.map +1 -1
  36. package/dist/pages/admin/tab-branding.d.ts.map +1 -1
  37. package/dist/pages/admin/tab-branding.js +4 -0
  38. package/dist/pages/admin/tab-branding.js.map +1 -1
  39. package/dist/pages/admin/tab-federation.d.ts +1 -1
  40. package/dist/pages/admin/tab-federation.d.ts.map +1 -1
  41. package/dist/pages/admin/tab-federation.js +21 -11
  42. package/dist/pages/admin/tab-federation.js.map +1 -1
  43. package/dist/pages/admin/tab-invitations.d.ts +1 -1
  44. package/dist/pages/admin/tab-invitations.d.ts.map +1 -1
  45. package/dist/pages/admin/tab-invitations.js +4 -0
  46. package/dist/pages/admin/tab-invitations.js.map +1 -1
  47. package/dist/pages/admin/tab-members.d.ts +1 -1
  48. package/dist/pages/admin/tab-members.d.ts.map +1 -1
  49. package/dist/pages/admin/tab-members.js +7 -0
  50. package/dist/pages/admin/tab-members.js.map +1 -1
  51. package/dist/pages/admin/tab-profile-accounts.d.ts.map +1 -1
  52. package/dist/pages/admin/tab-profile-accounts.js +10 -0
  53. package/dist/pages/admin/tab-profile-accounts.js.map +1 -1
  54. package/dist/pages/admin/tab-profile.d.ts +1 -1
  55. package/dist/pages/admin/tab-profile.d.ts.map +1 -1
  56. package/dist/pages/admin/tab-profile.js +6 -0
  57. package/dist/pages/admin/tab-profile.js.map +1 -1
  58. package/dist/pages/admin/tab-settings.d.ts.map +1 -1
  59. package/dist/pages/admin/tab-settings.js +11 -3
  60. package/dist/pages/admin/tab-settings.js.map +1 -1
  61. package/dist/pages/admin-instances-page.d.ts.map +1 -1
  62. package/dist/pages/admin-instances-page.js +5 -1
  63. package/dist/pages/admin-instances-page.js.map +1 -1
  64. package/dist/store/d1-store.d.ts.map +1 -1
  65. package/dist/store/d1-store.js +9 -0
  66. package/dist/store/d1-store.js.map +1 -1
  67. package/package.json +3 -3
@@ -3,6 +3,6 @@
3
3
  */
4
4
  type TFn = (key: string, params?: Record<string, string>) => string;
5
5
  export declare function federationSectionHtml(t: TFn): string;
6
- export declare const FEDERATION_SECTION_SCRIPT = "\n// \u2500\u2500\u2500 Federation (Unified Login) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nvar __fedData = null;\nvar __fedLoading = false;\n\nvar FED_ICON_REMOVE = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"/><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"/></svg>';\nvar FED_ICON_REFRESH = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"/><path d=\"M3 3v5h5\"/><path d=\"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16\"/><path d=\"M21 21v-5h-5\"/></svg>';\nvar FED_ICON_CHECK = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>';\nvar FED_ICON_UNLINK = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 7h3a5 5 0 0 1 0 10h-3m-6 0H6a5 5 0 0 1 0-10h3\"/><line x1=\"2\" y1=\"2\" x2=\"22\" y2=\"22\"/></svg>';\nvar FED_ICON_DISABLE = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"4.93\" y1=\"4.93\" x2=\"19.07\" y2=\"19.07\"/></svg>';\nvar FED_ICON_ENABLE = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18.36 6.64a9 9 0 1 1-12.73 0\"/><line x1=\"12\" y1=\"2\" x2=\"12\" y2=\"12\"/></svg>';\n\nfunction fedIconBtn(icon, title, onclick, variant) {\n var cls = variant === \"danger\" ? \"ap-icon-btn ap-icon-btn-danger\" : variant === \"success\" ? \"ap-icon-btn ap-icon-btn-success\" : \"ap-icon-btn\";\n return '<button class=\"' + cls + '\" title=\"' + fedEscAttr(title) + '\" onclick=\"' + onclick + '\">' + icon + '</button>';\n}\n\nfunction fedNormalizeUrl(input) {\n var s = (input || \"\").trim();\n if (!s) return \"\";\n if (!/^https?:\\/\\//i.test(s)) s = \"https://\" + s;\n try { return new URL(s).origin; } catch(e) { return s; }\n}\n\nfunction fedFormatUrl(el) {\n clearTimeout(el._fedTimer);\n el._fedTimer = setTimeout(function() {\n var v = el.value.trim();\n if (!v) return;\n var norm = fedNormalizeUrl(v);\n if (norm && norm !== v && norm !== \"https://\") {\n el.value = norm;\n }\n }, 800);\n}\n\nfunction fedLoad() {\n api(\"GET\", \"/admin/federation\").then(function(data) {\n if (!data || data.error) {\n __fedData = { role: null, enabled: false, sites: [], delegation: { exists: false } };\n } else {\n __fedData = data;\n }\n fedRender();\n });\n}\n\nfunction fedRender() {\n var el = document.getElementById(\"fed-section\");\n if (!el || !__fedData) return;\n if (!__fedData.role) {\n el.innerHTML = fedRenderUnconfigured();\n } else if (__fedData.role === \"master\") {\n el.innerHTML = fedRenderMaster();\n } else {\n el.innerHTML = fedRenderMember();\n }\n}\n\nfunction fedRenderUnconfigured() {\n return '<p class=\"settings-card-desc\">' + __t(\"federation.desc\") + '</p>'\n + '<div class=\"fed-init-buttons\">'\n + '<button class=\"btn\" id=\"fed-enable-btn\" onclick=\"fedEnable()\"' + (__fedLoading ? ' disabled' : '') + '>'\n + (__fedLoading ? '<span class=\"spinner\"></span> ' : '') + __t(\"federation.enableMaster\") + '</button>'\n + '<button class=\"btn btn-secondary\" onclick=\"fedOpenJoin()\"' + (__fedLoading ? ' disabled' : '') + '>'\n + __t(\"federation.joinMember\") + '</button>'\n + '</div>';\n}\n\nfunction fedRenderMaster() {\n var html = '<div style=\"padding:12px 20px;display:flex;align-items:center;gap:8px\">'\n + '<span class=\"fed-status-badge master\">&#9733; ' + __t(\"federation.masterSite\") + '</span>'\n + '<span style=\"font-size:13px;color:var(--text-secondary)\">' + __t(\"federation.masterDesc\") + '</span>'\n + '</div>';\n\n // Sites header with refresh button\n html += '<div style=\"padding:0 20px 8px;display:flex;justify-content:space-between;align-items:center\">'\n + '<span style=\"font-size:13px;font-weight:600;color:var(--text)\">' + __t(\"federation.sites\") + '</span>'\n + '<button class=\"ap-icon-btn\" title=\"' + __t(\"federation.refresh\") + '\" onclick=\"fedRefresh()\" id=\"fed-refresh-btn\">' + FED_ICON_REFRESH + '</button>'\n + '</div>';\n\n // Sort: master first, then approved, then pending\n var sites = (__fedData.sites || []).slice().sort(function(a, b) {\n if (a.isMaster) return -1;\n if (b.isMaster) return 1;\n if (a.status === \"pending\" && b.status !== \"pending\") return 1;\n if (b.status === \"pending\" && a.status !== \"pending\") return -1;\n return 0;\n });\n var currentOrigin = location.origin;\n for (var i = 0; i < sites.length; i++) {\n var s = sites[i];\n var star = s.isMaster ? '<span class=\"fed-star\">&#9733;</span> ' : '';\n var isMe = s.url && fedNormalizeUrl(s.url) === currentOrigin;\n var meTag = isMe ? ' <span style=\"font-size:10px;padding:1px 5px;border-radius:999px;background:var(--blue-light,#dbeafe);color:var(--blue-muted,#1d4ed8);font-weight:500\">' + __t(\"federation.currentSite\") + '</span>' : '';\n var isPending = s.status === \"pending\";\n var statusBadge = isPending\n ? ' <span style=\"font-size:11px;padding:1px 6px;border-radius:999px;background:var(--yellow-light,#fef3c7);color:var(--yellow-text,#92400e);font-weight:500\">' + __t(\"federation.pending\") + '</span>'\n : '';\n\n var actions = '';\n if (s.isMaster) {\n // Disabled placeholder buttons for alignment with member rows\n actions = '<button class=\"ap-icon-btn\" disabled style=\"opacity:0.25;cursor:default\">' + FED_ICON_DISABLE + '</button>'\n + '<button class=\"ap-icon-btn\" disabled style=\"opacity:0.25;cursor:default\">' + FED_ICON_UNLINK + '</button>';\n } else if (isPending) {\n actions = fedIconBtn(FED_ICON_CHECK, __t(\"federation.approve\"), \"fedApproveSite(&#39;\" + s.did + \"&#39;)\", \"success\")\n + fedIconBtn(FED_ICON_REMOVE, __t(\"federation.reject\"), \"fedRejectSite(&#39;\" + s.did + \"&#39;,&#39;\" + fedEscAttr(s.name) + \"&#39;)\", \"danger\");\n } else {\n // Approved member: disable/enable toggle + unbind\n if (s.enabled === false) {\n actions = fedIconBtn(FED_ICON_ENABLE, __t(\"federation.enable\"), \"fedToggleMember(&#39;\" + s.did + \"&#39;,true)\")\n + fedIconBtn(FED_ICON_UNLINK, __t(\"federation.unbind\"), \"fedUnbindSite(&#39;\" + s.did + \"&#39;,&#39;\" + fedEscAttr(s.name) + \"&#39;)\", \"danger\");\n } else {\n actions = fedIconBtn(FED_ICON_DISABLE, __t(\"federation.disable\"), \"fedToggleMember(&#39;\" + s.did + \"&#39;,false)\", \"danger\")\n + fedIconBtn(FED_ICON_UNLINK, __t(\"federation.unbind\"), \"fedUnbindSite(&#39;\" + s.did + \"&#39;,&#39;\" + fedEscAttr(s.name) + \"&#39;)\", \"danger\");\n }\n }\n // Disabled badge for approved but disabled members\n if (!isPending && !s.isMaster && s.enabled === false) {\n statusBadge = ' <span style=\"font-size:11px;padding:1px 6px;border-radius:999px;background:var(--bg-tertiary,#e5e7eb);color:var(--text-tertiary)\">' + __t(\"federation.disabled\") + '</span>';\n }\n\n html += '<div class=\"fed-site-row\">'\n + '<div class=\"fed-site-name\">' + star + '<span class=\"fed-site-label\">' + fedEscHtml(s.name) + '</span>' + meTag + statusBadge + '</div>'\n + '<div class=\"fed-site-url\" title=\"' + fedEscAttr(s.url) + '\"><a href=\"' + fedEscAttr(s.url) + '\" target=\"_blank\">' + fedEscHtml(s.url) + '</a></div>'\n + '<div class=\"fed-site-actions\">' + actions + '</div>'\n + '</div>';\n }\n\n // Disband\n html += '<div style=\"padding:16px 20px;border-top:1px solid var(--border)\">'\n + '<button class=\"btn btn-danger btn-sm\" style=\"width:auto\" onclick=\"fedDisband()\">' + __t(\"federation.disband\") + '</button>'\n + '</div>';\n\n return html;\n}\n\nfunction fedRenderMember() {\n var enabled = __fedData.enabled;\n var isPending = !enabled && __fedData.role === \"member\";\n\n // Header: badge + status + toggle (no toggle for pending)\n var badgeClass = isPending ? 'style=\"background:var(--yellow-light,#fef3c7);color:var(--yellow-text,#92400e)\"' : '';\n var badgeText = isPending ? __t(\"federation.pending\") : __t(\"federation.member\");\n var statusText = isPending ? __t(\"federation.pendingDesc\") : (enabled ? __t(\"federation.enabled\") : __t(\"federation.disabled\"));\n\n var html = '<div style=\"padding:12px 20px;display:flex;align-items:center;gap:12px;justify-content:space-between\">'\n + '<div style=\"display:flex;align-items:center;gap:8px\">'\n + '<span class=\"fed-status-badge member\" ' + badgeClass + '>' + badgeText + '</span>'\n + '<span style=\"font-size:13px;color:var(--text-secondary)\">' + statusText + '</span>'\n + '</div>';\n if (!isPending) {\n html += '<label class=\"toggle-switch\">'\n + '<input type=\"checkbox\" ' + (enabled ? 'checked' : '') + ' onchange=\"fedToggle(this.checked)\" />'\n + '<span class=\"toggle-slider\"></span>'\n + '</label>';\n }\n html += '</div>';\n\n // Master URL\n if (__fedData.masterUrl) {\n html += '<div style=\"padding:0 20px 8px;font-size:13px;color:var(--text-secondary)\">'\n + __t(\"federation.delegatedTo\") + ' <a href=\"' + fedEscAttr(__fedData.masterUrl) + '\" target=\"_blank\" style=\"color:var(--blue)\">' + fedEscHtml(__fedData.masterUrl) + '</a>'\n + '</div>';\n }\n\n // Site list \u2014 show all sites with role labels; pending sites get \"pending\" instead of \"member\"\n if (__fedData.sites && __fedData.sites.length > 0) {\n // Sort: master first, then approved, then pending\n var memberSites = __fedData.sites.slice().sort(function(a, b) {\n if (a.isMaster) return -1;\n if (b.isMaster) return 1;\n if (a.status === \"pending\" && b.status !== \"pending\") return 1;\n if (b.status === \"pending\" && a.status !== \"pending\") return -1;\n return 0;\n });\n html += '<div style=\"padding:8px 20px 4px;display:flex;justify-content:space-between;align-items:center\">'\n + '<span style=\"font-size:13px;font-weight:600;color:var(--text)\">' + __t(\"federation.sites\") + '</span>'\n + '<button class=\"ap-icon-btn\" title=\"' + __t(\"federation.refresh\") + '\" onclick=\"fedRefresh()\" id=\"fed-refresh-btn\">' + FED_ICON_REFRESH + '</button>'\n + '</div>';\n var currentOrigin = location.origin;\n for (var i = 0; i < memberSites.length; i++) {\n var s = memberSites[i];\n var star = s.isMaster ? '<span class=\"fed-star\">&#9733;</span> ' : '';\n var isMe = s.url && fedNormalizeUrl(s.url) === currentOrigin;\n var meTag = isMe ? ' <span style=\"font-size:10px;padding:1px 5px;border-radius:999px;background:var(--blue-light,#dbeafe);color:var(--blue-muted,#1d4ed8);font-weight:500\">' + __t(\"federation.currentSite\") + '</span>' : '';\n var isPendingSite = s.status === \"pending\";\n var role = s.isMaster ? __t(\"federation.master\")\n : (isPendingSite\n ? '<span style=\"color:var(--yellow-text,#92400e);font-weight:500\">' + __t(\"federation.pending\") + '</span>'\n : __t(\"federation.member\"));\n html += '<div class=\"fed-site-row\">'\n + '<div class=\"fed-site-name\">' + star + '<span class=\"fed-site-label\">' + fedEscHtml(s.name) + '</span>' + meTag + '</div>'\n + '<div class=\"fed-site-url\" title=\"' + fedEscAttr(s.url) + '\"><a href=\"' + fedEscAttr(s.url) + '\" target=\"_blank\">' + fedEscHtml(s.url) + '</a></div>'\n + '<div class=\"fed-site-role\">' + role + '</div>'\n + '</div>';\n }\n }\n\n html += '<div style=\"padding:16px 20px;border-top:1px solid var(--border)\">'\n + '<button class=\"btn btn-danger btn-sm\" style=\"width:auto\" onclick=\"fedLeave()\">' + __t(\"federation.leave\") + '</button>'\n + '</div>';\n\n return html;\n}\n\nfunction fedEscHtml(str) {\n if (!str) return '';\n return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n}\nfunction fedEscAttr(str) {\n if (!str) return '';\n return str.replace(/&/g,'&amp;').replace(/\"/g,'&quot;').replace(/'/g,'&#39;');\n}\n\nfunction fedSetLoading(loading) {\n __fedLoading = loading;\n fedRender();\n}\n\nfunction fedEnable() {\n confirmDialog({\n title: __t(\"federation.enableConfirm\"),\n message: __t(\"federation.enableMasterDesc\"),\n confirmLabel: __t(\"federation.enableMaster\"),\n }).then(function(ok) {\n if (!ok) return;\n fedSetLoading(true);\n api(\"POST\", \"/admin/federation/enable\", { url: location.origin, name: document.title || \"App\" }).then(function(data) {\n __fedLoading = false;\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.enabled\"), \"success\"); }\n else { showToast(data.error || \"Failed\", \"error\"); fedRender(); }\n });\n });\n}\n\nfunction fedOpenJoin() {\n document.getElementById(\"fed-join-url\").value = \"\";\n showDialog(\"fed-join-dialog\");\n}\n\nfunction fedJoinSubmit() {\n var masterUrl = fedNormalizeUrl(document.getElementById(\"fed-join-url\").value);\n if (!masterUrl) { showToast(__t(\"federation.masterUrl\") + \" is required\", \"error\"); return; }\n\n var btn = document.getElementById(\"fed-join-btn\");\n btn.disabled = true;\n btn.innerHTML = '<span class=\"spinner\"></span>';\n\n api(\"POST\", \"/admin/federation/join\", { masterUrl: masterUrl }).then(function(data) {\n btn.disabled = false;\n btn.textContent = __t(\"federation.joinMember\");\n if (data && !data.error) {\n __fedData = data;\n fedRender();\n closeDialog(\"fed-join-dialog\");\n showToast(__t(\"federation.pendingApproval\"), \"success\");\n } else {\n showToast(data.error || __t(\"federation.targetNotEnabled\"), \"error\");\n }\n });\n}\n\nfunction fedToggle(enabled) {\n if (!enabled) {\n confirmDialog({\n title: __t(\"federation.disableTitle\"),\n message: __t(\"federation.disableDesc\") + \" \" + __t(\"federation.walletSafe\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"common.confirm\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) { fedRender(); return; }\n api(\"PUT\", \"/admin/federation/toggle\", { enabled: false }).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); }\n });\n });\n var cb = document.querySelector(\"#fed-section input[type=checkbox]\");\n if (cb) cb.checked = true;\n } else {\n api(\"PUT\", \"/admin/federation/toggle\", { enabled: true }).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); }\n });\n }\n}\n\nfunction fedApproveSite(did) {\n api(\"POST\", \"/admin/federation/sites/\" + encodeURIComponent(did) + \"/approve\").then(function(data) {\n if (data && !data.error) {\n __fedData = data;\n fedRender();\n showToast(__t(\"federation.approved\"), \"success\");\n } else {\n showToast(data.error || \"Failed\", \"error\");\n }\n });\n}\n\nfunction fedRejectSite(did, name) {\n confirmDialog({\n title: __t(\"federation.rejectTitle\"),\n message: __t(\"federation.rejectDesc\"),\n confirmLabel: __t(\"federation.reject\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n api(\"DELETE\", \"/admin/federation/sites/\" + encodeURIComponent(did)).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.reject\") + \" \\u2713\", \"success\"); }\n });\n });\n}\n\nfunction fedRefresh() {\n var btn = document.getElementById(\"fed-refresh-btn\");\n if (btn) { btn.style.opacity = \"0.5\"; btn.style.pointerEvents = \"none\"; }\n fedLoad();\n setTimeout(function() { if (btn) { btn.style.opacity = \"\"; btn.style.pointerEvents = \"\"; } }, 500);\n}\n\n// \u2500\u2500\u2500 Actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction fedToggleMember(did, enabled) {\n if (!enabled) {\n confirmDialog({\n title: __t(\"federation.disableTitle\"),\n message: __t(\"federation.disableDesc\") + \" \" + __t(\"federation.walletSafe\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"federation.disable\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n api(\"PUT\", \"/admin/federation/sites/\" + encodeURIComponent(did) + \"/toggle\", { enabled: false }).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.disabled\"), \"success\"); }\n });\n });\n } else {\n api(\"PUT\", \"/admin/federation/sites/\" + encodeURIComponent(did) + \"/toggle\", { enabled: true }).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.enabled\"), \"success\"); }\n });\n }\n}\n\nfunction fedUnbindSite(did, name) {\n confirmDialog({\n title: __t(\"federation.unbindTitle\"),\n message: __t(\"federation.unbindDesc\") + \" \" + __t(\"federation.walletSafe\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"federation.unbind\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n api(\"DELETE\", \"/admin/federation/sites/\" + encodeURIComponent(did)).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.unbind\") + \" \\u2713\", \"success\"); }\n });\n });\n}\n\nfunction fedDisband() {\n confirmDialog({\n title: __t(\"federation.disbandTitle\"),\n message: __t(\"federation.disbandDesc\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"federation.disband\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n api(\"POST\", \"/admin/federation/disband\").then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.disband\") + \" \\u2713\", \"success\"); }\n });\n });\n}\n\nfunction fedLeave() {\n confirmDialog({\n title: __t(\"federation.leaveTitle\"),\n message: __t(\"federation.leaveDesc\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"federation.leave\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n api(\"POST\", \"/admin/federation/leave\").then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.leave\") + \" \\u2713\", \"success\"); }\n });\n });\n}\n";
6
+ export declare const FEDERATION_SECTION_SCRIPT = "\n// \u2500\u2500\u2500 Federation (Unified Login) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nvar __fedData = null;\nvar __fedLoading = false;\n\nvar FED_ICON_REMOVE = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 6h18\"/><path d=\"M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6\"/><path d=\"M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2\"/></svg>';\nvar FED_ICON_REFRESH = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"/><path d=\"M3 3v5h5\"/><path d=\"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16\"/><path d=\"M21 21v-5h-5\"/></svg>';\nvar FED_ICON_CHECK = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg>';\nvar FED_ICON_UNLINK = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 7h3a5 5 0 0 1 0 10h-3m-6 0H6a5 5 0 0 1 0-10h3\"/><line x1=\"2\" y1=\"2\" x2=\"22\" y2=\"22\"/></svg>';\nvar FED_ICON_DISABLE = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><line x1=\"4.93\" y1=\"4.93\" x2=\"19.07\" y2=\"19.07\"/></svg>';\nvar FED_ICON_ENABLE = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18.36 6.64a9 9 0 1 1-12.73 0\"/><line x1=\"12\" y1=\"2\" x2=\"12\" y2=\"12\"/></svg>';\n\nfunction fedIconBtn(icon, title, onclick, variant) {\n var cls = variant === \"danger\" ? \"ap-icon-btn ap-icon-btn-danger\" : variant === \"success\" ? \"ap-icon-btn ap-icon-btn-success\" : \"ap-icon-btn\";\n return '<button class=\"' + cls + '\" title=\"' + fedEscAttr(title) + '\" onclick=\"' + onclick + '\">' + icon + '</button>';\n}\n\nfunction fedNormalizeUrl(input) {\n var s = (input || \"\").trim();\n if (!s) return \"\";\n if (!/^https?:\\/\\//i.test(s)) s = \"https://\" + s;\n try { return new URL(s).origin; } catch(e) { return s; }\n}\n\nfunction fedFormatUrl(el) {\n clearTimeout(el._fedTimer);\n el._fedTimer = setTimeout(function() {\n var v = el.value.trim();\n if (!v) return;\n var norm = fedNormalizeUrl(v);\n if (norm && norm !== v && norm !== \"https://\") {\n el.value = norm;\n }\n }, 800);\n}\n\nfunction fedLoad() {\n api(\"GET\", \"/admin/federation\").then(function(data) {\n if (!data || data.error) {\n __fedData = { role: null, enabled: false, sites: [], delegation: { exists: false } };\n } else {\n __fedData = data;\n }\n fedRender();\n });\n}\n\nfunction fedRender() {\n var el = document.getElementById(\"fed-section\");\n if (!el || !__fedData) return;\n if (!__fedData.role) {\n el.innerHTML = fedRenderUnconfigured();\n } else if (__fedData.role === \"master\") {\n el.innerHTML = fedRenderMaster();\n } else {\n el.innerHTML = fedRenderMember();\n }\n}\n\nfunction fedRenderUnconfigured() {\n return '<p class=\"settings-card-desc\">' + __t(\"federation.desc\") + '</p>'\n + '<div class=\"fed-init-buttons\">'\n + '<button class=\"btn\" id=\"fed-enable-btn\" onclick=\"fedEnable()\"' + (__fedLoading ? ' disabled' : '') + '>'\n + (__fedLoading ? '<span class=\"spinner\"></span> ' : '') + __t(\"federation.enableMaster\") + '</button>'\n + '<button class=\"btn btn-secondary\" onclick=\"fedOpenJoin()\"' + (__fedLoading ? ' disabled' : '') + '>'\n + __t(\"federation.joinMember\") + '</button>'\n + '</div>';\n}\n\nfunction fedRenderMaster() {\n var html = '<div style=\"padding:12px 20px;display:flex;align-items:center;gap:8px\">'\n + '<span class=\"fed-status-badge master\">&#9733; ' + __t(\"federation.masterSite\") + '</span>'\n + '<span style=\"font-size:13px;color:var(--text-secondary)\">' + __t(\"federation.masterDesc\") + '</span>'\n + '</div>';\n\n // Sites header with refresh button\n html += '<div style=\"padding:0 20px 8px;display:flex;justify-content:space-between;align-items:center\">'\n + '<span style=\"font-size:13px;font-weight:600;color:var(--text)\">' + __t(\"federation.sites\") + '</span>'\n + '<button class=\"ap-icon-btn\" title=\"' + __t(\"federation.refresh\") + '\" onclick=\"fedRefresh()\" id=\"fed-refresh-btn\">' + FED_ICON_REFRESH + '</button>'\n + '</div>';\n\n // Sort: master first, then approved, then pending\n var sites = (__fedData.sites || []).slice().sort(function(a, b) {\n if (a.isMaster) return -1;\n if (b.isMaster) return 1;\n if (a.status === \"pending\" && b.status !== \"pending\") return 1;\n if (b.status === \"pending\" && a.status !== \"pending\") return -1;\n return 0;\n });\n var currentOrigin = location.origin;\n for (var i = 0; i < sites.length; i++) {\n var s = sites[i];\n var star = s.isMaster ? '<span class=\"fed-star\">&#9733;</span> ' : '';\n var isMe = s.url && fedNormalizeUrl(s.url) === currentOrigin;\n var meTag = isMe ? ' <span style=\"font-size:10px;padding:1px 5px;border-radius:999px;background:var(--blue-light,#dbeafe);color:var(--blue-muted,#1d4ed8);font-weight:500\">' + __t(\"federation.currentSite\") + '</span>' : '';\n var isPending = s.status === \"pending\";\n var statusBadge = isPending\n ? ' <span style=\"font-size:11px;padding:1px 6px;border-radius:999px;background:var(--yellow-light,#fef3c7);color:var(--yellow-text,#92400e);font-weight:500\">' + __t(\"federation.pending\") + '</span>'\n : '';\n\n var actions = '';\n if (s.isMaster) {\n // Disabled placeholder buttons for alignment with member rows\n actions = '<button class=\"ap-icon-btn\" disabled style=\"opacity:0.25;cursor:default\">' + FED_ICON_DISABLE + '</button>'\n + '<button class=\"ap-icon-btn\" disabled style=\"opacity:0.25;cursor:default\">' + FED_ICON_UNLINK + '</button>';\n } else if (isPending) {\n actions = fedIconBtn(FED_ICON_CHECK, __t(\"federation.approve\"), \"fedApproveSite(&#39;\" + s.did + \"&#39;)\", \"success\")\n + fedIconBtn(FED_ICON_REMOVE, __t(\"federation.reject\"), \"fedRejectSite(&#39;\" + s.did + \"&#39;,&#39;\" + fedEscAttr(s.name) + \"&#39;)\", \"danger\");\n } else {\n // Approved member: disable/enable toggle + unbind\n if (s.enabled === false) {\n actions = fedIconBtn(FED_ICON_ENABLE, __t(\"federation.enable\"), \"fedToggleMember(&#39;\" + s.did + \"&#39;,true)\")\n + fedIconBtn(FED_ICON_UNLINK, __t(\"federation.unbind\"), \"fedUnbindSite(&#39;\" + s.did + \"&#39;,&#39;\" + fedEscAttr(s.name) + \"&#39;)\", \"danger\");\n } else {\n actions = fedIconBtn(FED_ICON_DISABLE, __t(\"federation.disable\"), \"fedToggleMember(&#39;\" + s.did + \"&#39;,false)\", \"danger\")\n + fedIconBtn(FED_ICON_UNLINK, __t(\"federation.unbind\"), \"fedUnbindSite(&#39;\" + s.did + \"&#39;,&#39;\" + fedEscAttr(s.name) + \"&#39;)\", \"danger\");\n }\n }\n // Disabled badge for approved but disabled members\n if (!isPending && !s.isMaster && s.enabled === false) {\n statusBadge = ' <span style=\"font-size:11px;padding:1px 6px;border-radius:999px;background:var(--bg-tertiary,#e5e7eb);color:var(--text-tertiary)\">' + __t(\"federation.disabled\") + '</span>';\n }\n\n html += '<div class=\"fed-site-row\">'\n + '<div class=\"fed-site-name\">' + star + '<span class=\"fed-site-label\">' + fedEscHtml(s.name) + '</span>' + meTag + statusBadge + '</div>'\n + '<div class=\"fed-site-url\" title=\"' + fedEscAttr(s.url) + '\"><a href=\"' + fedEscAttr(s.url) + '\" target=\"_blank\">' + fedEscHtml(s.url) + '</a></div>'\n + '<div class=\"fed-site-actions\">' + actions + '</div>'\n + '</div>';\n }\n\n // Disband\n html += '<div style=\"padding:16px 20px;border-top:1px solid var(--border)\">'\n + '<button class=\"btn btn-danger btn-sm\" style=\"width:auto\" onclick=\"fedDisband()\">' + __t(\"federation.disband\") + '</button>'\n + '</div>';\n\n return html;\n}\n\nfunction fedRenderMember() {\n var enabled = __fedData.enabled;\n var isPending = !enabled && __fedData.role === \"member\";\n\n // Header: badge + status + toggle (no toggle for pending)\n var badgeClass = isPending ? 'style=\"background:var(--yellow-light,#fef3c7);color:var(--yellow-text,#92400e)\"' : '';\n var badgeText = isPending ? __t(\"federation.pending\") : __t(\"federation.member\");\n var statusText = isPending ? __t(\"federation.pendingDesc\") : (enabled ? __t(\"federation.enabled\") : __t(\"federation.disabled\"));\n\n var html = '<div style=\"padding:12px 20px;display:flex;align-items:center;gap:12px;justify-content:space-between\">'\n + '<div style=\"display:flex;align-items:center;gap:8px\">'\n + '<span class=\"fed-status-badge member\" ' + badgeClass + '>' + badgeText + '</span>'\n + '<span style=\"font-size:13px;color:var(--text-secondary)\">' + statusText + '</span>'\n + '</div>';\n if (!isPending) {\n html += '<label class=\"toggle-switch\">'\n + '<input type=\"checkbox\" ' + (enabled ? 'checked' : '') + ' onchange=\"fedToggle(this.checked)\" />'\n + '<span class=\"toggle-slider\"></span>'\n + '</label>';\n }\n html += '</div>';\n\n // Master URL\n if (__fedData.masterUrl) {\n html += '<div style=\"padding:0 20px 8px;font-size:13px;color:var(--text-secondary)\">'\n + __t(\"federation.delegatedTo\") + ' <a href=\"' + fedEscAttr(__fedData.masterUrl) + '\" target=\"_blank\" style=\"color:var(--blue)\">' + fedEscHtml(__fedData.masterUrl) + '</a>'\n + '</div>';\n }\n\n // Site list \u2014 show all sites with role labels; pending sites get \"pending\" instead of \"member\"\n if (__fedData.sites && __fedData.sites.length > 0) {\n // Sort: master first, then approved, then pending\n var memberSites = __fedData.sites.slice().sort(function(a, b) {\n if (a.isMaster) return -1;\n if (b.isMaster) return 1;\n if (a.status === \"pending\" && b.status !== \"pending\") return 1;\n if (b.status === \"pending\" && a.status !== \"pending\") return -1;\n return 0;\n });\n html += '<div style=\"padding:8px 20px 4px;display:flex;justify-content:space-between;align-items:center\">'\n + '<span style=\"font-size:13px;font-weight:600;color:var(--text)\">' + __t(\"federation.sites\") + '</span>'\n + '<button class=\"ap-icon-btn\" title=\"' + __t(\"federation.refresh\") + '\" onclick=\"fedRefresh()\" id=\"fed-refresh-btn\">' + FED_ICON_REFRESH + '</button>'\n + '</div>';\n var currentOrigin = location.origin;\n for (var i = 0; i < memberSites.length; i++) {\n var s = memberSites[i];\n var star = s.isMaster ? '<span class=\"fed-star\">&#9733;</span> ' : '';\n var isMe = s.url && fedNormalizeUrl(s.url) === currentOrigin;\n var meTag = isMe ? ' <span style=\"font-size:10px;padding:1px 5px;border-radius:999px;background:var(--blue-light,#dbeafe);color:var(--blue-muted,#1d4ed8);font-weight:500\">' + __t(\"federation.currentSite\") + '</span>' : '';\n var isPendingSite = s.status === \"pending\";\n var role = s.isMaster ? __t(\"federation.master\")\n : (isPendingSite\n ? '<span style=\"color:var(--yellow-text,#92400e);font-weight:500\">' + __t(\"federation.pending\") + '</span>'\n : __t(\"federation.member\"));\n html += '<div class=\"fed-site-row\">'\n + '<div class=\"fed-site-name\">' + star + '<span class=\"fed-site-label\">' + fedEscHtml(s.name) + '</span>' + meTag + '</div>'\n + '<div class=\"fed-site-url\" title=\"' + fedEscAttr(s.url) + '\"><a href=\"' + fedEscAttr(s.url) + '\" target=\"_blank\">' + fedEscHtml(s.url) + '</a></div>'\n + '<div class=\"fed-site-role\">' + role + '</div>'\n + '</div>';\n }\n }\n\n html += '<div style=\"padding:16px 20px;border-top:1px solid var(--border)\">'\n + '<button class=\"btn btn-danger btn-sm\" style=\"width:auto\" onclick=\"fedLeave()\">' + __t(\"federation.leave\") + '</button>'\n + '</div>';\n\n return html;\n}\n\nfunction fedEscHtml(str) {\n if (!str) return '';\n return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n}\nfunction fedEscAttr(str) {\n if (!str) return '';\n return str.replace(/&/g,'&amp;').replace(/\"/g,'&quot;').replace(/'/g,'&#39;');\n}\n\nfunction fedSetLoading(loading) {\n __fedLoading = loading;\n fedRender();\n}\n\nfunction fedEnable() {\n return confirmDialog({\n title: __t(\"federation.enableConfirm\"),\n message: __t(\"federation.enableMasterDesc\"),\n confirmLabel: __t(\"federation.enableMaster\"),\n }).then(function(ok) {\n if (!ok) return;\n fedSetLoading(true);\n return api(\"POST\", \"/admin/federation/enable\", { url: location.origin, name: document.title || \"App\" }).then(function(data) {\n __fedLoading = false;\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.enabled\"), \"success\"); }\n else { showToast(data.error || \"Failed\", \"error\"); fedRender(); }\n });\n });\n}\n\nfunction fedOpenJoin() {\n document.getElementById(\"fed-join-url\").value = \"\";\n showDialog(\"fed-join-dialog\");\n}\n\nfunction fedJoinSubmit() {\n var masterUrl = fedNormalizeUrl(document.getElementById(\"fed-join-url\").value);\n if (!masterUrl) { showToast(__t(\"federation.masterUrl\") + \" is required\", \"error\"); return; }\n\n var btn = document.getElementById(\"fed-join-btn\");\n btn.disabled = true;\n btn.innerHTML = '<span class=\"spinner\"></span>';\n\n api(\"POST\", \"/admin/federation/join\", { masterUrl: masterUrl }).then(function(data) {\n btn.disabled = false;\n btn.textContent = __t(\"federation.joinMember\");\n if (data && !data.error) {\n __fedData = data;\n fedRender();\n closeDialog(\"fed-join-dialog\");\n showToast(__t(\"federation.pendingApproval\"), \"success\");\n } else {\n showToast(data.error || __t(\"federation.targetNotEnabled\"), \"error\");\n }\n });\n}\n\nfunction fedToggle(enabled) {\n if (!enabled) {\n confirmDialog({\n title: __t(\"federation.disableTitle\"),\n message: __t(\"federation.disableDesc\") + \" \" + __t(\"federation.walletSafe\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"common.confirm\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) { fedRender(); return; }\n api(\"PUT\", \"/admin/federation/toggle\", { enabled: false }).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); }\n });\n });\n var cb = document.querySelector(\"#fed-section input[type=checkbox]\");\n if (cb) cb.checked = true;\n } else {\n api(\"PUT\", \"/admin/federation/toggle\", { enabled: true }).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); }\n });\n }\n}\n\nfunction fedApproveSite(did) {\n return api(\"POST\", \"/admin/federation/sites/\" + encodeURIComponent(did) + \"/approve\").then(function(data) {\n if (data && !data.error) {\n __fedData = data;\n fedRender();\n showToast(__t(\"federation.approved\"), \"success\");\n } else {\n showToast(data.error || \"Failed\", \"error\");\n }\n });\n}\n\nfunction fedRejectSite(did, name) {\n return confirmDialog({\n title: __t(\"federation.rejectTitle\"),\n message: __t(\"federation.rejectDesc\"),\n confirmLabel: __t(\"federation.reject\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n return api(\"DELETE\", \"/admin/federation/sites/\" + encodeURIComponent(did)).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.reject\") + \" \\u2713\", \"success\"); }\n });\n });\n}\n\nfunction fedRefresh() {\n var btn = document.getElementById(\"fed-refresh-btn\");\n if (btn) { btn.style.opacity = \"0.5\"; btn.style.pointerEvents = \"none\"; }\n fedLoad();\n setTimeout(function() { if (btn) { btn.style.opacity = \"\"; btn.style.pointerEvents = \"\"; } }, 500);\n}\n\n// \u2500\u2500\u2500 Actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction fedToggleMember(did, enabled) {\n if (!enabled) {\n confirmDialog({\n title: __t(\"federation.disableTitle\"),\n message: __t(\"federation.disableDesc\") + \" \" + __t(\"federation.walletSafe\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"federation.disable\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n api(\"PUT\", \"/admin/federation/sites/\" + encodeURIComponent(did) + \"/toggle\", { enabled: false }).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.disabled\"), \"success\"); }\n });\n });\n } else {\n api(\"PUT\", \"/admin/federation/sites/\" + encodeURIComponent(did) + \"/toggle\", { enabled: true }).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.enabled\"), \"success\"); }\n });\n }\n}\n\nfunction fedUnbindSite(did, name) {\n return confirmDialog({\n title: __t(\"federation.unbindTitle\"),\n message: __t(\"federation.unbindDesc\") + \" \" + __t(\"federation.walletSafe\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"federation.unbind\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n return api(\"DELETE\", \"/admin/federation/sites/\" + encodeURIComponent(did)).then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.unbind\") + \" \\u2713\", \"success\"); }\n });\n });\n}\n\nfunction fedDisband() {\n return confirmDialog({\n title: __t(\"federation.disbandTitle\"),\n message: __t(\"federation.disbandDesc\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"federation.disband\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n return api(\"POST\", \"/admin/federation/disband\").then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.disband\") + \" \\u2713\", \"success\"); }\n });\n });\n}\n\nfunction fedLeave() {\n return confirmDialog({\n title: __t(\"federation.leaveTitle\"),\n message: __t(\"federation.leaveDesc\") + \" \" + __t(\"federation.sessionWarning\"),\n confirmLabel: __t(\"federation.leave\"),\n danger: true,\n }).then(function(ok) {\n if (!ok) return;\n return api(\"POST\", \"/admin/federation/leave\").then(function(data) {\n if (data && !data.error) { __fedData = data; fedRender(); showToast(__t(\"federation.leave\") + \" \\u2713\", \"success\"); }\n });\n });\n}\n\n// Button-driven mutations must not double-submit (submitOnce \u2014 utils.ts).\n// fedJoinSubmit locks its own button; fedToggle / fedToggleMember are\n// checkbox toggles and stay unwrapped (dropping a toggle would desync the UI).\nfedEnable = submitOnce(fedEnable);\nfedApproveSite = submitOnce(fedApproveSite);\nfedRejectSite = submitOnce(fedRejectSite);\nfedUnbindSite = submitOnce(fedUnbindSite);\nfedDisband = submitOnce(fedDisband);\nfedLeave = submitOnce(fedLeave);\n";
7
7
  export {};
8
8
  //# sourceMappingURL=tab-federation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tab-federation.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-federation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC;AAEpE,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CA2BpD;AAED,eAAO,MAAM,yBAAyB,wzmBA+XrC,CAAC"}
1
+ {"version":3,"file":"tab-federation.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-federation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC;AAEpE,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CA2BpD;AAED,eAAO,MAAM,yBAAyB,m2nBAyYrC,CAAC"}
@@ -252,14 +252,14 @@ function fedSetLoading(loading) {
252
252
  }
253
253
 
254
254
  function fedEnable() {
255
- confirmDialog({
255
+ return confirmDialog({
256
256
  title: __t("federation.enableConfirm"),
257
257
  message: __t("federation.enableMasterDesc"),
258
258
  confirmLabel: __t("federation.enableMaster"),
259
259
  }).then(function(ok) {
260
260
  if (!ok) return;
261
261
  fedSetLoading(true);
262
- api("POST", "/admin/federation/enable", { url: location.origin, name: document.title || "App" }).then(function(data) {
262
+ return api("POST", "/admin/federation/enable", { url: location.origin, name: document.title || "App" }).then(function(data) {
263
263
  __fedLoading = false;
264
264
  if (data && !data.error) { __fedData = data; fedRender(); showToast(__t("federation.enabled"), "success"); }
265
265
  else { showToast(data.error || "Failed", "error"); fedRender(); }
@@ -317,7 +317,7 @@ function fedToggle(enabled) {
317
317
  }
318
318
 
319
319
  function fedApproveSite(did) {
320
- api("POST", "/admin/federation/sites/" + encodeURIComponent(did) + "/approve").then(function(data) {
320
+ return api("POST", "/admin/federation/sites/" + encodeURIComponent(did) + "/approve").then(function(data) {
321
321
  if (data && !data.error) {
322
322
  __fedData = data;
323
323
  fedRender();
@@ -329,14 +329,14 @@ function fedApproveSite(did) {
329
329
  }
330
330
 
331
331
  function fedRejectSite(did, name) {
332
- confirmDialog({
332
+ return confirmDialog({
333
333
  title: __t("federation.rejectTitle"),
334
334
  message: __t("federation.rejectDesc"),
335
335
  confirmLabel: __t("federation.reject"),
336
336
  danger: true,
337
337
  }).then(function(ok) {
338
338
  if (!ok) return;
339
- api("DELETE", "/admin/federation/sites/" + encodeURIComponent(did)).then(function(data) {
339
+ return api("DELETE", "/admin/federation/sites/" + encodeURIComponent(did)).then(function(data) {
340
340
  if (data && !data.error) { __fedData = data; fedRender(); showToast(__t("federation.reject") + " \\u2713", "success"); }
341
341
  });
342
342
  });
@@ -372,45 +372,55 @@ function fedToggleMember(did, enabled) {
372
372
  }
373
373
 
374
374
  function fedUnbindSite(did, name) {
375
- confirmDialog({
375
+ return confirmDialog({
376
376
  title: __t("federation.unbindTitle"),
377
377
  message: __t("federation.unbindDesc") + " " + __t("federation.walletSafe") + " " + __t("federation.sessionWarning"),
378
378
  confirmLabel: __t("federation.unbind"),
379
379
  danger: true,
380
380
  }).then(function(ok) {
381
381
  if (!ok) return;
382
- api("DELETE", "/admin/federation/sites/" + encodeURIComponent(did)).then(function(data) {
382
+ return api("DELETE", "/admin/federation/sites/" + encodeURIComponent(did)).then(function(data) {
383
383
  if (data && !data.error) { __fedData = data; fedRender(); showToast(__t("federation.unbind") + " \\u2713", "success"); }
384
384
  });
385
385
  });
386
386
  }
387
387
 
388
388
  function fedDisband() {
389
- confirmDialog({
389
+ return confirmDialog({
390
390
  title: __t("federation.disbandTitle"),
391
391
  message: __t("federation.disbandDesc") + " " + __t("federation.sessionWarning"),
392
392
  confirmLabel: __t("federation.disband"),
393
393
  danger: true,
394
394
  }).then(function(ok) {
395
395
  if (!ok) return;
396
- api("POST", "/admin/federation/disband").then(function(data) {
396
+ return api("POST", "/admin/federation/disband").then(function(data) {
397
397
  if (data && !data.error) { __fedData = data; fedRender(); showToast(__t("federation.disband") + " \\u2713", "success"); }
398
398
  });
399
399
  });
400
400
  }
401
401
 
402
402
  function fedLeave() {
403
- confirmDialog({
403
+ return confirmDialog({
404
404
  title: __t("federation.leaveTitle"),
405
405
  message: __t("federation.leaveDesc") + " " + __t("federation.sessionWarning"),
406
406
  confirmLabel: __t("federation.leave"),
407
407
  danger: true,
408
408
  }).then(function(ok) {
409
409
  if (!ok) return;
410
- api("POST", "/admin/federation/leave").then(function(data) {
410
+ return api("POST", "/admin/federation/leave").then(function(data) {
411
411
  if (data && !data.error) { __fedData = data; fedRender(); showToast(__t("federation.leave") + " \\u2713", "success"); }
412
412
  });
413
413
  });
414
414
  }
415
+
416
+ // Button-driven mutations must not double-submit (submitOnce — utils.ts).
417
+ // fedJoinSubmit locks its own button; fedToggle / fedToggleMember are
418
+ // checkbox toggles and stay unwrapped (dropping a toggle would desync the UI).
419
+ fedEnable = submitOnce(fedEnable);
420
+ fedApproveSite = submitOnce(fedApproveSite);
421
+ fedRejectSite = submitOnce(fedRejectSite);
422
+ fedUnbindSite = submitOnce(fedUnbindSite);
423
+ fedDisband = submitOnce(fedDisband);
424
+ fedLeave = submitOnce(fedLeave);
415
425
  `;
416
426
  //# sourceMappingURL=tab-federation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tab-federation.js","sourceRoot":"","sources":["../../../src/pages/admin/tab-federation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,UAAU,qBAAqB,CAAC,CAAM;IAC1C,OAAO,UAAU,CAAC;;;;cAIN,CAAC,CAAC,kBAAkB,CAAC;;;;;;;YAOvB,CAAC,CAAC,uBAAuB,CAAC;+DACyB,CAAC,CAAC,qBAAqB,CAAC;;oCAEnD,CAAC,CAAC,sBAAsB,CAAC;8DACC,CAAC,CAAC,iCAAiC,CAAC;;;;mFAIf,CAAC,CAAC,eAAe,CAAC;wEAC7B,CAAC,CAAC,uBAAuB,CAAC;;;;;GAK/F,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+XjD,CAAC"}
1
+ {"version":3,"file":"tab-federation.js","sourceRoot":"","sources":["../../../src/pages/admin/tab-federation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,UAAU,qBAAqB,CAAC,CAAM;IAC1C,OAAO,UAAU,CAAC;;;;cAIN,CAAC,CAAC,kBAAkB,CAAC;;;;;;;YAOvB,CAAC,CAAC,uBAAuB,CAAC;+DACyB,CAAC,CAAC,qBAAqB,CAAC;;oCAEnD,CAAC,CAAC,sBAAsB,CAAC;8DACC,CAAC,CAAC,iCAAiC,CAAC;;;;mFAIf,CAAC,CAAC,eAAe,CAAC;wEAC7B,CAAC,CAAC,uBAAuB,CAAC;;;;;GAK/F,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyYjD,CAAC"}
@@ -3,6 +3,6 @@
3
3
  */
4
4
  type TFn = (key: string, params?: Record<string, string>) => string;
5
5
  export declare function invitationsTabHtml(t: TFn): string;
6
- export declare const INVITATIONS_TAB_SCRIPT = "\nvar invitationsPage = 1;\nvar invitationsPageSize = 20;\n\n\nfunction updateRemarkCounter() {\n document.getElementById(\"remark-counter\").textContent = document.getElementById(\"invite-remark\").value.length;\n}\n\nasync function loadInvitations(page) {\n invitationsPage = page || 1;\n var qs = \"?page=\" + invitationsPage + \"&pageSize=\" + invitationsPageSize;\n document.getElementById(\"invitations-table-wrap\").innerHTML = skeletonTable(6, 4);\n document.getElementById(\"invitations-pagination\").innerHTML = \"\";\n var data = await api(\"GET\", \"/invitations\" + qs);\n if (!data.ok) {\n document.getElementById(\"invitations-table-wrap\").innerHTML = '<div class=\"empty-state\"><div class=\"empty-state-icon\"><svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4m0 4h.01\"/></svg></div><div class=\"empty-state-title\">' + __t(\"invitations.loadFailed\") + '</div></div>';\n return;\n }\n renderInvitationsTable(data.invitations, data.paging);\n}\n\nfunction renderInvitationsTable(invitations, paging) {\n var wrap = document.getElementById(\"invitations-table-wrap\");\n if (!invitations || invitations.length === 0) {\n wrap.innerHTML = '<div class=\"empty-state\"><div class=\"empty-state-icon\"><svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M22 12h-4l-3 9L9 3l-3 9H2\"/></svg></div><div class=\"empty-state-title\">' + __t(\"invitations.noInvitations\") + '</div><div class=\"empty-state-desc\">' + __t(\"invitations.noInvitationsHint\") + '</div></div>';\n document.getElementById(\"invitations-pagination\").innerHTML = \"\";\n return;\n }\n\n var html = '<table class=\"table\"><thead><tr><th>' + __t(\"invitations.col.role\") + '</th><th class=\"col-hide-sm\">' + __t(\"invitations.col.remark\") + '</th><th>' + __t(\"invitations.col.uses\") + '</th><th>' + __t(\"invitations.col.expires\") + '</th><th class=\"col-hide-md\">' + __t(\"invitations.col.createdBy\") + '</th><th></th></tr></thead><tbody>';\n invitations.forEach(function(inv) {\n var roleBadge = '<span class=\"badge badge-' + inv.role + '\">' + __t(\"common.\" + inv.role) + '</span>';\n var remark = inv.remark ? escapeHtml(inv.remark.length > 30 ? inv.remark.slice(0, 30) + \"...\" : inv.remark) : '<span class=\"text-muted\">\u2014</span>';\n var uses = inv.useCount + \" / \" + inv.maxUses;\n if (inv.useCount >= inv.maxUses) uses += ' <span class=\"badge badge-blocked\">' + __t(\"invitations.closed\") + '</span>';\n var expired = new Date(inv.expireAt) < new Date();\n var expireText = expired ? '<span class=\"badge badge-expired\">' + __t(\"common.expired\") + '</span>' : '<span class=\"cell-date\">' + escapeHtml(relativeTime(inv.expireAt)) + '</span>';\n var isActive = !expired && inv.status !== \"closed\" && inv.useCount < inv.maxUses;\n var inviter = inv.inviterName ? escapeHtml(inv.inviterName) : '<span class=\"text-muted\">\u2014</span>';\n\n var actions = '';\n if (isActive) {\n actions += '<button class=\"action-menu-item\" onclick=\"event.stopPropagation();copyToClipboard(\\'' + escapeHtml(inv.link) + '\\')\">' + __t(\"invitations.copyLink\") + '</button>';\n }\n var canDelete = __caller.role === \"owner\" || inv.inviterDid === __caller.did;\n if (canDelete) {\n if (actions) actions += '<div class=\"action-menu-sep\"></div>';\n actions += '<button class=\"action-menu-item danger\" onclick=\"event.stopPropagation();deleteInvitation(\\'' + escapeHtml(inv.id) + '\\')\">' + __t(\"common.delete\") + '</button>';\n }\n var actionMenu = actions ? '<td class=\"action-cell\"><button class=\"action-trigger\" onclick=\"event.stopPropagation();toggleActionMenu(this)\">\u22EF</button><div class=\"action-menu\">' + actions + '</div></td>' : '<td></td>';\n\n html += '<tr><td>' + roleBadge + '</td><td class=\"col-hide-sm\">' + remark + '</td><td>' + uses + '</td><td>' + expireText + '</td><td class=\"col-hide-md\">' + inviter + '</td>' + actionMenu + '</tr>';\n });\n html += '</tbody></table>';\n wrap.innerHTML = html;\n\n renderPagination(\"invitations-pagination\", paging, loadInvitations);\n}\n\nasync function createInvitation() {\n var btn = document.getElementById(\"create-invite-btn\");\n btn.disabled = true;\n\n var role = document.getElementById(\"invite-role\").value;\n var remark = document.getElementById(\"invite-remark\").value.trim();\n var expireHours = parseInt(document.getElementById(\"invite-expire\").value, 10);\n var maxUses = parseInt(document.getElementById(\"invite-max-uses\").value, 10) || 1;\n maxUses = Math.min(100, Math.max(1, maxUses));\n\n var data = await api(\"POST\", \"/invitations\", { role: role, remark: remark, expireHours: expireHours, maxUses: maxUses });\n btn.disabled = false;\n\n if (data.ok && data.invitation) {\n closeDialog(\"create-invitation-dialog\");\n copyToClipboard(data.invitation.link);\n showToast(__t(\"invitations.created\"), \"success\");\n // Reset form\n document.getElementById(\"invite-remark\").value = \"\";\n document.getElementById(\"invite-max-uses\").value = \"1\";\n updateRemarkCounter();\n loadInvitations(invitationsPage);\n }\n}\n\nasync function deleteInvitation(id) {\n var ok = await confirmDialog({ title: __t(\"invitations.deleteTitle\"), message: __t(\"invitations.deleteMsg\"), confirmLabel: __t(\"common.delete\"), danger: true });\n if (!ok) return;\n var data = await api(\"DELETE\", \"/invitations/\" + encodeURIComponent(id));\n if (data.ok) { showToast(__t(\"invitations.deleted\"), \"success\"); loadInvitations(invitationsPage); }\n}\n\n// Initialize role options based on caller role\nfunction initInviteRoleOptions() {\n var select = document.getElementById(\"invite-role\");\n if (__caller.role === \"owner\") {\n select.innerHTML = '<option value=\"guest\">' + __t(\"common.guest\") + '</option><option value=\"member\">' + __t(\"common.member\") + '</option><option value=\"admin\">' + __t(\"common.admin\") + '</option>';\n }\n}\n\nregisterTab(\"invitations\", function() {\n initInviteRoleOptions();\n loadInvitations(1);\n});\n";
6
+ export declare const INVITATIONS_TAB_SCRIPT = "\nvar invitationsPage = 1;\nvar invitationsPageSize = 20;\n\n\nfunction updateRemarkCounter() {\n document.getElementById(\"remark-counter\").textContent = document.getElementById(\"invite-remark\").value.length;\n}\n\nasync function loadInvitations(page) {\n invitationsPage = page || 1;\n var qs = \"?page=\" + invitationsPage + \"&pageSize=\" + invitationsPageSize;\n document.getElementById(\"invitations-table-wrap\").innerHTML = skeletonTable(6, 4);\n document.getElementById(\"invitations-pagination\").innerHTML = \"\";\n var data = await api(\"GET\", \"/invitations\" + qs);\n if (!data.ok) {\n document.getElementById(\"invitations-table-wrap\").innerHTML = '<div class=\"empty-state\"><div class=\"empty-state-icon\"><svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4m0 4h.01\"/></svg></div><div class=\"empty-state-title\">' + __t(\"invitations.loadFailed\") + '</div></div>';\n return;\n }\n renderInvitationsTable(data.invitations, data.paging);\n}\n\nfunction renderInvitationsTable(invitations, paging) {\n var wrap = document.getElementById(\"invitations-table-wrap\");\n if (!invitations || invitations.length === 0) {\n wrap.innerHTML = '<div class=\"empty-state\"><div class=\"empty-state-icon\"><svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M22 12h-4l-3 9L9 3l-3 9H2\"/></svg></div><div class=\"empty-state-title\">' + __t(\"invitations.noInvitations\") + '</div><div class=\"empty-state-desc\">' + __t(\"invitations.noInvitationsHint\") + '</div></div>';\n document.getElementById(\"invitations-pagination\").innerHTML = \"\";\n return;\n }\n\n var html = '<table class=\"table\"><thead><tr><th>' + __t(\"invitations.col.role\") + '</th><th class=\"col-hide-sm\">' + __t(\"invitations.col.remark\") + '</th><th>' + __t(\"invitations.col.uses\") + '</th><th>' + __t(\"invitations.col.expires\") + '</th><th class=\"col-hide-md\">' + __t(\"invitations.col.createdBy\") + '</th><th></th></tr></thead><tbody>';\n invitations.forEach(function(inv) {\n var roleBadge = '<span class=\"badge badge-' + inv.role + '\">' + __t(\"common.\" + inv.role) + '</span>';\n var remark = inv.remark ? escapeHtml(inv.remark.length > 30 ? inv.remark.slice(0, 30) + \"...\" : inv.remark) : '<span class=\"text-muted\">\u2014</span>';\n var uses = inv.useCount + \" / \" + inv.maxUses;\n if (inv.useCount >= inv.maxUses) uses += ' <span class=\"badge badge-blocked\">' + __t(\"invitations.closed\") + '</span>';\n var expired = new Date(inv.expireAt) < new Date();\n var expireText = expired ? '<span class=\"badge badge-expired\">' + __t(\"common.expired\") + '</span>' : '<span class=\"cell-date\">' + escapeHtml(relativeTime(inv.expireAt)) + '</span>';\n var isActive = !expired && inv.status !== \"closed\" && inv.useCount < inv.maxUses;\n var inviter = inv.inviterName ? escapeHtml(inv.inviterName) : '<span class=\"text-muted\">\u2014</span>';\n\n var actions = '';\n if (isActive) {\n actions += '<button class=\"action-menu-item\" onclick=\"event.stopPropagation();copyToClipboard(\\'' + escapeHtml(inv.link) + '\\')\">' + __t(\"invitations.copyLink\") + '</button>';\n }\n var canDelete = __caller.role === \"owner\" || inv.inviterDid === __caller.did;\n if (canDelete) {\n if (actions) actions += '<div class=\"action-menu-sep\"></div>';\n actions += '<button class=\"action-menu-item danger\" onclick=\"event.stopPropagation();deleteInvitation(\\'' + escapeHtml(inv.id) + '\\')\">' + __t(\"common.delete\") + '</button>';\n }\n var actionMenu = actions ? '<td class=\"action-cell\"><button class=\"action-trigger\" onclick=\"event.stopPropagation();toggleActionMenu(this)\">\u22EF</button><div class=\"action-menu\">' + actions + '</div></td>' : '<td></td>';\n\n html += '<tr><td>' + roleBadge + '</td><td class=\"col-hide-sm\">' + remark + '</td><td>' + uses + '</td><td>' + expireText + '</td><td class=\"col-hide-md\">' + inviter + '</td>' + actionMenu + '</tr>';\n });\n html += '</tbody></table>';\n wrap.innerHTML = html;\n\n renderPagination(\"invitations-pagination\", paging, loadInvitations);\n}\n\nasync function createInvitation() {\n var btn = document.getElementById(\"create-invite-btn\");\n btn.disabled = true;\n\n var role = document.getElementById(\"invite-role\").value;\n var remark = document.getElementById(\"invite-remark\").value.trim();\n var expireHours = parseInt(document.getElementById(\"invite-expire\").value, 10);\n var maxUses = parseInt(document.getElementById(\"invite-max-uses\").value, 10) || 1;\n maxUses = Math.min(100, Math.max(1, maxUses));\n\n var data = await api(\"POST\", \"/invitations\", { role: role, remark: remark, expireHours: expireHours, maxUses: maxUses });\n btn.disabled = false;\n\n if (data.ok && data.invitation) {\n closeDialog(\"create-invitation-dialog\");\n copyToClipboard(data.invitation.link);\n showToast(__t(\"invitations.created\"), \"success\");\n // Reset form\n document.getElementById(\"invite-remark\").value = \"\";\n document.getElementById(\"invite-max-uses\").value = \"1\";\n updateRemarkCounter();\n loadInvitations(invitationsPage);\n }\n}\n\nasync function deleteInvitation(id) {\n var ok = await confirmDialog({ title: __t(\"invitations.deleteTitle\"), message: __t(\"invitations.deleteMsg\"), confirmLabel: __t(\"common.delete\"), danger: true });\n if (!ok) return;\n var data = await api(\"DELETE\", \"/invitations/\" + encodeURIComponent(id));\n if (data.ok) { showToast(__t(\"invitations.deleted\"), \"success\"); loadInvitations(invitationsPage); }\n}\n\n// Initialize role options based on caller role\nfunction initInviteRoleOptions() {\n var select = document.getElementById(\"invite-role\");\n if (__caller.role === \"owner\") {\n select.innerHTML = '<option value=\"guest\">' + __t(\"common.guest\") + '</option><option value=\"member\">' + __t(\"common.member\") + '</option><option value=\"admin\">' + __t(\"common.admin\") + '</option>';\n }\n}\n\n// Button-driven mutations must not double-submit (submitOnce \u2014 utils.ts).\n// createInvitation locks its own button (#create-invite-btn) already.\ndeleteInvitation = submitOnce(deleteInvitation);\n\nregisterTab(\"invitations\", function() {\n initInviteRoleOptions();\n loadInvitations(1);\n});\n";
7
7
  export {};
8
8
  //# sourceMappingURL=tab-invitations.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tab-invitations.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-invitations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC;AAEpE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAmDjD;AAED,eAAO,MAAM,sBAAsB,opMAwGlC,CAAC"}
1
+ {"version":3,"file":"tab-invitations.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-invitations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC;AAEpE,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAmDjD;AAED,eAAO,MAAM,sBAAsB,i2MA4GlC,CAAC"}
@@ -153,6 +153,10 @@ function initInviteRoleOptions() {
153
153
  }
154
154
  }
155
155
 
156
+ // Button-driven mutations must not double-submit (submitOnce — utils.ts).
157
+ // createInvitation locks its own button (#create-invite-btn) already.
158
+ deleteInvitation = submitOnce(deleteInvitation);
159
+
156
160
  registerTab("invitations", function() {
157
161
  initInviteRoleOptions();
158
162
  loadInvitations(1);
@@ -1 +1 @@
1
- {"version":3,"file":"tab-invitations.js","sourceRoot":"","sources":["../../../src/pages/admin/tab-invitations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,UAAU,kBAAkB,CAAC,CAAM;IACvC,OAAO,UAAU,CAAC;;;yDAGqC,CAAC,CAAC,mBAAmB,CAAC;oFACK,CAAC,CAAC,oBAAoB,CAAC;;4BAE/E,CAAC,CAAC,kBAAkB,CAAC;;;;;;;;;;;UAWvC,CAAC,CAAC,oBAAoB,CAAC;6BACJ,CAAC,CAAC,wBAAwB,CAAC;;kCAEtB,CAAC,CAAC,kBAAkB,CAAC;;gCAEvB,CAAC,CAAC,cAAc,CAAC;iCAChB,CAAC,CAAC,eAAe,CAAC;;;;kCAIjB,CAAC,CAAC,oBAAoB,CAAC;yFACgC,CAAC,CAAC,+BAA+B,CAAC;oEACvD,CAAC,CAAC,uBAAuB,CAAC;;;kCAG5D,CAAC,CAAC,uBAAuB,CAAC;;6BAE/B,CAAC,CAAC,kBAAkB,CAAC;uCACX,CAAC,CAAC,mBAAmB,CAAC;8BAC/B,CAAC,CAAC,oBAAoB,CAAC;;;;kCAInB,CAAC,CAAC,qBAAqB,CAAC;;;;;0FAKgC,CAAC,CAAC,eAAe,CAAC;8EAC9B,CAAC,CAAC,uBAAuB,CAAC;;;CAGvG,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwG9C,CAAC"}
1
+ {"version":3,"file":"tab-invitations.js","sourceRoot":"","sources":["../../../src/pages/admin/tab-invitations.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,UAAU,kBAAkB,CAAC,CAAM;IACvC,OAAO,UAAU,CAAC;;;yDAGqC,CAAC,CAAC,mBAAmB,CAAC;oFACK,CAAC,CAAC,oBAAoB,CAAC;;4BAE/E,CAAC,CAAC,kBAAkB,CAAC;;;;;;;;;;;UAWvC,CAAC,CAAC,oBAAoB,CAAC;6BACJ,CAAC,CAAC,wBAAwB,CAAC;;kCAEtB,CAAC,CAAC,kBAAkB,CAAC;;gCAEvB,CAAC,CAAC,cAAc,CAAC;iCAChB,CAAC,CAAC,eAAe,CAAC;;;;kCAIjB,CAAC,CAAC,oBAAoB,CAAC;yFACgC,CAAC,CAAC,+BAA+B,CAAC;oEACvD,CAAC,CAAC,uBAAuB,CAAC;;;kCAG5D,CAAC,CAAC,uBAAuB,CAAC;;6BAE/B,CAAC,CAAC,kBAAkB,CAAC;uCACX,CAAC,CAAC,mBAAmB,CAAC;8BAC/B,CAAC,CAAC,oBAAoB,CAAC;;;;kCAInB,CAAC,CAAC,qBAAqB,CAAC;;;;;0FAKgC,CAAC,CAAC,eAAe,CAAC;8EAC9B,CAAC,CAAC,uBAAuB,CAAC;;;CAGvG,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4G9C,CAAC"}
@@ -3,6 +3,6 @@
3
3
  */
4
4
  type TFn = (key: string, params?: Record<string, string>) => string;
5
5
  export declare function membersTabHtml(t: TFn): string;
6
- export declare const MEMBERS_TAB_SCRIPT = "\nvar membersPage = 1;\nvar membersPageSize = 20;\nvar memberSearchTimeout = null;\nvar currentMembersList = [];\nvar selectedMember = null;\nvar __membersAggregated = false;\n// Sort state: empty string = default (server decides + self-first on client).\n// Three-state click cycle: (none) \u2192 desc \u2192 asc \u2192 (none).\n// Persisted in location.search as sortBy/sortOrder so it survives refresh + share.\nvar membersSortBy = \"\";\nvar membersSortOrder = \"\";\nvar MEMBERS_SORT_VALID = [\"name\", \"role\", \"status\", \"source\", \"joined\"];\n\n/** Populate module sort state from the current URL query string. Idempotent. */\nfunction readMembersSortFromURL() {\n membersSortBy = \"\";\n membersSortOrder = \"\";\n var params = new URLSearchParams(location.search);\n var by = params.get(\"sortBy\");\n var order = params.get(\"sortOrder\");\n if (by && MEMBERS_SORT_VALID.indexOf(by) >= 0) {\n membersSortBy = by;\n membersSortOrder = (order === \"asc\" || order === \"desc\") ? order : \"desc\";\n }\n}\n\n/** Write current sort state back to location.search without reloading. */\nfunction writeMembersSortToURL() {\n var params = new URLSearchParams(location.search);\n if (membersSortBy) {\n params.set(\"sortBy\", membersSortBy);\n params.set(\"sortOrder\", membersSortOrder || \"desc\");\n } else {\n params.delete(\"sortBy\");\n params.delete(\"sortOrder\");\n }\n var qs = params.toString();\n var newUrl = location.pathname + (qs ? \"?\" + qs : \"\") + location.hash;\n history.replaceState(null, \"\", newUrl);\n}\n\n/** Cycle sort state for a column key and reload. */\nfunction toggleSort(key) {\n if (membersSortBy !== key) {\n membersSortBy = key;\n membersSortOrder = \"desc\";\n } else if (membersSortOrder === \"desc\") {\n membersSortOrder = \"asc\";\n } else {\n // Third click \u2014 back to default.\n membersSortBy = \"\";\n membersSortOrder = \"\";\n }\n writeMembersSortToURL();\n loadMembers(1);\n}\n\n/** Render a <th> with click-to-sort behavior. */\nfunction sortableTh(key, label, extraClass) {\n var isActive = membersSortBy === key;\n var arrow;\n if (isActive) {\n arrow = membersSortOrder === \"asc\"\n ? '<span class=\"sort-arrow sort-arrow-active\">\\u2191</span>'\n : '<span class=\"sort-arrow sort-arrow-active\">\\u2193</span>';\n } else {\n arrow = '<span class=\"sort-arrow\">\\u2195</span>';\n }\n var cls = \"sortable\" + (isActive ? \" sort-active\" : \"\") + (extraClass ? \" \" + extraClass : \"\");\n return '<th class=\"' + cls + '\" onclick=\"toggleSort(\\'' + key + '\\')\">' + label + arrow + '</th>';\n}\n\nfunction providerLabel(sp) {\n if (!sp) return \"\\u2014\";\n var key = \"members.providers.\" + sp;\n var val = __t(key);\n return val !== key ? val : sp;\n}\n\nfunction debouncedMemberSearch() {\n clearTimeout(memberSearchTimeout);\n memberSearchTimeout = setTimeout(function() { loadMembers(1); }, 300);\n}\n\n// Load registered instances for the filter dropdown (system admin only)\nvar __instanceNames = {};\nvar __instanceFilterInitialized = false;\nasync function initInstanceFilter() {\n if (__instanceFilterInitialized) return;\n try {\n // Best-effort: this endpoint is system-admin-only. Instance-scoped owners/admins\n // get 403 here, which is expected \u2014 fail silently (no error toast).\n var data = await api(\"GET\", \"/../admin/instances\", undefined, { silent: true });\n if (data.ok && data.instances && data.instances.length > 0) {\n __instanceFilterInitialized = true;\n var sel = document.getElementById(\"members-instance-filter\");\n data.instances.forEach(function(inst) {\n __instanceNames[inst.instanceDid] = inst.appName || inst.instanceDid.substring(0, 16);\n var opt = document.createElement(\"option\");\n opt.value = inst.instanceDid;\n opt.textContent = __instanceNames[inst.instanceDid];\n sel.appendChild(opt);\n });\n sel.style.display = \"\";\n }\n } catch(e) { /* not admin or no instances */ }\n}\n\n// Also support specific instance filter via query param\n// When ?instance=z1xxx, team handler needs to use that instanceDid\n\nasync function loadMembers(page) {\n membersPage = page || 1;\n var instanceFilter = document.getElementById(\"members-instance-filter\").value;\n var qs = \"?page=\" + membersPage + \"&pageSize=\" + membersPageSize;\n // System view (\"\") \u2192 use defaultInstanceDid so we query membership, not global users\n if (instanceFilter === \"\" && __pageData.instanceDid) {\n qs += \"&instance=\" + encodeURIComponent(__pageData.instanceDid);\n } else if (instanceFilter) {\n qs += \"&instance=\" + encodeURIComponent(instanceFilter);\n }\n var search = document.getElementById(\"members-search\").value.trim();\n if (search) qs += \"&search=\" + encodeURIComponent(search);\n var role = document.getElementById(\"members-role-filter\").value;\n if (role) qs += \"&role=\" + encodeURIComponent(role);\n var status = document.getElementById(\"members-status-filter\").value;\n if (status !== \"\") qs += \"&approved=\" + status;\n var source = document.getElementById(\"members-source-filter\").value;\n if (source) qs += \"&sourceProvider=\" + encodeURIComponent(source);\n if (membersSortBy) {\n qs += \"&sortBy=\" + encodeURIComponent(membersSortBy);\n qs += \"&sortOrder=\" + encodeURIComponent(membersSortOrder || \"desc\");\n }\n\n document.getElementById(\"members-table-wrap\").innerHTML = skeletonTable(8, 6);\n document.getElementById(\"members-pagination\").innerHTML = \"\";\n var data = await api(\"GET\", \"/members\" + qs);\n if (!data.ok) {\n document.getElementById(\"members-table-wrap\").innerHTML = '<div class=\"empty-state\"><div class=\"empty-state-icon\"><svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4m0 4h.01\"/></svg></div><div class=\"empty-state-title\">' + __t(\"members.loadFailed\") + '</div></div>';\n return;\n }\n __membersAggregated = !!data.aggregated;\n // Only pin the caller to the top when using the default (server) order.\n // Under explicit sort, honor the user's intent without re-shuffling.\n if (membersSortBy) {\n currentMembersList = data.users || [];\n } else {\n currentMembersList = (data.users || []).slice().sort(function(a, b) {\n if (a.did === __caller.did) return -1;\n if (b.did === __caller.did) return 1;\n return (a.createdAt || 0) - (b.createdAt || 0);\n });\n }\n renderMembersTable(currentMembersList, data.paging);\n updateTransferSection(data.users);\n}\n\nfunction renderMembersTable(users, paging) {\n var wrap = document.getElementById(\"members-table-wrap\");\n // Aggregated (cross-instance \"All Instances\") view can't tell which instance\n // a role change should apply to, so per-row actions are disabled there \u2014 see\n // canOperate below. Surface why, instead of silently hiding the buttons.\n var aggregatedHint = __membersAggregated ? '<p class=\"text-muted\" style=\"font-size:13px;margin-bottom:12px;\">' + __t(\"members.aggregatedHint\") + '</p>' : '';\n if (!users || users.length === 0) {\n wrap.innerHTML = aggregatedHint + '<div class=\"empty-state\"><div class=\"empty-state-icon\"><svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/></svg></div><div class=\"empty-state-title\">' + __t(\"members.noMembers\") + '</div><div class=\"empty-state-desc\">' + __t(\"members.noMembersHint\") + '</div></div>';\n document.getElementById(\"members-pagination\").innerHTML = \"\";\n return;\n }\n\n var instanceSel = document.getElementById(\"members-instance-filter\");\n var instanceVisible = instanceSel && instanceSel.style.display !== 'none';\n var showInstance = instanceVisible && instanceSel.value === \"all\";\n var html = aggregatedHint + '<table class=\"table\"><thead><tr>';\n html += sortableTh(\"name\", __t(\"members.col.member\"));\n html += sortableTh(\"role\", __t(\"members.col.role\"));\n if (showInstance) html += '<th>' + __t(\"members.col.instance\") + '</th>';\n html += sortableTh(\"source\", __t(\"members.col.source\"), \"col-hide-sm\");\n html += sortableTh(\"status\", __t(\"members.col.status\"), \"col-hide-sm\");\n html += sortableTh(\"joined\", __t(\"members.col.joined\"), \"col-hide-md\");\n // Registered/Last Login From columns removed \u2014 domain info not meaningful in Worker context\n html += '<th></th></tr></thead><tbody>';\n users.forEach(function(u) {\n var userDid = u.did || u.user_did || '';\n var av = renderAvatar(u.avatar, u.fullName, \"sm\");\n var name = u.fullName ? escapeHtml(u.fullName) : '<span class=\"text-muted\">(unnamed)</span>';\n var did = '<did-address did=\"' + escapeHtml(userDid) + '\" compact></did-address>';\n var roleBadge = '<span class=\"badge badge-' + (u.role || \"guest\") + '\">' + __t(\"common.\" + (u.role || \"guest\")) + '</span>';\n var statusBadge = u.approved !== undefined ? (u.approved ? '<span class=\"badge badge-active\">' + __t(\"common.active\") + '</span>' : '<span class=\"badge badge-blocked\">' + __t(\"common.blocked\") + '</span>') : '<span class=\"text-muted\">\u2014</span>';\n var joined = (u.createdAt || u.joined_at) ? '<span class=\"cell-date\">' + escapeHtml(relativeTime(u.createdAt || u.joined_at)) + '</span>' : '<span class=\"text-muted\">\u2014</span>';\n html += '<tr style=\"cursor:pointer\" onclick=\"openMemberDetail(\\'' + escapeHtml(userDid) + '\\')\">';\n html += '<td><div class=\"member-identity\">' + av + '<div><div class=\"member-name\">' + name + '</div><div class=\"member-email\">' + did + '</div></div></div></td>';\n html += '<td>' + roleBadge + '</td>';\n if (showInstance) {\n if (__membersAggregated && u.instance_count > 1) {\n html += '<td><a href=\"javascript:void(0)\" onclick=\"event.stopPropagation();openMemberDetail(\\'' + escapeHtml(userDid) + '\\')\" style=\"color:var(--blue);font-size:14px;text-decoration:none\">' + u.instance_count + ' ' + __t(\"members.instances\") + '</a></td>';\n } else {\n var instDid = (__membersAggregated && u.instances && u.instances.length === 1) ? u.instances[0].instance_did : (u.instance_did || '');\n var instName = __instanceNames[instDid] || '';\n if (!instName) instName = instDid ? __t(\"members.currentInstance\") : '\u2014';\n html += '<td><div>' + escapeHtml(instName);\n if (instDid) html += '<div style=\"font-size:11px;color:var(--text-secondary);margin-top:2px\" onclick=\"event.stopPropagation()\"><did-address did=\"' + escapeHtml(instDid) + '\" compact></did-address></div>';\n html += '</div></td>';\n }\n }\n var sourceBadge = '<span class=\"text-muted\">' + escapeHtml(providerLabel(u.sourceProvider)) + '</span>';\n html += '<td class=\"col-hide-sm\">' + sourceBadge + '</td>';\n html += '<td class=\"col-hide-sm\">' + statusBadge + '</td>';\n html += '<td class=\"col-hide-md\">' + joined + '</td>';\n // Actions: only for current instance members, not cross-instance\n var canOperate = !instanceVisible || (instanceVisible && instanceSel.value === '');\n // In aggregated mode, only allow actions on users whose instance matches current system\n if (__membersAggregated) canOperate = false;\n html += '<td class=\"action-cell\">' + (canOperate ? renderMemberActions(u) : '') + '</td>';\n html += '</tr>';\n });\n html += '</tbody></table>';\n wrap.innerHTML = html;\n\n renderPagination(\"members-pagination\", paging, loadMembers);\n}\n\nfunction renderMemberActions(u) {\n var isOwner = __caller.role === \"owner\";\n var isAdmin = __caller.role === \"admin\";\n var isSelf = u.did === __caller.did;\n var ROLE_LEVEL = { owner: 3, admin: 2, member: 1, guest: 0 };\n var callerLevel = ROLE_LEVEL[__caller.role] || 0;\n var targetLevel = ROLE_LEVEL[u.role] || 0;\n // Can only act on members with lower privilege\n var canManage = !isSelf && targetLevel < callerLevel;\n var items = [];\n\n // Change role: can manage lower-level members\n if (canManage) {\n items.push('<button class=\"action-menu-item\" onclick=\"event.stopPropagation();openChangeRoleForDid(\\'' + escapeHtml(u.did) + '\\')\">' + __t(\"members.changeRole\") + '</button>');\n }\n if (canManage && u.approved) {\n items.push('<button class=\"action-menu-item\" onclick=\"event.stopPropagation();blockMember(\\'' + escapeHtml(u.did) + '\\')\">' + __t(\"members.block\") + '</button>');\n }\n if (canManage && !u.approved) {\n items.push('<button class=\"action-menu-item\" onclick=\"event.stopPropagation();unblockMember(\\'' + escapeHtml(u.did) + '\\')\">' + __t(\"members.unblock\") + '</button>');\n }\n if (items.length > 0 && canManage) {\n items.push('<div class=\"action-menu-sep\"></div>');\n }\n if (canManage) {\n items.push('<button class=\"action-menu-item danger\" onclick=\"event.stopPropagation();removeMember(\\'' + escapeHtml(u.did) + '\\')\">' + __t(\"members.remove\") + '</button>');\n }\n\n if (items.length === 0) return \"\";\n return '<button class=\"action-trigger\" onclick=\"event.stopPropagation();toggleActionMenu(this)\">\u22EF</button><div class=\"action-menu\">' + items.join(\"\") + '</div>';\n}\n\nfunction toggleActionMenu(trigger) {\n var menu = trigger.nextElementSibling;\n var wasOpen = menu.classList.contains(\"open\");\n // Close all menus first\n document.querySelectorAll(\".action-menu.open\").forEach(function(m) { m.classList.remove(\"open\"); });\n if (!wasOpen) menu.classList.toggle(\"open\");\n}\n\nasync function openMemberDetail(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n if (!u) return;\n selectedMember = u;\n\n // Fetch full member info (with connectedAccounts) from API.\n // Falls back to in-memory list data if the request fails.\n var detail = u;\n try {\n var resp = await api(\"GET\", \"/members/\" + encodeURIComponent(did));\n if (resp && resp.ok && resp.user) {\n // Merge: in-memory list has fields the API may omit (instances, passkeyCount in some paths).\n detail = Object.assign({}, u, resp.user);\n selectedMember = detail;\n }\n } catch (e) {}\n\n var body = document.getElementById(\"member-detail-body\");\n var html = '<div class=\"profile-header\">' + renderAvatar(detail.avatar, detail.fullName) + '<div>';\n html += '<div style=\"font-weight:500;font-size:16px;color:var(--text-white)\">' + escapeHtml(detail.fullName || \"(unnamed)\") + '</div>';\n html += '<did-address did=\"' + escapeHtml(detail.did) + '\" compact></did-address>';\n html += '</div></div>';\n\n html += '<div class=\"settings-card\" style=\"margin-top:16px\">';\n html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailRole\") + '</span><span class=\"settings-value\"><span class=\"badge badge-' + (detail.role || \"guest\") + '\">' + __t(\"common.\" + (detail.role || \"guest\")) + '</span></span></div>';\n html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailStatus\") + '</span><span class=\"settings-value\">' + (detail.approved ? '<span class=\"badge badge-active\">' + __t(\"common.active\") + '</span>' : '<span class=\"badge badge-blocked\">' + __t(\"common.blocked\") + '</span>') + '</span></div>';\n html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailSource\") + '</span><span class=\"settings-value\">' + escapeHtml(providerLabel(detail.sourceProvider)) + '</span></div>';\n if (detail.email) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailEmail\") + '</span><span class=\"settings-value\">' + escapeHtml(detail.email) + '</span></div>';\n if (detail.inviterName) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailInvitedBy\") + '</span><span class=\"settings-value\">' + escapeHtml(detail.inviterName) + '</span></div>';\n if (detail.createdAt) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailJoined\") + '</span><span class=\"settings-value\">' + escapeHtml(absoluteTime(detail.createdAt)) + '</span></div>';\n if (detail.lastLoginAt) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailLastLogin\") + '</span><span class=\"settings-value\">' + escapeHtml(absoluteTime(detail.lastLoginAt)) + '</span></div>';\n if (detail.sourceDomain) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailRegisteredFrom\") + '</span><span class=\"settings-value\">' + escapeHtml(detail.sourceDomain) + '</span></div>';\n if (detail.lastLoginDomain) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailLastLoginFrom\") + '</span><span class=\"settings-value\">' + escapeHtml(detail.lastLoginDomain) + '</span></div>';\n if (detail.passkeyCount !== undefined) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailPasskeys\") + '</span><span class=\"settings-value\">' + detail.passkeyCount + '</span></div>';\n html += '</div>';\n\n // Connected Accounts (read-only)\n if (detail.connectedAccounts && detail.connectedAccounts.length > 0) {\n html += '<div class=\"settings-card\" style=\"margin-top:16px\">';\n html += '<div class=\"settings-card-header\"><span>' + __t(\"members.detailConnectedAccounts\") + ' (' + detail.connectedAccounts.length + ')</span></div>';\n html += '<div style=\"max-height:240px;overflow-y:auto\">';\n detail.connectedAccounts.forEach(function(a) {\n var label = providerLabel(a.provider);\n var info = '';\n if (a.userInfo && a.userInfo.name) info = a.userInfo.name;\n else if (a.userInfo && a.userInfo.email) info = a.userInfo.email;\n else if (a.id) info = a.id;\n html += '<div class=\"settings-row\" style=\"gap:12px;align-items:flex-start\">';\n html += '<div style=\"flex:1;min-width:0\">';\n html += '<div style=\"font-size:14px;font-weight:500;color:var(--text)\">' + escapeHtml(label);\n if (a.isMain) html += ' <span class=\"badge badge-active\" style=\"font-size:10px;padding:1px 6px;\">' + __t(\"profile.mainAccount\") + '</span>';\n html += '</div>';\n if (info) html += '<div style=\"font-size:12px;color:var(--text-secondary);margin-top:2px\">' + escapeHtml(info) + '</div>';\n if (a.did) html += '<div style=\"font-size:11px;color:var(--text-secondary);margin-top:2px\"><did-address did=\"' + escapeHtml(a.did) + '\" compact></did-address></div>';\n html += '</div>';\n if (a.lastLoginAt) html += '<div style=\"font-size:12px;color:var(--text-secondary);flex-shrink:0\">' + escapeHtml(relativeTime(a.lastLoginAt)) + '</div>';\n html += '</div>';\n });\n html += '</div></div>';\n }\n\n // Instance memberships (aggregated mode)\n if (detail.instances && detail.instances.length > 0) {\n html += '<div class=\"settings-card\" style=\"margin-top:16px\">';\n html += '<div class=\"settings-card-header\"><span>' + __t(\"members.instanceMemberships\") + ' (' + detail.instances.length + ')</span></div>';\n html += '<div style=\"max-height:240px;overflow-y:auto\">';\n detail.instances.forEach(function(inst) {\n var instName = __instanceNames[inst.instance_did] || '';\n var instDid = inst.instance_did || '';\n html += '<div class=\"settings-row\" style=\"gap:12px\">';\n html += '<div style=\"flex:1;min-width:0\"><div style=\"font-size:14px;font-weight:500;color:var(--text)\">' + escapeHtml(instName || __t(\"members.currentInstance\")) + '</div>';\n if (instDid) html += '<div style=\"font-size:11px;color:var(--text-secondary);margin-top:2px\"><did-address did=\"' + escapeHtml(instDid) + '\" compact></did-address></div>';\n html += '</div>';\n html += '<div style=\"display:flex;align-items:center;gap:8px;flex-shrink:0\"><span class=\"badge badge-' + inst.role + '\">' + __t(\"common.\" + inst.role) + '</span>';\n if (inst.joined_at) html += '<span style=\"font-size:12px;color:var(--text-secondary)\">' + escapeHtml(relativeTime(inst.joined_at)) + '</span>';\n html += '</div></div>';\n });\n html += '</div></div>';\n }\n\n body.innerHTML = html;\n\n // Change role: mirror list's canOperate + renderMemberActions logic exactly.\n // Use post-merge detail so role/did reflect the latest API response.\n var changeRoleBtn = document.getElementById(\"member-detail-change-role\");\n var instanceSel = document.getElementById(\"members-instance-filter\");\n var instanceVisible = instanceSel && instanceSel.style.display !== 'none';\n var canOperate = !instanceVisible || (instanceVisible && instanceSel.value === '');\n if (__membersAggregated) canOperate = false;\n var isSelf = detail.did === __caller.did;\n var ROLE_LEVEL = { owner: 3, admin: 2, member: 1, guest: 0 };\n var callerLevel = ROLE_LEVEL[__caller.role] || 0;\n var targetLevel = ROLE_LEVEL[detail.role] || 0;\n var canChangeRole = canOperate && !isSelf && targetLevel < callerLevel;\n if (canChangeRole) {\n changeRoleBtn.classList.remove(\"hidden\");\n } else {\n changeRoleBtn.classList.add(\"hidden\");\n }\n\n showDialog(\"member-detail-dialog\");\n}\n\nfunction openChangeRoleForDid(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n if (!u) return;\n selectedMember = u;\n openChangeRoleDialog();\n}\n\nfunction openChangeRoleDialog() {\n if (!selectedMember) return;\n document.getElementById(\"change-role-title\").textContent = __t(\"members.changeRole\");\n document.getElementById(\"change-role-subtitle\").textContent = __t(\"members.changeRoleFor\", { name: selectedMember.fullName || truncateDid(selectedMember.did) });\n // Show/hide role options based on caller's level (only owner can assign admin)\n var radios = document.querySelectorAll('input[name=\"new-role\"]');\n radios.forEach(function(r) {\n r.checked = r.value === selectedMember.role;\n var label = r.closest('label');\n if (label) {\n if (r.value === 'admin' && __caller.role !== 'owner') label.style.display = 'none';\n else label.style.display = '';\n }\n });\n closeDialog(\"member-detail-dialog\");\n showDialog(\"change-role-dialog\");\n}\n\nasync function saveRoleChange() {\n if (!selectedMember) return;\n var selected = document.querySelector('input[name=\"new-role\"]:checked');\n if (!selected) return;\n var newRole = selected.value;\n var data = await api(\"PUT\", \"/members/\" + encodeURIComponent(selectedMember.did) + \"/role\", { role: newRole });\n if (data.ok) {\n showToast(__t(\"members.roleUpdated\"), \"success\");\n closeDialog(\"change-role-dialog\");\n loadMembers(membersPage);\n }\n}\n\n// name is looked up (not passed via onclick) so a member's self-set fullName\n// never has to be embedded as a JS-string literal inside an inline onclick=\n// attribute \u2014 HTML entity-escaping alone can't fully neutralize that context,\n// since the browser decodes entities before parsing onclick as script.\nasync function blockMember(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n var name = (u && u.fullName) || did;\n var ok = await confirmDialog({ title: __t(\"members.blockTitle\"), message: __t(\"members.blockMsg\", { name: name }), confirmLabel: __t(\"members.blockBtn\"), danger: true });\n if (!ok) return;\n var data = await api(\"PUT\", \"/members/\" + encodeURIComponent(did) + \"/approval\", { approved: false });\n if (data.ok) { showToast(__t(\"members.blockSuccess\"), \"success\"); loadMembers(membersPage); }\n}\n\nasync function unblockMember(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n var name = (u && u.fullName) || did;\n var ok = await confirmDialog({ title: __t(\"members.unblockTitle\"), message: __t(\"members.unblockMsg\", { name: name }), confirmLabel: __t(\"members.unblock\") });\n if (!ok) return;\n var data = await api(\"PUT\", \"/members/\" + encodeURIComponent(did) + \"/approval\", { approved: true });\n if (data.ok) { showToast(__t(\"members.unblockSuccess\"), \"success\"); loadMembers(membersPage); }\n}\n\nasync function removeMember(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n var name = (u && u.fullName) || did;\n var ok = await confirmDialog({ title: __t(\"members.removeTitle\"), message: __t(\"members.removeMsg\", { name: name }), confirmLabel: __t(\"members.remove\"), danger: true });\n if (!ok) return;\n var data = await api(\"DELETE\", \"/members/\" + encodeURIComponent(did));\n if (data.ok) { showToast(__t(\"members.removeSuccess\"), \"success\"); loadMembers(membersPage); }\n}\n\nfunction updateTransferSection(users) {\n var section = document.getElementById(\"ownership-transfer-section\");\n if (__caller.role !== \"owner\") {\n section.classList.add(\"hidden\");\n return;\n }\n section.classList.remove(\"hidden\");\n var select = document.getElementById(\"transfer-target\");\n var html = '<option value=\"\">' + __t(\"members.transferSelect\") + '</option>';\n users.forEach(function(u) {\n if (u.did !== __caller.did && u.approved && u.role !== \"owner\") {\n html += '<option value=\"' + escapeHtml(u.did) + '\">' + escapeHtml(u.fullName || truncateDid(u.did)) + ' (' + u.role + ')</option>';\n }\n });\n select.innerHTML = html;\n}\n\nasync function handleTransferOwnership() {\n var target = document.getElementById(\"transfer-target\").value;\n if (!target) { showToast(__t(\"members.selectMemberFirst\"), \"error\"); return; }\n var targetUser = currentMembersList.find(function(u) { return u.did === target; });\n var name = targetUser ? (targetUser.fullName || truncateDid(target)) : truncateDid(target);\n var ok = await confirmDialog({\n title: __t(\"members.transferTitle\"),\n message: __t(\"members.transferMsg\", { name: name }),\n confirmLabel: __t(\"members.transferConfirm\"),\n danger: true,\n });\n if (!ok) return;\n var data = await api(\"POST\", \"/transfer-ownership\", { targetDid: target });\n if (data.ok) {\n showToast(__t(\"members.transferSuccess\"), \"success\");\n setTimeout(function() { location.reload(); }, 1000);\n }\n}\n\n// Close action menus on outside click\ndocument.addEventListener(\"click\", function() {\n document.querySelectorAll(\".action-menu.open\").forEach(function(m) { m.classList.remove(\"open\"); });\n});\n\nregisterTab(\"members\", function() {\n initInstanceFilter().then(function() {\n // Auto-select instance from URL query (e.g., ?instance=z1xxx)\n var urlInstance = new URLSearchParams(window.location.search).get(\"instance\");\n if (urlInstance) {\n var sel = document.getElementById(\"members-instance-filter\");\n if (sel) sel.value = urlInstance;\n }\n // Restore sort state from URL (sortBy / sortOrder) on tab enter.\n readMembersSortFromURL();\n loadMembers(1);\n });\n});\n";
6
+ export declare const MEMBERS_TAB_SCRIPT = "\nvar membersPage = 1;\nvar membersPageSize = 20;\nvar memberSearchTimeout = null;\nvar currentMembersList = [];\nvar selectedMember = null;\nvar __membersAggregated = false;\n// Sort state: empty string = default (server decides + self-first on client).\n// Three-state click cycle: (none) \u2192 desc \u2192 asc \u2192 (none).\n// Persisted in location.search as sortBy/sortOrder so it survives refresh + share.\nvar membersSortBy = \"\";\nvar membersSortOrder = \"\";\nvar MEMBERS_SORT_VALID = [\"name\", \"role\", \"status\", \"source\", \"joined\"];\n\n/** Populate module sort state from the current URL query string. Idempotent. */\nfunction readMembersSortFromURL() {\n membersSortBy = \"\";\n membersSortOrder = \"\";\n var params = new URLSearchParams(location.search);\n var by = params.get(\"sortBy\");\n var order = params.get(\"sortOrder\");\n if (by && MEMBERS_SORT_VALID.indexOf(by) >= 0) {\n membersSortBy = by;\n membersSortOrder = (order === \"asc\" || order === \"desc\") ? order : \"desc\";\n }\n}\n\n/** Write current sort state back to location.search without reloading. */\nfunction writeMembersSortToURL() {\n var params = new URLSearchParams(location.search);\n if (membersSortBy) {\n params.set(\"sortBy\", membersSortBy);\n params.set(\"sortOrder\", membersSortOrder || \"desc\");\n } else {\n params.delete(\"sortBy\");\n params.delete(\"sortOrder\");\n }\n var qs = params.toString();\n var newUrl = location.pathname + (qs ? \"?\" + qs : \"\") + location.hash;\n history.replaceState(null, \"\", newUrl);\n}\n\n/** Cycle sort state for a column key and reload. */\nfunction toggleSort(key) {\n if (membersSortBy !== key) {\n membersSortBy = key;\n membersSortOrder = \"desc\";\n } else if (membersSortOrder === \"desc\") {\n membersSortOrder = \"asc\";\n } else {\n // Third click \u2014 back to default.\n membersSortBy = \"\";\n membersSortOrder = \"\";\n }\n writeMembersSortToURL();\n loadMembers(1);\n}\n\n/** Render a <th> with click-to-sort behavior. */\nfunction sortableTh(key, label, extraClass) {\n var isActive = membersSortBy === key;\n var arrow;\n if (isActive) {\n arrow = membersSortOrder === \"asc\"\n ? '<span class=\"sort-arrow sort-arrow-active\">\\u2191</span>'\n : '<span class=\"sort-arrow sort-arrow-active\">\\u2193</span>';\n } else {\n arrow = '<span class=\"sort-arrow\">\\u2195</span>';\n }\n var cls = \"sortable\" + (isActive ? \" sort-active\" : \"\") + (extraClass ? \" \" + extraClass : \"\");\n return '<th class=\"' + cls + '\" onclick=\"toggleSort(\\'' + key + '\\')\">' + label + arrow + '</th>';\n}\n\nfunction providerLabel(sp) {\n if (!sp) return \"\\u2014\";\n var key = \"members.providers.\" + sp;\n var val = __t(key);\n return val !== key ? val : sp;\n}\n\nfunction debouncedMemberSearch() {\n clearTimeout(memberSearchTimeout);\n memberSearchTimeout = setTimeout(function() { loadMembers(1); }, 300);\n}\n\n// Load registered instances for the filter dropdown (system admin only)\nvar __instanceNames = {};\nvar __instanceFilterInitialized = false;\nasync function initInstanceFilter() {\n if (__instanceFilterInitialized) return;\n try {\n // Best-effort: this endpoint is system-admin-only. Instance-scoped owners/admins\n // get 403 here, which is expected \u2014 fail silently (no error toast).\n var data = await api(\"GET\", \"/../admin/instances\", undefined, { silent: true });\n if (data.ok && data.instances && data.instances.length > 0) {\n __instanceFilterInitialized = true;\n var sel = document.getElementById(\"members-instance-filter\");\n data.instances.forEach(function(inst) {\n __instanceNames[inst.instanceDid] = inst.appName || inst.instanceDid.substring(0, 16);\n var opt = document.createElement(\"option\");\n opt.value = inst.instanceDid;\n opt.textContent = __instanceNames[inst.instanceDid];\n sel.appendChild(opt);\n });\n sel.style.display = \"\";\n }\n } catch(e) { /* not admin or no instances */ }\n}\n\n// Also support specific instance filter via query param\n// When ?instance=z1xxx, team handler needs to use that instanceDid\n\nasync function loadMembers(page) {\n membersPage = page || 1;\n var instanceFilter = document.getElementById(\"members-instance-filter\").value;\n var qs = \"?page=\" + membersPage + \"&pageSize=\" + membersPageSize;\n // System view (\"\") \u2192 use defaultInstanceDid so we query membership, not global users\n if (instanceFilter === \"\" && __pageData.instanceDid) {\n qs += \"&instance=\" + encodeURIComponent(__pageData.instanceDid);\n } else if (instanceFilter) {\n qs += \"&instance=\" + encodeURIComponent(instanceFilter);\n }\n var search = document.getElementById(\"members-search\").value.trim();\n if (search) qs += \"&search=\" + encodeURIComponent(search);\n var role = document.getElementById(\"members-role-filter\").value;\n if (role) qs += \"&role=\" + encodeURIComponent(role);\n var status = document.getElementById(\"members-status-filter\").value;\n if (status !== \"\") qs += \"&approved=\" + status;\n var source = document.getElementById(\"members-source-filter\").value;\n if (source) qs += \"&sourceProvider=\" + encodeURIComponent(source);\n if (membersSortBy) {\n qs += \"&sortBy=\" + encodeURIComponent(membersSortBy);\n qs += \"&sortOrder=\" + encodeURIComponent(membersSortOrder || \"desc\");\n }\n\n document.getElementById(\"members-table-wrap\").innerHTML = skeletonTable(8, 6);\n document.getElementById(\"members-pagination\").innerHTML = \"\";\n var data = await api(\"GET\", \"/members\" + qs);\n if (!data.ok) {\n document.getElementById(\"members-table-wrap\").innerHTML = '<div class=\"empty-state\"><div class=\"empty-state-icon\"><svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 8v4m0 4h.01\"/></svg></div><div class=\"empty-state-title\">' + __t(\"members.loadFailed\") + '</div></div>';\n return;\n }\n __membersAggregated = !!data.aggregated;\n // Only pin the caller to the top when using the default (server) order.\n // Under explicit sort, honor the user's intent without re-shuffling.\n if (membersSortBy) {\n currentMembersList = data.users || [];\n } else {\n currentMembersList = (data.users || []).slice().sort(function(a, b) {\n if (a.did === __caller.did) return -1;\n if (b.did === __caller.did) return 1;\n return (a.createdAt || 0) - (b.createdAt || 0);\n });\n }\n renderMembersTable(currentMembersList, data.paging);\n updateTransferSection(data.users);\n}\n\nfunction renderMembersTable(users, paging) {\n var wrap = document.getElementById(\"members-table-wrap\");\n // Aggregated (cross-instance \"All Instances\") view can't tell which instance\n // a role change should apply to, so per-row actions are disabled there \u2014 see\n // canOperate below. Surface why, instead of silently hiding the buttons.\n var aggregatedHint = __membersAggregated ? '<p class=\"text-muted\" style=\"font-size:13px;margin-bottom:12px;\">' + __t(\"members.aggregatedHint\") + '</p>' : '';\n if (!users || users.length === 0) {\n wrap.innerHTML = aggregatedHint + '<div class=\"empty-state\"><div class=\"empty-state-icon\"><svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/></svg></div><div class=\"empty-state-title\">' + __t(\"members.noMembers\") + '</div><div class=\"empty-state-desc\">' + __t(\"members.noMembersHint\") + '</div></div>';\n document.getElementById(\"members-pagination\").innerHTML = \"\";\n return;\n }\n\n var instanceSel = document.getElementById(\"members-instance-filter\");\n var instanceVisible = instanceSel && instanceSel.style.display !== 'none';\n var showInstance = instanceVisible && instanceSel.value === \"all\";\n var html = aggregatedHint + '<table class=\"table\"><thead><tr>';\n html += sortableTh(\"name\", __t(\"members.col.member\"));\n html += sortableTh(\"role\", __t(\"members.col.role\"));\n if (showInstance) html += '<th>' + __t(\"members.col.instance\") + '</th>';\n html += sortableTh(\"source\", __t(\"members.col.source\"), \"col-hide-sm\");\n html += sortableTh(\"status\", __t(\"members.col.status\"), \"col-hide-sm\");\n html += sortableTh(\"joined\", __t(\"members.col.joined\"), \"col-hide-md\");\n // Registered/Last Login From columns removed \u2014 domain info not meaningful in Worker context\n html += '<th></th></tr></thead><tbody>';\n users.forEach(function(u) {\n var userDid = u.did || u.user_did || '';\n var av = renderAvatar(u.avatar, u.fullName, \"sm\");\n var name = u.fullName ? escapeHtml(u.fullName) : '<span class=\"text-muted\">(unnamed)</span>';\n var did = '<did-address did=\"' + escapeHtml(userDid) + '\" compact></did-address>';\n var roleBadge = '<span class=\"badge badge-' + (u.role || \"guest\") + '\">' + __t(\"common.\" + (u.role || \"guest\")) + '</span>';\n var statusBadge = u.approved !== undefined ? (u.approved ? '<span class=\"badge badge-active\">' + __t(\"common.active\") + '</span>' : '<span class=\"badge badge-blocked\">' + __t(\"common.blocked\") + '</span>') : '<span class=\"text-muted\">\u2014</span>';\n var joined = (u.createdAt || u.joined_at) ? '<span class=\"cell-date\">' + escapeHtml(relativeTime(u.createdAt || u.joined_at)) + '</span>' : '<span class=\"text-muted\">\u2014</span>';\n html += '<tr style=\"cursor:pointer\" onclick=\"openMemberDetail(\\'' + escapeHtml(userDid) + '\\')\">';\n html += '<td><div class=\"member-identity\">' + av + '<div><div class=\"member-name\">' + name + '</div><div class=\"member-email\">' + did + '</div></div></div></td>';\n html += '<td>' + roleBadge + '</td>';\n if (showInstance) {\n if (__membersAggregated && u.instance_count > 1) {\n html += '<td><a href=\"javascript:void(0)\" onclick=\"event.stopPropagation();openMemberDetail(\\'' + escapeHtml(userDid) + '\\')\" style=\"color:var(--blue);font-size:14px;text-decoration:none\">' + u.instance_count + ' ' + __t(\"members.instances\") + '</a></td>';\n } else {\n var instDid = (__membersAggregated && u.instances && u.instances.length === 1) ? u.instances[0].instance_did : (u.instance_did || '');\n var instName = __instanceNames[instDid] || '';\n if (!instName) instName = instDid ? __t(\"members.currentInstance\") : '\u2014';\n html += '<td><div>' + escapeHtml(instName);\n if (instDid) html += '<div style=\"font-size:11px;color:var(--text-secondary);margin-top:2px\" onclick=\"event.stopPropagation()\"><did-address did=\"' + escapeHtml(instDid) + '\" compact></did-address></div>';\n html += '</div></td>';\n }\n }\n var sourceBadge = '<span class=\"text-muted\">' + escapeHtml(providerLabel(u.sourceProvider)) + '</span>';\n html += '<td class=\"col-hide-sm\">' + sourceBadge + '</td>';\n html += '<td class=\"col-hide-sm\">' + statusBadge + '</td>';\n html += '<td class=\"col-hide-md\">' + joined + '</td>';\n // Actions: only for current instance members, not cross-instance\n var canOperate = !instanceVisible || (instanceVisible && instanceSel.value === '');\n // In aggregated mode, only allow actions on users whose instance matches current system\n if (__membersAggregated) canOperate = false;\n html += '<td class=\"action-cell\">' + (canOperate ? renderMemberActions(u) : '') + '</td>';\n html += '</tr>';\n });\n html += '</tbody></table>';\n wrap.innerHTML = html;\n\n renderPagination(\"members-pagination\", paging, loadMembers);\n}\n\nfunction renderMemberActions(u) {\n var isOwner = __caller.role === \"owner\";\n var isAdmin = __caller.role === \"admin\";\n var isSelf = u.did === __caller.did;\n var ROLE_LEVEL = { owner: 3, admin: 2, member: 1, guest: 0 };\n var callerLevel = ROLE_LEVEL[__caller.role] || 0;\n var targetLevel = ROLE_LEVEL[u.role] || 0;\n // Can only act on members with lower privilege\n var canManage = !isSelf && targetLevel < callerLevel;\n var items = [];\n\n // Change role: can manage lower-level members\n if (canManage) {\n items.push('<button class=\"action-menu-item\" onclick=\"event.stopPropagation();openChangeRoleForDid(\\'' + escapeHtml(u.did) + '\\')\">' + __t(\"members.changeRole\") + '</button>');\n }\n if (canManage && u.approved) {\n items.push('<button class=\"action-menu-item\" onclick=\"event.stopPropagation();blockMember(\\'' + escapeHtml(u.did) + '\\')\">' + __t(\"members.block\") + '</button>');\n }\n if (canManage && !u.approved) {\n items.push('<button class=\"action-menu-item\" onclick=\"event.stopPropagation();unblockMember(\\'' + escapeHtml(u.did) + '\\')\">' + __t(\"members.unblock\") + '</button>');\n }\n if (items.length > 0 && canManage) {\n items.push('<div class=\"action-menu-sep\"></div>');\n }\n if (canManage) {\n items.push('<button class=\"action-menu-item danger\" onclick=\"event.stopPropagation();removeMember(\\'' + escapeHtml(u.did) + '\\')\">' + __t(\"members.remove\") + '</button>');\n }\n\n if (items.length === 0) return \"\";\n return '<button class=\"action-trigger\" onclick=\"event.stopPropagation();toggleActionMenu(this)\">\u22EF</button><div class=\"action-menu\">' + items.join(\"\") + '</div>';\n}\n\nfunction toggleActionMenu(trigger) {\n var menu = trigger.nextElementSibling;\n var wasOpen = menu.classList.contains(\"open\");\n // Close all menus first\n document.querySelectorAll(\".action-menu.open\").forEach(function(m) { m.classList.remove(\"open\"); });\n if (!wasOpen) menu.classList.toggle(\"open\");\n}\n\nasync function openMemberDetail(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n if (!u) return;\n selectedMember = u;\n\n // Fetch full member info (with connectedAccounts) from API.\n // Falls back to in-memory list data if the request fails.\n var detail = u;\n try {\n var resp = await api(\"GET\", \"/members/\" + encodeURIComponent(did));\n if (resp && resp.ok && resp.user) {\n // Merge: in-memory list has fields the API may omit (instances, passkeyCount in some paths).\n detail = Object.assign({}, u, resp.user);\n selectedMember = detail;\n }\n } catch (e) {}\n\n var body = document.getElementById(\"member-detail-body\");\n var html = '<div class=\"profile-header\">' + renderAvatar(detail.avatar, detail.fullName) + '<div>';\n html += '<div style=\"font-weight:500;font-size:16px;color:var(--text-white)\">' + escapeHtml(detail.fullName || \"(unnamed)\") + '</div>';\n html += '<did-address did=\"' + escapeHtml(detail.did) + '\" compact></did-address>';\n html += '</div></div>';\n\n html += '<div class=\"settings-card\" style=\"margin-top:16px\">';\n html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailRole\") + '</span><span class=\"settings-value\"><span class=\"badge badge-' + (detail.role || \"guest\") + '\">' + __t(\"common.\" + (detail.role || \"guest\")) + '</span></span></div>';\n html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailStatus\") + '</span><span class=\"settings-value\">' + (detail.approved ? '<span class=\"badge badge-active\">' + __t(\"common.active\") + '</span>' : '<span class=\"badge badge-blocked\">' + __t(\"common.blocked\") + '</span>') + '</span></div>';\n html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailSource\") + '</span><span class=\"settings-value\">' + escapeHtml(providerLabel(detail.sourceProvider)) + '</span></div>';\n if (detail.email) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailEmail\") + '</span><span class=\"settings-value\">' + escapeHtml(detail.email) + '</span></div>';\n if (detail.inviterName) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailInvitedBy\") + '</span><span class=\"settings-value\">' + escapeHtml(detail.inviterName) + '</span></div>';\n if (detail.createdAt) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailJoined\") + '</span><span class=\"settings-value\">' + escapeHtml(absoluteTime(detail.createdAt)) + '</span></div>';\n if (detail.lastLoginAt) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailLastLogin\") + '</span><span class=\"settings-value\">' + escapeHtml(absoluteTime(detail.lastLoginAt)) + '</span></div>';\n if (detail.sourceDomain) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailRegisteredFrom\") + '</span><span class=\"settings-value\">' + escapeHtml(detail.sourceDomain) + '</span></div>';\n if (detail.lastLoginDomain) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailLastLoginFrom\") + '</span><span class=\"settings-value\">' + escapeHtml(detail.lastLoginDomain) + '</span></div>';\n if (detail.passkeyCount !== undefined) html += '<div class=\"settings-row\"><span class=\"settings-label\">' + __t(\"members.detailPasskeys\") + '</span><span class=\"settings-value\">' + detail.passkeyCount + '</span></div>';\n html += '</div>';\n\n // Connected Accounts (read-only)\n if (detail.connectedAccounts && detail.connectedAccounts.length > 0) {\n html += '<div class=\"settings-card\" style=\"margin-top:16px\">';\n html += '<div class=\"settings-card-header\"><span>' + __t(\"members.detailConnectedAccounts\") + ' (' + detail.connectedAccounts.length + ')</span></div>';\n html += '<div style=\"max-height:240px;overflow-y:auto\">';\n detail.connectedAccounts.forEach(function(a) {\n var label = providerLabel(a.provider);\n var info = '';\n if (a.userInfo && a.userInfo.name) info = a.userInfo.name;\n else if (a.userInfo && a.userInfo.email) info = a.userInfo.email;\n else if (a.id) info = a.id;\n html += '<div class=\"settings-row\" style=\"gap:12px;align-items:flex-start\">';\n html += '<div style=\"flex:1;min-width:0\">';\n html += '<div style=\"font-size:14px;font-weight:500;color:var(--text)\">' + escapeHtml(label);\n if (a.isMain) html += ' <span class=\"badge badge-active\" style=\"font-size:10px;padding:1px 6px;\">' + __t(\"profile.mainAccount\") + '</span>';\n html += '</div>';\n if (info) html += '<div style=\"font-size:12px;color:var(--text-secondary);margin-top:2px\">' + escapeHtml(info) + '</div>';\n if (a.did) html += '<div style=\"font-size:11px;color:var(--text-secondary);margin-top:2px\"><did-address did=\"' + escapeHtml(a.did) + '\" compact></did-address></div>';\n html += '</div>';\n if (a.lastLoginAt) html += '<div style=\"font-size:12px;color:var(--text-secondary);flex-shrink:0\">' + escapeHtml(relativeTime(a.lastLoginAt)) + '</div>';\n html += '</div>';\n });\n html += '</div></div>';\n }\n\n // Instance memberships (aggregated mode)\n if (detail.instances && detail.instances.length > 0) {\n html += '<div class=\"settings-card\" style=\"margin-top:16px\">';\n html += '<div class=\"settings-card-header\"><span>' + __t(\"members.instanceMemberships\") + ' (' + detail.instances.length + ')</span></div>';\n html += '<div style=\"max-height:240px;overflow-y:auto\">';\n detail.instances.forEach(function(inst) {\n var instName = __instanceNames[inst.instance_did] || '';\n var instDid = inst.instance_did || '';\n html += '<div class=\"settings-row\" style=\"gap:12px\">';\n html += '<div style=\"flex:1;min-width:0\"><div style=\"font-size:14px;font-weight:500;color:var(--text)\">' + escapeHtml(instName || __t(\"members.currentInstance\")) + '</div>';\n if (instDid) html += '<div style=\"font-size:11px;color:var(--text-secondary);margin-top:2px\"><did-address did=\"' + escapeHtml(instDid) + '\" compact></did-address></div>';\n html += '</div>';\n html += '<div style=\"display:flex;align-items:center;gap:8px;flex-shrink:0\"><span class=\"badge badge-' + inst.role + '\">' + __t(\"common.\" + inst.role) + '</span>';\n if (inst.joined_at) html += '<span style=\"font-size:12px;color:var(--text-secondary)\">' + escapeHtml(relativeTime(inst.joined_at)) + '</span>';\n html += '</div></div>';\n });\n html += '</div></div>';\n }\n\n body.innerHTML = html;\n\n // Change role: mirror list's canOperate + renderMemberActions logic exactly.\n // Use post-merge detail so role/did reflect the latest API response.\n var changeRoleBtn = document.getElementById(\"member-detail-change-role\");\n var instanceSel = document.getElementById(\"members-instance-filter\");\n var instanceVisible = instanceSel && instanceSel.style.display !== 'none';\n var canOperate = !instanceVisible || (instanceVisible && instanceSel.value === '');\n if (__membersAggregated) canOperate = false;\n var isSelf = detail.did === __caller.did;\n var ROLE_LEVEL = { owner: 3, admin: 2, member: 1, guest: 0 };\n var callerLevel = ROLE_LEVEL[__caller.role] || 0;\n var targetLevel = ROLE_LEVEL[detail.role] || 0;\n var canChangeRole = canOperate && !isSelf && targetLevel < callerLevel;\n if (canChangeRole) {\n changeRoleBtn.classList.remove(\"hidden\");\n } else {\n changeRoleBtn.classList.add(\"hidden\");\n }\n\n showDialog(\"member-detail-dialog\");\n}\n\nfunction openChangeRoleForDid(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n if (!u) return;\n selectedMember = u;\n openChangeRoleDialog();\n}\n\nfunction openChangeRoleDialog() {\n if (!selectedMember) return;\n document.getElementById(\"change-role-title\").textContent = __t(\"members.changeRole\");\n document.getElementById(\"change-role-subtitle\").textContent = __t(\"members.changeRoleFor\", { name: selectedMember.fullName || truncateDid(selectedMember.did) });\n // Show/hide role options based on caller's level (only owner can assign admin)\n var radios = document.querySelectorAll('input[name=\"new-role\"]');\n radios.forEach(function(r) {\n r.checked = r.value === selectedMember.role;\n var label = r.closest('label');\n if (label) {\n if (r.value === 'admin' && __caller.role !== 'owner') label.style.display = 'none';\n else label.style.display = '';\n }\n });\n closeDialog(\"member-detail-dialog\");\n showDialog(\"change-role-dialog\");\n}\n\nasync function saveRoleChange() {\n if (!selectedMember) return;\n var selected = document.querySelector('input[name=\"new-role\"]:checked');\n if (!selected) return;\n var newRole = selected.value;\n var data = await api(\"PUT\", \"/members/\" + encodeURIComponent(selectedMember.did) + \"/role\", { role: newRole });\n if (data.ok) {\n showToast(__t(\"members.roleUpdated\"), \"success\");\n closeDialog(\"change-role-dialog\");\n loadMembers(membersPage);\n }\n}\n\n// name is looked up (not passed via onclick) so a member's self-set fullName\n// never has to be embedded as a JS-string literal inside an inline onclick=\n// attribute \u2014 HTML entity-escaping alone can't fully neutralize that context,\n// since the browser decodes entities before parsing onclick as script.\nasync function blockMember(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n var name = (u && u.fullName) || did;\n var ok = await confirmDialog({ title: __t(\"members.blockTitle\"), message: __t(\"members.blockMsg\", { name: name }), confirmLabel: __t(\"members.blockBtn\"), danger: true });\n if (!ok) return;\n var data = await api(\"PUT\", \"/members/\" + encodeURIComponent(did) + \"/approval\", { approved: false });\n if (data.ok) { showToast(__t(\"members.blockSuccess\"), \"success\"); loadMembers(membersPage); }\n}\n\nasync function unblockMember(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n var name = (u && u.fullName) || did;\n var ok = await confirmDialog({ title: __t(\"members.unblockTitle\"), message: __t(\"members.unblockMsg\", { name: name }), confirmLabel: __t(\"members.unblock\") });\n if (!ok) return;\n var data = await api(\"PUT\", \"/members/\" + encodeURIComponent(did) + \"/approval\", { approved: true });\n if (data.ok) { showToast(__t(\"members.unblockSuccess\"), \"success\"); loadMembers(membersPage); }\n}\n\nasync function removeMember(did) {\n var u = currentMembersList.find(function(m) { return m.did === did; });\n var name = (u && u.fullName) || did;\n var ok = await confirmDialog({ title: __t(\"members.removeTitle\"), message: __t(\"members.removeMsg\", { name: name }), confirmLabel: __t(\"members.remove\"), danger: true });\n if (!ok) return;\n var data = await api(\"DELETE\", \"/members/\" + encodeURIComponent(did));\n if (data.ok) { showToast(__t(\"members.removeSuccess\"), \"success\"); loadMembers(membersPage); }\n}\n\nfunction updateTransferSection(users) {\n var section = document.getElementById(\"ownership-transfer-section\");\n if (__caller.role !== \"owner\") {\n section.classList.add(\"hidden\");\n return;\n }\n section.classList.remove(\"hidden\");\n var select = document.getElementById(\"transfer-target\");\n var html = '<option value=\"\">' + __t(\"members.transferSelect\") + '</option>';\n users.forEach(function(u) {\n if (u.did !== __caller.did && u.approved && u.role !== \"owner\") {\n html += '<option value=\"' + escapeHtml(u.did) + '\">' + escapeHtml(u.fullName || truncateDid(u.did)) + ' (' + u.role + ')</option>';\n }\n });\n select.innerHTML = html;\n}\n\nasync function handleTransferOwnership() {\n var target = document.getElementById(\"transfer-target\").value;\n if (!target) { showToast(__t(\"members.selectMemberFirst\"), \"error\"); return; }\n var targetUser = currentMembersList.find(function(u) { return u.did === target; });\n var name = targetUser ? (targetUser.fullName || truncateDid(target)) : truncateDid(target);\n var ok = await confirmDialog({\n title: __t(\"members.transferTitle\"),\n message: __t(\"members.transferMsg\", { name: name }),\n confirmLabel: __t(\"members.transferConfirm\"),\n danger: true,\n });\n if (!ok) return;\n var data = await api(\"POST\", \"/transfer-ownership\", { targetDid: target });\n if (data.ok) {\n showToast(__t(\"members.transferSuccess\"), \"success\");\n setTimeout(function() { location.reload(); }, 1000);\n }\n}\n\n// Button-driven mutations must not double-submit (submitOnce \u2014 utils.ts).\nsaveRoleChange = submitOnce(saveRoleChange);\nblockMember = submitOnce(blockMember);\nunblockMember = submitOnce(unblockMember);\nremoveMember = submitOnce(removeMember);\nhandleTransferOwnership = submitOnce(handleTransferOwnership);\n\n// Close action menus on outside click\ndocument.addEventListener(\"click\", function() {\n document.querySelectorAll(\".action-menu.open\").forEach(function(m) { m.classList.remove(\"open\"); });\n});\n\nregisterTab(\"members\", function() {\n initInstanceFilter().then(function() {\n // Auto-select instance from URL query (e.g., ?instance=z1xxx)\n var urlInstance = new URLSearchParams(window.location.search).get(\"instance\");\n if (urlInstance) {\n var sel = document.getElementById(\"members-instance-filter\");\n if (sel) sel.value = urlInstance;\n }\n // Restore sort state from URL (sortBy / sortOrder) on tab enter.\n readMembersSortFromURL();\n loadMembers(1);\n });\n});\n";
7
7
  export {};
8
8
  //# sourceMappingURL=tab-members.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tab-members.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-members.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC;AAEpE,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAiG7C;AAED,eAAO,MAAM,kBAAkB,mu1BAwe9B,CAAC"}
1
+ {"version":3,"file":"tab-members.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-members.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC;AAEpE,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAiG7C;AAED,eAAO,MAAM,kBAAkB,ki2BA+e9B,CAAC"}
@@ -569,6 +569,13 @@ async function handleTransferOwnership() {
569
569
  }
570
570
  }
571
571
 
572
+ // Button-driven mutations must not double-submit (submitOnce — utils.ts).
573
+ saveRoleChange = submitOnce(saveRoleChange);
574
+ blockMember = submitOnce(blockMember);
575
+ unblockMember = submitOnce(unblockMember);
576
+ removeMember = submitOnce(removeMember);
577
+ handleTransferOwnership = submitOnce(handleTransferOwnership);
578
+
572
579
  // Close action menus on outside click
573
580
  document.addEventListener("click", function() {
574
581
  document.querySelectorAll(".action-menu.open").forEach(function(m) { m.classList.remove("open"); });
@@ -1 +1 @@
1
- {"version":3,"file":"tab-members.js","sourceRoot":"","sources":["../../../src/pages/admin/tab-members.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,UAAU,cAAc,CAAC,CAAM;IACnC,OAAO,UAAU,CAAC;;8BAEU,CAAC,CAAC,eAAe,CAAC;4BACpB,CAAC,CAAC,cAAc,CAAC;;;;;+EAKkC,CAAC,CAAC,gBAAgB,CAAC;;;4BAGtE,CAAC,CAAC,sBAAsB,CAAC;kCACnB,CAAC,CAAC,yBAAyB,CAAC;;;yBAGrC,CAAC,CAAC,kBAAkB,CAAC;8BAChB,CAAC,CAAC,cAAc,CAAC;8BACjB,CAAC,CAAC,cAAc,CAAC;+BAChB,CAAC,CAAC,eAAe,CAAC;8BACnB,CAAC,CAAC,cAAc,CAAC;;;yBAGtB,CAAC,CAAC,mBAAmB,CAAC;0BACrB,CAAC,CAAC,eAAe,CAAC;0BAClB,CAAC,CAAC,gBAAgB,CAAC;;;yBAGpB,CAAC,CAAC,oBAAoB,CAAC;+BACjB,CAAC,CAAC,0BAA0B,CAAC;gCAC5B,CAAC,CAAC,2BAA2B,CAAC;8BAChC,CAAC,CAAC,yBAAyB,CAAC;+BAC3B,CAAC,CAAC,0BAA0B,CAAC;+BAC7B,CAAC,CAAC,0BAA0B,CAAC;8BAC9B,CAAC,CAAC,yBAAyB,CAAC;gCAC1B,CAAC,CAAC,2BAA2B,CAAC;iCAC7B,CAAC,CAAC,4BAA4B,CAAC;8BAClC,CAAC,CAAC,yBAAyB,CAAC;;;;;;;;;;;;8EAYoB,CAAC,CAAC,kBAAkB,CAAC;;;2EAGxB,CAAC,CAAC,sBAAsB,CAAC;;;sFAGd,CAAC,CAAC,qBAAqB,CAAC;;;;;;;;;;UAUpG,CAAC,CAAC,gBAAgB,CAAC;;;;sFAIyD,CAAC,CAAC,cAAc,CAAC;iGACN,CAAC,CAAC,oBAAoB,CAAC;;;;;;;iCAOvF,CAAC,CAAC,oBAAoB,CAAC;;;;qEAIa,CAAC,CAAC,mBAAmB,CAAC;;;sEAGrB,CAAC,CAAC,oBAAoB,CAAC;;;qEAGxB,CAAC,CAAC,mBAAmB,CAAC;;;;;oFAKP,CAAC,CAAC,eAAe,CAAC;qDACjD,CAAC,CAAC,oBAAoB,CAAC;;;CAG3E,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwe1C,CAAC"}
1
+ {"version":3,"file":"tab-members.js","sourceRoot":"","sources":["../../../src/pages/admin/tab-members.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,UAAU,cAAc,CAAC,CAAM;IACnC,OAAO,UAAU,CAAC;;8BAEU,CAAC,CAAC,eAAe,CAAC;4BACpB,CAAC,CAAC,cAAc,CAAC;;;;;+EAKkC,CAAC,CAAC,gBAAgB,CAAC;;;4BAGtE,CAAC,CAAC,sBAAsB,CAAC;kCACnB,CAAC,CAAC,yBAAyB,CAAC;;;yBAGrC,CAAC,CAAC,kBAAkB,CAAC;8BAChB,CAAC,CAAC,cAAc,CAAC;8BACjB,CAAC,CAAC,cAAc,CAAC;+BAChB,CAAC,CAAC,eAAe,CAAC;8BACnB,CAAC,CAAC,cAAc,CAAC;;;yBAGtB,CAAC,CAAC,mBAAmB,CAAC;0BACrB,CAAC,CAAC,eAAe,CAAC;0BAClB,CAAC,CAAC,gBAAgB,CAAC;;;yBAGpB,CAAC,CAAC,oBAAoB,CAAC;+BACjB,CAAC,CAAC,0BAA0B,CAAC;gCAC5B,CAAC,CAAC,2BAA2B,CAAC;8BAChC,CAAC,CAAC,yBAAyB,CAAC;+BAC3B,CAAC,CAAC,0BAA0B,CAAC;+BAC7B,CAAC,CAAC,0BAA0B,CAAC;8BAC9B,CAAC,CAAC,yBAAyB,CAAC;gCAC1B,CAAC,CAAC,2BAA2B,CAAC;iCAC7B,CAAC,CAAC,4BAA4B,CAAC;8BAClC,CAAC,CAAC,yBAAyB,CAAC;;;;;;;;;;;;8EAYoB,CAAC,CAAC,kBAAkB,CAAC;;;2EAGxB,CAAC,CAAC,sBAAsB,CAAC;;;sFAGd,CAAC,CAAC,qBAAqB,CAAC;;;;;;;;;;UAUpG,CAAC,CAAC,gBAAgB,CAAC;;;;sFAIyD,CAAC,CAAC,cAAc,CAAC;iGACN,CAAC,CAAC,oBAAoB,CAAC;;;;;;;iCAOvF,CAAC,CAAC,oBAAoB,CAAC;;;;qEAIa,CAAC,CAAC,mBAAmB,CAAC;;;sEAGrB,CAAC,CAAC,oBAAoB,CAAC;;;qEAGxB,CAAC,CAAC,mBAAmB,CAAC;;;;;oFAKP,CAAC,CAAC,eAAe,CAAC;qDACjD,CAAC,CAAC,oBAAoB,CAAC;;;CAG3E,CAAC;AACF,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+e1C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tab-profile-accounts.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-profile-accounts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC;AAqBpE,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAUxD;AAID,eAAO,MAAM,yBAAyB,QAohClC,CAAC"}
1
+ {"version":3,"file":"tab-profile-accounts.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-profile-accounts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAWH,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC;AAqBpE,wBAAgB,yBAAyB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAUxD;AAID,eAAO,MAAM,yBAAyB,QA8hClC,CAAC"}
@@ -1079,5 +1079,15 @@ function bufferToBase64url(buf) {
1079
1079
  for (var i = 0; i < bytes.length; i++) str += String.fromCharCode(bytes[i]);
1080
1080
  return btoa(str).replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=+$/, "");
1081
1081
  }
1082
+
1083
+ // Button-driven mutations must not double-submit (submitOnce — utils.ts).
1084
+ // OAuth/wallet bind flows are single-shot by construction (once-listeners,
1085
+ // polling stopped before the completing POST) and are not wrapped.
1086
+ unbindOAuth = submitOnce(unbindOAuth);
1087
+ unbindWallet = submitOnce(unbindWallet);
1088
+ addPasskey = submitOnce(addPasskey);
1089
+ removePasskey = submitOnce(removePasskey);
1090
+ openRenamePasskeyDialog = submitOnce(openRenamePasskeyDialog);
1091
+ startAccountMerge = submitOnce(startAccountMerge);
1082
1092
  `)();
1083
1093
  //# sourceMappingURL=tab-profile-accounts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tab-profile-accounts.js","sourceRoot":"","sources":["../../../src/pages/admin/tab-profile-accounts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,eAAe,EACf,UAAU,EACV,YAAY,EACZ,WAAW,GACZ,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAC9F,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAIvE,uDAAuD;AACvD,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC;AAE/D,4DAA4D;AAC5D,SAAS,gBAAgB;IACvB,MAAM,GAAG,GAA2B;QAClC,OAAO,EAAE,YAAY;QACrB,MAAM,EAAE,eAAe;QACvB,aAAa,EAAE,eAAe;QAC9B,KAAK,EAAE,UAAU;KAClB,CAAC;IACF,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,CAAM;IAC9C,OAAO,UAAU,CAAC;;;0CAGsB,CAAC,CAAC,2BAA2B,CAAC;;;;;CAKvE,CAAC;AACF,CAAC;AAED,mFAAmF;AACnF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,yBAAyB,GAAG,eAAe,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC;;;;;wBAKjD,gBAAgB,EAAE;0BAChB,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC;EAC7D,uBAAuB,CAAC,QAAQ,EAAE;EAClC,0BAA0B,CAAC,QAAQ,EAAE;EACrC,2BAA2B,CAAC,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2gCvC,CAAC,EAAE,CAAC"}
1
+ {"version":3,"file":"tab-profile-accounts.js","sourceRoot":"","sources":["../../../src/pages/admin/tab-profile-accounts.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,eAAe,EACf,UAAU,EACV,YAAY,EACZ,WAAW,GACZ,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAC9F,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAIvE,uDAAuD;AACvD,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC;AAE/D,4DAA4D;AAC5D,SAAS,gBAAgB;IACvB,MAAM,GAAG,GAA2B;QAClC,OAAO,EAAE,YAAY;QACrB,MAAM,EAAE,eAAe;QACvB,aAAa,EAAE,eAAe;QAC9B,KAAK,EAAE,UAAU;KAClB,CAAC;IACF,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;QACjB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,yBAAyB,CAAC,CAAM;IAC9C,OAAO,UAAU,CAAC;;;0CAGsB,CAAC,CAAC,2BAA2B,CAAC;;;;;CAKvE,CAAC;AACF,CAAC;AAED,mFAAmF;AACnF,+EAA+E;AAC/E,MAAM,CAAC,MAAM,yBAAyB,GAAG,eAAe,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC;;;;;wBAKjD,gBAAgB,EAAE;0BAChB,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC;EAC7D,uBAAuB,CAAC,QAAQ,EAAE;EAClC,0BAA0B,CAAC,QAAQ,EAAE;EACrC,2BAA2B,CAAC,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqhCvC,CAAC,EAAE,CAAC"}