@arcblock/did-connect-service 4.1.11 → 4.1.13

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 (121) hide show
  1. package/dist/_generated/asset-bytes.js +1 -1
  2. package/dist/_generated/asset-manifest.js +3 -3
  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/access/change-role.d.ts +48 -0
  8. package/dist/access/change-role.d.ts.map +1 -0
  9. package/dist/access/change-role.js +52 -0
  10. package/dist/access/change-role.js.map +1 -0
  11. package/dist/assets/admin-core.a63bbc69.js +1464 -0
  12. package/dist/assets/admin-extra.143b5774.js +2543 -0
  13. package/dist/assets/admin-extra.5b309e5e.js +2547 -0
  14. package/dist/assets/login.4404e695.js +720 -0
  15. package/dist/assets/login.76c483c2.js +720 -0
  16. package/dist/assets/login.bb9f8b03.js +720 -0
  17. package/dist/assets/login.fa25f37a.js +720 -0
  18. package/dist/handler-errors.d.ts +118 -0
  19. package/dist/handler-errors.d.ts.map +1 -0
  20. package/dist/handler-errors.js +38 -0
  21. package/dist/handler-errors.js.map +1 -0
  22. package/dist/handlers/access-key-connect-handler.d.ts.map +1 -1
  23. package/dist/handlers/access-key-connect-handler.js +2 -1
  24. package/dist/handlers/access-key-connect-handler.js.map +1 -1
  25. package/dist/handlers/admin-instance-handler.d.ts.map +1 -1
  26. package/dist/handlers/admin-instance-handler.js +2 -1
  27. package/dist/handlers/admin-instance-handler.js.map +1 -1
  28. package/dist/handlers/auth-handler.d.ts.map +1 -1
  29. package/dist/handlers/auth-handler.js +32 -9
  30. package/dist/handlers/auth-handler.js.map +1 -1
  31. package/dist/handlers/branding-handler.d.ts.map +1 -1
  32. package/dist/handlers/branding-handler.js +12 -9
  33. package/dist/handlers/branding-handler.js.map +1 -1
  34. package/dist/handlers/did-connect-handler.d.ts.map +1 -1
  35. package/dist/handlers/did-connect-handler.js +6 -5
  36. package/dist/handlers/did-connect-handler.js.map +1 -1
  37. package/dist/handlers/email-login-handler.d.ts +1 -1
  38. package/dist/handlers/email-login-handler.d.ts.map +1 -1
  39. package/dist/handlers/email-login-handler.js +63 -12
  40. package/dist/handlers/email-login-handler.js.map +1 -1
  41. package/dist/handlers/federation-admin-handler.d.ts.map +1 -1
  42. package/dist/handlers/federation-admin-handler.js +48 -45
  43. package/dist/handlers/federation-admin-handler.js.map +1 -1
  44. package/dist/handlers/oauth-handler.d.ts.map +1 -1
  45. package/dist/handlers/oauth-handler.js +23 -12
  46. package/dist/handlers/oauth-handler.js.map +1 -1
  47. package/dist/handlers/passkey-handler.d.ts.map +1 -1
  48. package/dist/handlers/passkey-handler.js +5 -4
  49. package/dist/handlers/passkey-handler.js.map +1 -1
  50. package/dist/handlers/team-handler.d.ts.map +1 -1
  51. package/dist/handlers/team-handler.js +19 -19
  52. package/dist/handlers/team-handler.js.map +1 -1
  53. package/dist/http/security-headers.d.ts +38 -0
  54. package/dist/http/security-headers.d.ts.map +1 -0
  55. package/dist/http/security-headers.js +67 -0
  56. package/dist/http/security-headers.js.map +1 -0
  57. package/dist/identity/refresh-tokens.d.ts +21 -0
  58. package/dist/identity/refresh-tokens.d.ts.map +1 -0
  59. package/dist/identity/refresh-tokens.js +45 -0
  60. package/dist/identity/refresh-tokens.js.map +1 -0
  61. package/dist/identity/session-context.d.ts +12 -0
  62. package/dist/identity/session-context.d.ts.map +1 -1
  63. package/dist/identity/session-context.js +12 -0
  64. package/dist/identity/session-context.js.map +1 -1
  65. package/dist/identity/session-tokens.d.ts +15 -0
  66. package/dist/identity/session-tokens.d.ts.map +1 -0
  67. package/dist/identity/session-tokens.js +15 -0
  68. package/dist/identity/session-tokens.js.map +1 -0
  69. package/dist/locale.d.ts +1 -1
  70. package/dist/locale.d.ts.map +1 -1
  71. package/dist/og/ssrf-guard.d.ts.map +1 -1
  72. package/dist/og/ssrf-guard.js +21 -3
  73. package/dist/og/ssrf-guard.js.map +1 -1
  74. package/dist/og/templates.d.ts.map +1 -1
  75. package/dist/og/templates.js +33 -4
  76. package/dist/og/templates.js.map +1 -1
  77. package/dist/pages/admin/i18n.d.ts.map +1 -1
  78. package/dist/pages/admin/i18n.js +33 -9
  79. package/dist/pages/admin/i18n.js.map +1 -1
  80. package/dist/pages/admin/scripts/api-client.d.ts +1 -1
  81. package/dist/pages/admin/scripts/api-client.d.ts.map +1 -1
  82. package/dist/pages/admin/scripts/api-client.js +4 -4
  83. package/dist/pages/admin/scripts/utils.d.ts +1 -1
  84. package/dist/pages/admin/scripts/utils.d.ts.map +1 -1
  85. package/dist/pages/admin/scripts/utils.js +7 -8
  86. package/dist/pages/admin/scripts/utils.js.map +1 -1
  87. package/dist/pages/admin/tab-access-keys.d.ts +1 -1
  88. package/dist/pages/admin/tab-access-keys.d.ts.map +1 -1
  89. package/dist/pages/admin/tab-access-keys.js +4 -4
  90. package/dist/pages/admin/tab-invitations.d.ts +1 -1
  91. package/dist/pages/admin/tab-invitations.d.ts.map +1 -1
  92. package/dist/pages/admin/tab-invitations.js +2 -2
  93. package/dist/pages/admin/tab-members.d.ts +1 -1
  94. package/dist/pages/admin/tab-members.d.ts.map +1 -1
  95. package/dist/pages/admin/tab-members.js +7 -3
  96. package/dist/pages/admin/tab-members.js.map +1 -1
  97. package/dist/pages/admin/tab-profile-accounts.js +1 -1
  98. package/dist/pages/admin-instances-page.d.ts +1 -1
  99. package/dist/pages/admin-instances-page.d.ts.map +1 -1
  100. package/dist/pages/admin-instances-page.js +195 -65
  101. package/dist/pages/admin-instances-page.js.map +1 -1
  102. package/dist/pages/gen-access-key-page.d.ts +1 -1
  103. package/dist/pages/gen-access-key-page.d.ts.map +1 -1
  104. package/dist/pages/gen-access-key-page.js +163 -35
  105. package/dist/pages/gen-access-key-page.js.map +1 -1
  106. package/dist/pages/homepage.d.ts.map +1 -1
  107. package/dist/pages/homepage.js +0 -4
  108. package/dist/pages/homepage.js.map +1 -1
  109. package/dist/pages/invite-page.d.ts +1 -1
  110. package/dist/pages/invite-page.d.ts.map +1 -1
  111. package/dist/pages/invite-page.js +91 -21
  112. package/dist/pages/invite-page.js.map +1 -1
  113. package/dist/pages/oauth-callback-page.d.ts.map +1 -1
  114. package/dist/pages/oauth-callback-page.js +7 -3
  115. package/dist/pages/oauth-callback-page.js.map +1 -1
  116. package/dist/store/d1-store.d.ts +55 -1
  117. package/dist/store/d1-store.d.ts.map +1 -1
  118. package/dist/store/d1-store.js +162 -9
  119. package/dist/store/d1-store.js.map +1 -1
  120. package/migrations/0011_refresh_tokens.sql +23 -0
  121. package/package.json +5 -5
@@ -3,6 +3,6 @@
3
3
  */
4
4
  type TFn = (key: string, params?: Record<string, string>) => string;
5
5
  export declare function accessKeysTabHtml(t: TFn): string;
6
- export declare const ACCESS_KEYS_TAB_SCRIPT = "\nvar akPage = 1;\nvar akPageSize = 20;\nvar akSearchTimer = null;\n\nfunction debounceAkSearch() {\n clearTimeout(akSearchTimer);\n akSearchTimer = setTimeout(function() { loadAccessKeys(1); }, 300);\n}\n\nasync function loadAccessKeys(page) {\n akPage = page || 1;\n var search = document.getElementById(\"ak-search\").value.trim();\n var qs = \"?page=\" + akPage + \"&pageSize=\" + akPageSize;\n if (search) qs += \"&search=\" + encodeURIComponent(search);\n document.getElementById(\"ak-table-wrap\").innerHTML = skeletonTable(7, 5);\n document.getElementById(\"ak-pagination\").innerHTML = \"\";\n var data = await apiRaw(\"GET\", \"/.well-known/service/api/access-keys\" + qs);\n if (!data || data.error) {\n document.getElementById(\"ak-table-wrap\").innerHTML = '<div class=\"empty-state\"><div class=\"empty-state-title\">' + __t(\"accessKeys.loadFailed\") + '</div></div>';\n return;\n }\n renderAccessKeys(data.keys, { total: data.total, page: data.page, pageSize: data.pageSize });\n}\n\nfunction renderAccessKeys(keys, paging) {\n var wrap = document.getElementById(\"ak-table-wrap\");\n if (!keys || keys.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=\"M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4\"/></svg></div><div class=\"empty-state-title\">' + __t(\"accessKeys.noKeys\") + '</div><div class=\"empty-state-desc\">' + __t(\"accessKeys.noKeysHint\") + '</div></div>';\n document.getElementById(\"ak-pagination\").innerHTML = \"\";\n return;\n }\n\n var html = '<table class=\"table\"><thead><tr><th>' + __t(\"accessKeys.col.keyId\") + '</th><th>' + __t(\"accessKeys.col.role\") + '</th><th class=\"col-hide-sm\">' + __t(\"accessKeys.col.remark\") + '</th><th class=\"col-hide-md\">' + __t(\"accessKeys.col.createdBy\") + '</th><th>' + __t(\"accessKeys.col.expires\") + '</th><th class=\"col-hide-md\">' + __t(\"accessKeys.col.lastUsed\") + '</th><th>' + __t(\"common.actions\") + '</th></tr></thead><tbody>';\n keys.forEach(function(k) {\n var keyIdHtml = '<did-address did=\"' + escapeHtml(k.accessKeyId) + '\" compact></did-address>';\n var roleBadge = '<span class=\"badge badge-' + k.role + '\">' + __t(\"common.\" + k.role) + '</span>';\n var remark = k.remark ? escapeHtml(k.remark.length > 40 ? k.remark.slice(0, 40) + \"...\" : k.remark) : '<span style=\"color:var(--text-secondary)\">\u2014</span>';\n var creator = k.createdByName\n ? escapeHtml(k.createdByName)\n : '<did-address did=\"' + escapeHtml(k.createdBy) + '\" compact></did-address>';\n var expiry = k.expireAt ? (new Date(k.expireAt) < new Date() ? '<span style=\"color:var(--error)\">' + __t(\"common.expired\") + '</span>' : relativeTime(k.expireAt)) : __t(\"common.never\");\n var lastUsed = k.lastUsedAt ? relativeTime(k.lastUsedAt) : __t(\"common.never\");\n var deleteLabel = k.remark ? escapeHtml(k.remark) : truncateDid(k.accessKeyId);\n\n html += '<tr>';\n html += '<td><code style=\"font-size:12px\">' + keyIdHtml + '</code></td>';\n html += '<td>' + roleBadge + '</td>';\n html += '<td class=\"col-hide-sm\">' + remark + '</td>';\n html += '<td class=\"col-hide-md\">' + creator + '</td>';\n html += '<td>' + expiry + '</td>';\n html += '<td class=\"col-hide-md\">' + lastUsed + '</td>';\n html += '<td class=\"action-cell\"><button class=\"action-trigger\" onclick=\"event.stopPropagation();toggleActionMenu(this)\">\u22EF</button><div class=\"action-menu\"><button class=\"action-menu-item\" onclick=\"event.stopPropagation();openEditAkDialog(\\''+k.accessKeyId+'\\')\">' + __t(\"common.edit\") + '</button><div class=\"action-menu-sep\"></div><button class=\"action-menu-item danger\" onclick=\"event.stopPropagation();confirmDeleteAk(\\''+k.accessKeyId+'\\',\\''+escapeHtml(deleteLabel)+'\\')\">' + __t(\"common.delete\") + '</button></div></td>';\n html += '</tr>';\n });\n html += '</tbody></table>';\n wrap.innerHTML = html;\n\n renderPagination(\"ak-pagination\", paging, loadAccessKeys);\n}\n\nfunction openCreateAkDialog() {\n var callerRole = __caller.role;\n var roleOptions = '<option value=\"guest\">Guest</option>';\n if (callerRole === \"member\" || callerRole === \"admin\" || callerRole === \"owner\") roleOptions += '<option value=\"member\"' + (callerRole === \"member\" ? ' selected' : '') + '>Member</option>';\n if (callerRole === \"admin\" || callerRole === \"owner\") roleOptions += '<option value=\"admin\"' + (callerRole === \"admin\" ? ' selected' : '') + '>Admin</option>';\n if (callerRole === \"owner\") roleOptions += '<option value=\"owner\">Owner</option>';\n\n var expiryOptions = '<option value=\"\">' + __t(\"accessKeys.expirations.never\") + '</option><option value=\"7d\">' + __t(\"accessKeys.expirations.days7\") + '</option><option value=\"30d\">' + __t(\"accessKeys.expirations.days30\") + '</option><option value=\"90d\">' + __t(\"accessKeys.expirations.days90\") + '</option><option value=\"custom\">' + __t(\"accessKeys.expirations.custom\") + '</option>';\n\n var html = '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.role\") + '</label><select class=\"select\" id=\"ak-create-role\">' + roleOptions + '</select></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.remark\") + '</label><input type=\"text\" class=\"input\" id=\"ak-create-remark\" placeholder=\"' + __t(\"accessKeys.remarkPlaceholder\") + '\" maxlength=\"200\" /></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.expiration\") + '</label><select class=\"select\" id=\"ak-create-expiry\" onchange=\"toggleAkCustomExpiry()\">' + expiryOptions + '</select></div>';\n html += '<div class=\"form-group hidden\" id=\"ak-custom-expiry-wrap\"><label class=\"form-label\">' + __t(\"accessKeys.customDate\") + '</label><input type=\"datetime-local\" class=\"input\" id=\"ak-create-custom-expiry\" /></div>';\n\n openDialog(__t(\"accessKeys.createTitle\"), html, __t(\"common.save\"), async function() {\n var role = document.getElementById(\"ak-create-role\").value;\n var remark = document.getElementById(\"ak-create-remark\").value;\n var expirySelect = document.getElementById(\"ak-create-expiry\").value;\n var expireAt = null;\n\n if (expirySelect === \"custom\") {\n var customDate = document.getElementById(\"ak-create-custom-expiry\").value;\n if (!customDate) { showToast(__t(\"accessKeys.selectExpDate\"), \"error\"); return; }\n expireAt = new Date(customDate).toISOString();\n } else if (expirySelect) {\n var days = parseInt(expirySelect);\n expireAt = new Date(Date.now() + days * 86400000).toISOString();\n }\n\n var result = await apiRaw(\"POST\", \"/.well-known/service/api/access-keys\", { role: role, remark: remark, expireAt: expireAt });\n if (!result || result.error) {\n showToast(result ? result.error : __t(\"accessKeys.createFailed\"), \"error\");\n return;\n }\n\n closeDialog();\n showSecretDialog(result.accessKeySecret);\n loadAccessKeys(1);\n });\n}\n\nfunction toggleAkCustomExpiry() {\n var sel = document.getElementById(\"ak-create-expiry\").value;\n var wrap = document.getElementById(\"ak-custom-expiry-wrap\");\n if (sel === \"custom\") wrap.classList.remove(\"hidden\");\n else wrap.classList.add(\"hidden\");\n}\n\n// Slugify a display name into something safe to use as an MCP server name\n// (lowercase, alnum + hyphens). Falls back to a fixed default when empty.\nfunction akSlug(s) {\n var slug = String(s || \"\").toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n return slug || \"did-connect\";\n}\n\nvar __akSnippets = null;\n\nfunction showSecretDialog(secret) {\n var html = '<div style=\"margin-bottom:12px;color:var(--error);font-weight:500\">' + __t(\"accessKeys.secretWarning\") + '</div>';\n var copyIconSvg = '<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\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"/><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"/></svg>';\n var copyBtnStyle = 'position:absolute;top:4px;right:4px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:var(--bg-base);border:1px solid var(--border);border-radius:4px;cursor:pointer;color:var(--text-secondary);transition:color 0.15s,border-color 0.15s';\n html += '<div style=\"position:relative\"><code id=\"ak-secret-display\" style=\"display:block;padding:12px 40px 12px 12px;background:var(--bg-base);border:1px solid var(--border);border-radius:6px;font-size:13px;word-break:break-all;user-select:all;overflow-wrap:anywhere\">' + escapeHtml(secret) + '</code>';\n html += '<button id=\"ak-copy-btn\" style=\"' + copyBtnStyle + '\" onclick=\"copyAkSecret()\" title=\"' + __t(\"common.copy\") + '\">' + copyIconSvg + '</button></div>';\n\n var mcpUrl = location.origin + \"/mcp\";\n var serverName = akSlug(__pageData && __pageData.appName);\n\n __akSnippets = {\n curl: {\n label: __t(\"accessKeys.tabCurl\"),\n hint: \"\",\n code: 'curl -H \"Authorization: Bearer ' + secret + '\" ' + location.origin + '/.well-known/service/api/did/session',\n },\n claude: {\n label: __t(\"accessKeys.tabClaude\"),\n hint: \"\",\n code: 'claude mcp add --transport http ' + serverName + ' ' + mcpUrl + ' --header \"Authorization: Bearer ' + secret + '\"',\n },\n codex: {\n label: __t(\"accessKeys.tabCodex\"),\n hint: __t(\"accessKeys.addToConfig\", { file: \"~/.codex/config.toml\" }),\n code: '[mcp_servers.' + serverName + ']\\nurl = \"' + mcpUrl + '\"\\nhttp_headers = { \"Authorization\" = \"Bearer ' + secret + '\" }',\n },\n cursor: {\n label: __t(\"accessKeys.tabCursor\"),\n hint: __t(\"accessKeys.addToConfig\", { file: \"mcp.json\" }),\n code: '{\\n \"mcpServers\": {\\n \"' + serverName + '\": {\\n \"url\": \"' + mcpUrl + '\",\\n \"headers\": {\\n \"Authorization\": \"Bearer ' + secret + '\"\\n }\\n }\\n }\\n}',\n },\n };\n\n html += '<div style=\"margin-top:16px;font-size:13px;color:var(--text-secondary)\"><strong>' + __t(\"accessKeys.usage\") + '</strong></div>';\n html += '<div style=\"display:flex;gap:4px;margin-top:8px;border-bottom:1px solid var(--border)\">';\n [\"curl\", \"claude\", \"codex\", \"cursor\"].forEach(function(key) {\n var isActive = key === \"curl\";\n html += '<button type=\"button\" class=\"ak-tab-btn\" data-ak-tab=\"' + key + '\" onclick=\"switchAkTab(\\'' + key + '\\')\" style=\"padding:6px 12px;font-size:12px;font-weight:500;border:none;background:none;cursor:pointer;color:' + (isActive ? \"var(--text-white)\" : \"var(--text-tertiary)\") + ';border-bottom:2px solid ' + (isActive ? \"var(--text-white)\" : \"transparent\") + '\">' + __akSnippets[key].label + '</button>';\n });\n html += '</div>';\n html += '<div id=\"ak-snippet-hint\" style=\"margin-top:8px;font-size:12px;color:var(--text-secondary);display:none\"></div>';\n html += '<div style=\"position:relative;margin-top:6px\"><pre id=\"ak-snippet-display\" style=\"margin:0;display:block;padding:12px 40px 12px 12px;background:var(--bg-base);border:1px solid var(--border);border-radius:6px;font-size:12px;white-space:pre-wrap;word-break:break-all;overflow-wrap:anywhere;user-select:all;font-family:inherit\"></pre>';\n html += '<button id=\"ak-snippet-copy-btn\" style=\"' + copyBtnStyle + '\" onclick=\"copyAkSnippet()\" title=\"' + __t(\"common.copy\") + '\">' + copyIconSvg + '</button></div>';\n\n openDialog(__t(\"accessKeys.secretTitle\"), html, __t(\"accessKeys.done\"), function() {\n document.getElementById(\"ak-secret-display\").textContent = \"\";\n __akSnippets = null;\n closeDialog();\n });\n\n switchAkTab(\"curl\");\n}\n\nfunction switchAkTab(key) {\n var snippet = __akSnippets && __akSnippets[key];\n if (!snippet) return;\n\n document.querySelectorAll(\".ak-tab-btn\").forEach(function(btn) {\n var isActive = btn.getAttribute(\"data-ak-tab\") === key;\n btn.style.color = isActive ? \"var(--text-white)\" : \"var(--text-tertiary)\";\n btn.style.borderBottomColor = isActive ? \"var(--text-white)\" : \"transparent\";\n });\n\n var hintEl = document.getElementById(\"ak-snippet-hint\");\n if (hintEl) {\n hintEl.textContent = snippet.hint || \"\";\n hintEl.style.display = snippet.hint ? \"block\" : \"none\";\n }\n var displayEl = document.getElementById(\"ak-snippet-display\");\n if (displayEl) displayEl.textContent = snippet.code;\n}\n\nfunction copyAkSnippet() {\n var text = document.getElementById(\"ak-snippet-display\").textContent;\n var btn = document.getElementById(\"ak-snippet-copy-btn\");\n var origHtml = btn.innerHTML;\n function onCopied() {\n btn.innerHTML = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 13l4 4L19 7\"/></svg>';\n btn.style.color = \"var(--green)\";\n setTimeout(function() { btn.innerHTML = origHtml; btn.style.color = \"\"; }, 2000);\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(onCopied).catch(function() {\n fallbackCopy(text);\n onCopied();\n });\n } else {\n fallbackCopy(text);\n onCopied();\n }\n}\n\nfunction copyAkSecret() {\n var text = document.getElementById(\"ak-secret-display\").textContent;\n var btn = document.getElementById(\"ak-copy-btn\");\n var origHtml = btn.innerHTML;\n function onCopied() {\n btn.innerHTML = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 13l4 4L19 7\"/></svg>';\n btn.style.color = \"var(--green)\";\n setTimeout(function() { btn.innerHTML = origHtml; btn.style.color = \"\"; }, 2000);\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(onCopied).catch(function() {\n fallbackCopy(text);\n onCopied();\n });\n } else {\n fallbackCopy(text);\n onCopied();\n }\n}\n\nasync function openEditAkDialog(accessKeyId) {\n var data = await apiRaw(\"GET\", \"/.well-known/service/api/access-keys/\" + encodeURIComponent(accessKeyId));\n if (!data || data.error) { showToast(__t(\"accessKeys.loadKeyFailed\"), \"error\"); return; }\n\n var html = '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.col.keyId\") + '</label><code style=\"font-size:12px\">' + escapeHtml(data.accessKeyId) + '</code></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.role\") + '</label><span class=\"badge badge-' + data.role + '\">' + __t(\"common.\" + data.role) + '</span></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.remark\") + '</label><input type=\"text\" class=\"input\" id=\"ak-edit-remark\" value=\"' + escapeHtml(data.remark || \"\") + '\" maxlength=\"200\" /></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.expiration\") + '</label><input type=\"datetime-local\" class=\"input\" id=\"ak-edit-expiry\" value=\"' + (data.expireAt ? data.expireAt.slice(0,16) : \"\") + '\" /> <label style=\"margin-top:4px;display:block;font-size:13px\"><input type=\"checkbox\" id=\"ak-edit-no-expiry\"' + (data.expireAt ? \"\" : \" checked\") + ' onchange=\"toggleAkEditExpiry()\" /> ' + __t(\"accessKeys.neverExpires\") + '</label></div>';\n\n openDialog(__t(\"accessKeys.editTitle\"), html, __t(\"common.save\"), async function() {\n var remark = document.getElementById(\"ak-edit-remark\").value;\n var noExpiry = document.getElementById(\"ak-edit-no-expiry\").checked;\n var expireAt = noExpiry ? null : (document.getElementById(\"ak-edit-expiry\").value ? new Date(document.getElementById(\"ak-edit-expiry\").value).toISOString() : undefined);\n\n var body = { remark: remark };\n if (expireAt !== undefined) body.expireAt = expireAt;\n\n var result = await apiRaw(\"PUT\", \"/.well-known/service/api/access-keys/\" + encodeURIComponent(accessKeyId), body);\n if (!result || result.error) {\n showToast(result ? result.error : __t(\"accessKeys.updateFailed\"), \"error\");\n return;\n }\n closeDialog();\n showToast(__t(\"accessKeys.keyUpdated\"), \"success\");\n loadAccessKeys(akPage);\n });\n}\n\nfunction toggleAkEditExpiry() {\n var noExpiry = document.getElementById(\"ak-edit-no-expiry\").checked;\n document.getElementById(\"ak-edit-expiry\").disabled = noExpiry;\n}\n\nfunction confirmDeleteAk(accessKeyId, label) {\n openDialog(__t(\"accessKeys.deleteTitle\"), '<p>' + __t(\"accessKeys.deleteMsg\", { label: escapeHtml(label) }) + '</p><p style=\"color:var(--error)\">' + __t(\"accessKeys.deleteWarning\") + '</p>', __t(\"common.delete\"), async function() {\n var result = await apiRaw(\"DELETE\", \"/.well-known/service/api/access-keys/\" + encodeURIComponent(accessKeyId));\n if (result && result.error) {\n showToast(result.error, \"error\");\n return;\n }\n closeDialog();\n showToast(__t(\"accessKeys.keyDeleted\"), \"success\");\n loadAccessKeys(akPage);\n });\n}\n\n// Direct API call (bypasses team handler's /team prefix)\nasync function apiRaw(method, path, body) {\n try {\n var opts = { method: method, headers: { \"Accept\": \"application/json\" } };\n if (body !== undefined) {\n opts.headers[\"Content-Type\"] = \"application/json\";\n opts.body = JSON.stringify(body);\n }\n var res = await fetch(path, opts);\n if (res.status === 204) return {};\n return await res.json();\n } catch(e) {\n return null;\n }\n}\n\nregisterTab(\"access-keys\", function() { loadAccessKeys(1); });\n";
6
+ export declare const ACCESS_KEYS_TAB_SCRIPT = "\nvar akPage = 1;\nvar akPageSize = 20;\nvar akSearchTimer = null;\n\nfunction debounceAkSearch() {\n clearTimeout(akSearchTimer);\n akSearchTimer = setTimeout(function() { loadAccessKeys(1); }, 300);\n}\n\nasync function loadAccessKeys(page) {\n akPage = page || 1;\n var search = document.getElementById(\"ak-search\").value.trim();\n var qs = \"?page=\" + akPage + \"&pageSize=\" + akPageSize;\n if (search) qs += \"&search=\" + encodeURIComponent(search);\n document.getElementById(\"ak-table-wrap\").innerHTML = skeletonTable(7, 5);\n document.getElementById(\"ak-pagination\").innerHTML = \"\";\n var data = await apiRaw(\"GET\", \"/.well-known/service/api/access-keys\" + qs);\n if (!data || data.error) {\n document.getElementById(\"ak-table-wrap\").innerHTML = '<div class=\"empty-state\"><div class=\"empty-state-title\">' + __t(\"accessKeys.loadFailed\") + '</div></div>';\n return;\n }\n renderAccessKeys(data.keys, { total: data.total, page: data.page, pageSize: data.pageSize });\n}\n\nfunction renderAccessKeys(keys, paging) {\n var wrap = document.getElementById(\"ak-table-wrap\");\n if (!keys || keys.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=\"M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4\"/></svg></div><div class=\"empty-state-title\">' + __t(\"accessKeys.noKeys\") + '</div><div class=\"empty-state-desc\">' + __t(\"accessKeys.noKeysHint\") + '</div></div>';\n document.getElementById(\"ak-pagination\").innerHTML = \"\";\n return;\n }\n\n var html = '<table class=\"table\"><thead><tr><th>' + __t(\"accessKeys.col.keyId\") + '</th><th>' + __t(\"accessKeys.col.role\") + '</th><th class=\"col-hide-sm\">' + __t(\"accessKeys.col.remark\") + '</th><th class=\"col-hide-md\">' + __t(\"accessKeys.col.createdBy\") + '</th><th>' + __t(\"accessKeys.col.expires\") + '</th><th class=\"col-hide-md\">' + __t(\"accessKeys.col.lastUsed\") + '</th><th>' + __t(\"common.actions\") + '</th></tr></thead><tbody>';\n keys.forEach(function(k) {\n var keyIdHtml = '<did-address did=\"' + escapeHtml(k.accessKeyId) + '\" compact></did-address>';\n var roleBadge = '<span class=\"badge badge-' + k.role + '\">' + __t(\"common.\" + k.role) + '</span>';\n var remark = k.remark ? escapeHtml(k.remark.length > 40 ? k.remark.slice(0, 40) + \"...\" : k.remark) : '<span style=\"color:var(--text-secondary)\">\u2014</span>';\n var creator = k.createdByName\n ? escapeHtml(k.createdByName)\n : '<did-address did=\"' + escapeHtml(k.createdBy) + '\" compact></did-address>';\n var expiry = k.expireAt ? (new Date(k.expireAt) < new Date() ? '<span style=\"color:var(--error)\">' + __t(\"common.expired\") + '</span>' : relativeTime(k.expireAt)) : __t(\"common.never\");\n var lastUsed = k.lastUsedAt ? relativeTime(k.lastUsedAt) : __t(\"common.never\");\n var deleteLabel = k.remark ? escapeHtml(k.remark) : truncateDid(k.accessKeyId);\n\n html += '<tr>';\n html += '<td><code style=\"font-size:12px\">' + keyIdHtml + '</code></td>';\n html += '<td>' + roleBadge + '</td>';\n html += '<td class=\"col-hide-sm\">' + remark + '</td>';\n html += '<td class=\"col-hide-md\">' + creator + '</td>';\n html += '<td>' + expiry + '</td>';\n html += '<td class=\"col-hide-md\">' + lastUsed + '</td>';\n html += '<td class=\"action-cell\"><button class=\"action-trigger\" onclick=\"event.stopPropagation();toggleActionMenu(this)\">\u22EF</button><div class=\"action-menu\"><button class=\"action-menu-item\" onclick=\"event.stopPropagation();openEditAkDialog(\\''+k.accessKeyId+'\\')\">' + __t(\"common.edit\") + '</button><div class=\"action-menu-sep\"></div><button class=\"action-menu-item danger\" onclick=\"event.stopPropagation();confirmDeleteAk(\\''+k.accessKeyId+'\\',\\''+escapeHtml(deleteLabel)+'\\')\">' + __t(\"common.delete\") + '</button></div></td>';\n html += '</tr>';\n });\n html += '</tbody></table>';\n wrap.innerHTML = html;\n\n renderPagination(\"ak-pagination\", paging, loadAccessKeys);\n}\n\nfunction openCreateAkDialog() {\n var callerRole = __caller.role;\n var roleOptions = '<option value=\"guest\">' + __t(\"common.guest\") + '</option>';\n if (callerRole === \"member\" || callerRole === \"admin\" || callerRole === \"owner\") roleOptions += '<option value=\"member\"' + (callerRole === \"member\" ? ' selected' : '') + '>' + __t(\"common.member\") + '</option>';\n if (callerRole === \"admin\" || callerRole === \"owner\") roleOptions += '<option value=\"admin\"' + (callerRole === \"admin\" ? ' selected' : '') + '>' + __t(\"common.admin\") + '</option>';\n if (callerRole === \"owner\") roleOptions += '<option value=\"owner\">' + __t(\"common.owner\") + '</option>';\n\n var expiryOptions = '<option value=\"\">' + __t(\"accessKeys.expirations.never\") + '</option><option value=\"7d\">' + __t(\"accessKeys.expirations.days7\") + '</option><option value=\"30d\">' + __t(\"accessKeys.expirations.days30\") + '</option><option value=\"90d\">' + __t(\"accessKeys.expirations.days90\") + '</option><option value=\"custom\">' + __t(\"accessKeys.expirations.custom\") + '</option>';\n\n var html = '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.role\") + '</label><select class=\"select\" id=\"ak-create-role\">' + roleOptions + '</select></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.remark\") + '</label><input type=\"text\" class=\"input\" id=\"ak-create-remark\" placeholder=\"' + __t(\"accessKeys.remarkPlaceholder\") + '\" maxlength=\"200\" /></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.expiration\") + '</label><select class=\"select\" id=\"ak-create-expiry\" onchange=\"toggleAkCustomExpiry()\">' + expiryOptions + '</select></div>';\n html += '<div class=\"form-group hidden\" id=\"ak-custom-expiry-wrap\"><label class=\"form-label\">' + __t(\"accessKeys.customDate\") + '</label><input type=\"datetime-local\" class=\"input\" id=\"ak-create-custom-expiry\" /></div>';\n\n openDialog(__t(\"accessKeys.createTitle\"), html, __t(\"common.save\"), async function() {\n var role = document.getElementById(\"ak-create-role\").value;\n var remark = document.getElementById(\"ak-create-remark\").value;\n var expirySelect = document.getElementById(\"ak-create-expiry\").value;\n var expireAt = null;\n\n if (expirySelect === \"custom\") {\n var customDate = document.getElementById(\"ak-create-custom-expiry\").value;\n if (!customDate) { showToast(__t(\"accessKeys.selectExpDate\"), \"error\"); return; }\n expireAt = new Date(customDate).toISOString();\n } else if (expirySelect) {\n var days = parseInt(expirySelect);\n expireAt = new Date(Date.now() + days * 86400000).toISOString();\n }\n\n var result = await apiRaw(\"POST\", \"/.well-known/service/api/access-keys\", { role: role, remark: remark, expireAt: expireAt });\n if (!result || result.error) {\n showToast(result ? result.error : __t(\"accessKeys.createFailed\"), \"error\");\n return;\n }\n\n closeDialog();\n showSecretDialog(result.accessKeySecret);\n loadAccessKeys(1);\n });\n}\n\nfunction toggleAkCustomExpiry() {\n var sel = document.getElementById(\"ak-create-expiry\").value;\n var wrap = document.getElementById(\"ak-custom-expiry-wrap\");\n if (sel === \"custom\") wrap.classList.remove(\"hidden\");\n else wrap.classList.add(\"hidden\");\n}\n\n// Slugify a display name into something safe to use as an MCP server name\n// (lowercase, alnum + hyphens). Falls back to a fixed default when empty.\nfunction akSlug(s) {\n var slug = String(s || \"\").toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n return slug || \"did-connect\";\n}\n\nvar __akSnippets = null;\n\nfunction showSecretDialog(secret) {\n var html = '<div style=\"margin-bottom:12px;color:var(--error);font-weight:500\">' + __t(\"accessKeys.secretWarning\") + '</div>';\n var copyIconSvg = '<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\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"/><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"/></svg>';\n var copyBtnStyle = 'position:absolute;top:4px;right:4px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;background:var(--bg-base);border:1px solid var(--border);border-radius:4px;cursor:pointer;color:var(--text-secondary);transition:color 0.15s,border-color 0.15s';\n html += '<div style=\"position:relative\"><code id=\"ak-secret-display\" style=\"display:block;padding:12px 40px 12px 12px;background:var(--bg-base);border:1px solid var(--border);border-radius:6px;font-size:13px;word-break:break-all;user-select:all;overflow-wrap:anywhere\">' + escapeHtml(secret) + '</code>';\n html += '<button id=\"ak-copy-btn\" style=\"' + copyBtnStyle + '\" onclick=\"copyAkSecret()\" title=\"' + __t(\"common.copy\") + '\">' + copyIconSvg + '</button></div>';\n\n var mcpUrl = location.origin + \"/mcp\";\n var serverName = akSlug(__pageData && __pageData.appName);\n\n __akSnippets = {\n curl: {\n label: __t(\"accessKeys.tabCurl\"),\n hint: \"\",\n code: 'curl -H \"Authorization: Bearer ' + secret + '\" ' + location.origin + '/.well-known/service/api/did/session',\n },\n claude: {\n label: __t(\"accessKeys.tabClaude\"),\n hint: \"\",\n code: 'claude mcp add --transport http ' + serverName + ' ' + mcpUrl + ' --header \"Authorization: Bearer ' + secret + '\"',\n },\n codex: {\n label: __t(\"accessKeys.tabCodex\"),\n hint: __t(\"accessKeys.addToConfig\", { file: \"~/.codex/config.toml\" }),\n code: '[mcp_servers.' + serverName + ']\\nurl = \"' + mcpUrl + '\"\\nhttp_headers = { \"Authorization\" = \"Bearer ' + secret + '\" }',\n },\n cursor: {\n label: __t(\"accessKeys.tabCursor\"),\n hint: __t(\"accessKeys.addToConfig\", { file: \"mcp.json\" }),\n code: '{\\n \"mcpServers\": {\\n \"' + serverName + '\": {\\n \"url\": \"' + mcpUrl + '\",\\n \"headers\": {\\n \"Authorization\": \"Bearer ' + secret + '\"\\n }\\n }\\n }\\n}',\n },\n };\n\n html += '<div style=\"margin-top:16px;font-size:13px;color:var(--text-secondary)\"><strong>' + __t(\"accessKeys.usage\") + '</strong></div>';\n html += '<div style=\"display:flex;gap:4px;margin-top:8px;border-bottom:1px solid var(--border)\">';\n [\"curl\", \"claude\", \"codex\", \"cursor\"].forEach(function(key) {\n var isActive = key === \"curl\";\n html += '<button type=\"button\" class=\"ak-tab-btn\" data-ak-tab=\"' + key + '\" onclick=\"switchAkTab(\\'' + key + '\\')\" style=\"padding:6px 12px;font-size:12px;font-weight:500;border:none;background:none;cursor:pointer;color:' + (isActive ? \"var(--text-white)\" : \"var(--text-tertiary)\") + ';border-bottom:2px solid ' + (isActive ? \"var(--text-white)\" : \"transparent\") + '\">' + __akSnippets[key].label + '</button>';\n });\n html += '</div>';\n html += '<div id=\"ak-snippet-hint\" style=\"margin-top:8px;font-size:12px;color:var(--text-secondary);display:none\"></div>';\n html += '<div style=\"position:relative;margin-top:6px\"><pre id=\"ak-snippet-display\" style=\"margin:0;display:block;padding:12px 40px 12px 12px;background:var(--bg-base);border:1px solid var(--border);border-radius:6px;font-size:12px;white-space:pre-wrap;word-break:break-all;overflow-wrap:anywhere;user-select:all;font-family:inherit\"></pre>';\n html += '<button id=\"ak-snippet-copy-btn\" style=\"' + copyBtnStyle + '\" onclick=\"copyAkSnippet()\" title=\"' + __t(\"common.copy\") + '\">' + copyIconSvg + '</button></div>';\n\n openDialog(__t(\"accessKeys.secretTitle\"), html, __t(\"accessKeys.done\"), function() {\n document.getElementById(\"ak-secret-display\").textContent = \"\";\n __akSnippets = null;\n closeDialog();\n });\n\n switchAkTab(\"curl\");\n}\n\nfunction switchAkTab(key) {\n var snippet = __akSnippets && __akSnippets[key];\n if (!snippet) return;\n\n document.querySelectorAll(\".ak-tab-btn\").forEach(function(btn) {\n var isActive = btn.getAttribute(\"data-ak-tab\") === key;\n btn.style.color = isActive ? \"var(--text-white)\" : \"var(--text-tertiary)\";\n btn.style.borderBottomColor = isActive ? \"var(--text-white)\" : \"transparent\";\n });\n\n var hintEl = document.getElementById(\"ak-snippet-hint\");\n if (hintEl) {\n hintEl.textContent = snippet.hint || \"\";\n hintEl.style.display = snippet.hint ? \"block\" : \"none\";\n }\n var displayEl = document.getElementById(\"ak-snippet-display\");\n if (displayEl) displayEl.textContent = snippet.code;\n}\n\nfunction copyAkSnippet() {\n var text = document.getElementById(\"ak-snippet-display\").textContent;\n var btn = document.getElementById(\"ak-snippet-copy-btn\");\n var origHtml = btn.innerHTML;\n function onCopied() {\n btn.innerHTML = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 13l4 4L19 7\"/></svg>';\n btn.style.color = \"var(--green)\";\n setTimeout(function() { btn.innerHTML = origHtml; btn.style.color = \"\"; }, 2000);\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(onCopied).catch(function() {\n fallbackCopy(text);\n onCopied();\n });\n } else {\n fallbackCopy(text);\n onCopied();\n }\n}\n\nfunction copyAkSecret() {\n var text = document.getElementById(\"ak-secret-display\").textContent;\n var btn = document.getElementById(\"ak-copy-btn\");\n var origHtml = btn.innerHTML;\n function onCopied() {\n btn.innerHTML = '<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 13l4 4L19 7\"/></svg>';\n btn.style.color = \"var(--green)\";\n setTimeout(function() { btn.innerHTML = origHtml; btn.style.color = \"\"; }, 2000);\n }\n if (navigator.clipboard && navigator.clipboard.writeText) {\n navigator.clipboard.writeText(text).then(onCopied).catch(function() {\n fallbackCopy(text);\n onCopied();\n });\n } else {\n fallbackCopy(text);\n onCopied();\n }\n}\n\nasync function openEditAkDialog(accessKeyId) {\n var data = await apiRaw(\"GET\", \"/.well-known/service/api/access-keys/\" + encodeURIComponent(accessKeyId));\n if (!data || data.error) { showToast(__t(\"accessKeys.loadKeyFailed\"), \"error\"); return; }\n\n var html = '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.col.keyId\") + '</label><code style=\"font-size:12px\">' + escapeHtml(data.accessKeyId) + '</code></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.role\") + '</label><span class=\"badge badge-' + data.role + '\">' + __t(\"common.\" + data.role) + '</span></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.remark\") + '</label><input type=\"text\" class=\"input\" id=\"ak-edit-remark\" value=\"' + escapeHtml(data.remark || \"\") + '\" maxlength=\"200\" /></div>';\n html += '<div class=\"form-group\"><label class=\"form-label\">' + __t(\"accessKeys.expiration\") + '</label><input type=\"datetime-local\" class=\"input\" id=\"ak-edit-expiry\" value=\"' + (data.expireAt ? data.expireAt.slice(0,16) : \"\") + '\" /> <label style=\"margin-top:4px;display:block;font-size:13px\"><input type=\"checkbox\" id=\"ak-edit-no-expiry\"' + (data.expireAt ? \"\" : \" checked\") + ' onchange=\"toggleAkEditExpiry()\" /> ' + __t(\"accessKeys.neverExpires\") + '</label></div>';\n\n openDialog(__t(\"accessKeys.editTitle\"), html, __t(\"common.save\"), async function() {\n var remark = document.getElementById(\"ak-edit-remark\").value;\n var noExpiry = document.getElementById(\"ak-edit-no-expiry\").checked;\n var expireAt = noExpiry ? null : (document.getElementById(\"ak-edit-expiry\").value ? new Date(document.getElementById(\"ak-edit-expiry\").value).toISOString() : undefined);\n\n var body = { remark: remark };\n if (expireAt !== undefined) body.expireAt = expireAt;\n\n var result = await apiRaw(\"PUT\", \"/.well-known/service/api/access-keys/\" + encodeURIComponent(accessKeyId), body);\n if (!result || result.error) {\n showToast(result ? result.error : __t(\"accessKeys.updateFailed\"), \"error\");\n return;\n }\n closeDialog();\n showToast(__t(\"accessKeys.keyUpdated\"), \"success\");\n loadAccessKeys(akPage);\n });\n}\n\nfunction toggleAkEditExpiry() {\n var noExpiry = document.getElementById(\"ak-edit-no-expiry\").checked;\n document.getElementById(\"ak-edit-expiry\").disabled = noExpiry;\n}\n\nfunction confirmDeleteAk(accessKeyId, label) {\n openDialog(__t(\"accessKeys.deleteTitle\"), '<p>' + __t(\"accessKeys.deleteMsg\", { label: escapeHtml(label) }) + '</p><p style=\"color:var(--error)\">' + __t(\"accessKeys.deleteWarning\") + '</p>', __t(\"common.delete\"), async function() {\n var result = await apiRaw(\"DELETE\", \"/.well-known/service/api/access-keys/\" + encodeURIComponent(accessKeyId));\n if (result && result.error) {\n showToast(result.error, \"error\");\n return;\n }\n closeDialog();\n showToast(__t(\"accessKeys.keyDeleted\"), \"success\");\n loadAccessKeys(akPage);\n });\n}\n\n// Direct API call (bypasses team handler's /team prefix)\nasync function apiRaw(method, path, body) {\n try {\n var opts = { method: method, headers: { \"Accept\": \"application/json\" } };\n if (body !== undefined) {\n opts.headers[\"Content-Type\"] = \"application/json\";\n opts.body = JSON.stringify(body);\n }\n var res = await fetch(path, opts);\n if (res.status === 204) return {};\n return await res.json();\n } catch(e) {\n return null;\n }\n}\n\nregisterTab(\"access-keys\", function() { loadAccessKeys(1); });\n";
7
7
  export {};
8
8
  //# sourceMappingURL=tab-access-keys.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tab-access-keys.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-access-keys.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,iBAAiB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAoBhD;AAED,eAAO,MAAM,sBAAsB,m/jBAqSlC,CAAC"}
1
+ {"version":3,"file":"tab-access-keys.d.ts","sourceRoot":"","sources":["../../../src/pages/admin/tab-access-keys.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,iBAAiB,CAAC,CAAC,EAAE,GAAG,GAAG,MAAM,CAoBhD;AAED,eAAO,MAAM,sBAAsB,mlkBAqSlC,CAAC"}
@@ -85,10 +85,10 @@ function renderAccessKeys(keys, paging) {
85
85
 
86
86
  function openCreateAkDialog() {
87
87
  var callerRole = __caller.role;
88
- var roleOptions = '<option value="guest">Guest</option>';
89
- if (callerRole === "member" || callerRole === "admin" || callerRole === "owner") roleOptions += '<option value="member"' + (callerRole === "member" ? ' selected' : '') + '>Member</option>';
90
- if (callerRole === "admin" || callerRole === "owner") roleOptions += '<option value="admin"' + (callerRole === "admin" ? ' selected' : '') + '>Admin</option>';
91
- if (callerRole === "owner") roleOptions += '<option value="owner">Owner</option>';
88
+ var roleOptions = '<option value="guest">' + __t("common.guest") + '</option>';
89
+ if (callerRole === "member" || callerRole === "admin" || callerRole === "owner") roleOptions += '<option value="member"' + (callerRole === "member" ? ' selected' : '') + '>' + __t("common.member") + '</option>';
90
+ if (callerRole === "admin" || callerRole === "owner") roleOptions += '<option value="admin"' + (callerRole === "admin" ? ' selected' : '') + '>' + __t("common.admin") + '</option>';
91
+ if (callerRole === "owner") roleOptions += '<option value="owner">' + __t("common.owner") + '</option>';
92
92
 
93
93
  var expiryOptions = '<option value="">' + __t("accessKeys.expirations.never") + '</option><option value="7d">' + __t("accessKeys.expirations.days7") + '</option><option value="30d">' + __t("accessKeys.expirations.days30") + '</option><option value="90d">' + __t("accessKeys.expirations.days90") + '</option><option value="custom">' + __t("accessKeys.expirations.custom") + '</option>';
94
94
 
@@ -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\">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\">Guest</option><option value=\"member\">Member</option><option value=\"admin\">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\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,ojMAwGlC,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,opMAwGlC,CAAC"}
@@ -90,7 +90,7 @@ function renderInvitationsTable(invitations, paging) {
90
90
  var uses = inv.useCount + " / " + inv.maxUses;
91
91
  if (inv.useCount >= inv.maxUses) uses += ' <span class="badge badge-blocked">' + __t("invitations.closed") + '</span>';
92
92
  var expired = new Date(inv.expireAt) < new Date();
93
- var expireText = expired ? '<span class="badge badge-expired">expired</span>' : '<span class="cell-date">' + escapeHtml(relativeTime(inv.expireAt)) + '</span>';
93
+ var expireText = expired ? '<span class="badge badge-expired">' + __t("common.expired") + '</span>' : '<span class="cell-date">' + escapeHtml(relativeTime(inv.expireAt)) + '</span>';
94
94
  var isActive = !expired && inv.status !== "closed" && inv.useCount < inv.maxUses;
95
95
  var inviter = inv.inviterName ? escapeHtml(inv.inviterName) : '<span class="text-muted">—</span>';
96
96
 
@@ -149,7 +149,7 @@ async function deleteInvitation(id) {
149
149
  function initInviteRoleOptions() {
150
150
  var select = document.getElementById("invite-role");
151
151
  if (__caller.role === "owner") {
152
- select.innerHTML = '<option value="guest">Guest</option><option value="member">Member</option><option value="admin">Admin</option>';
152
+ select.innerHTML = '<option value="guest">' + __t("common.guest") + '</option><option value="member">' + __t("common.member") + '</option><option value="admin">' + __t("common.admin") + '</option>';
153
153
  }
154
154
  }
155
155
 
@@ -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 if (!users || users.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=\"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 = '<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// 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,oy0BAoe9B,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,mu1BAwe9B,CAAC"}
@@ -14,7 +14,7 @@ export function membersTabHtml(t) {
14
14
  </div>
15
15
  <select class="select" id="members-instance-filter" onchange="loadMembers(1)" style="display:none">
16
16
  <option value="all">${t("members.allInstances")}</option>
17
- <option value="">${t("members.currentInstance")}</option>
17
+ <option value="" selected>${t("members.currentInstance")}</option>
18
18
  </select>
19
19
  <select class="select" id="members-role-filter" onchange="loadMembers(1)">
20
20
  <option value="">${t("members.allRoles")}</option>
@@ -259,8 +259,12 @@ async function loadMembers(page) {
259
259
 
260
260
  function renderMembersTable(users, paging) {
261
261
  var wrap = document.getElementById("members-table-wrap");
262
+ // Aggregated (cross-instance "All Instances") view can't tell which instance
263
+ // a role change should apply to, so per-row actions are disabled there — see
264
+ // canOperate below. Surface why, instead of silently hiding the buttons.
265
+ var aggregatedHint = __membersAggregated ? '<p class="text-muted" style="font-size:13px;margin-bottom:12px;">' + __t("members.aggregatedHint") + '</p>' : '';
262
266
  if (!users || users.length === 0) {
263
- 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="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>';
267
+ 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>';
264
268
  document.getElementById("members-pagination").innerHTML = "";
265
269
  return;
266
270
  }
@@ -268,7 +272,7 @@ function renderMembersTable(users, paging) {
268
272
  var instanceSel = document.getElementById("members-instance-filter");
269
273
  var instanceVisible = instanceSel && instanceSel.style.display !== 'none';
270
274
  var showInstance = instanceVisible && instanceSel.value === "all";
271
- var html = '<table class="table"><thead><tr>';
275
+ var html = aggregatedHint + '<table class="table"><thead><tr>';
272
276
  html += sortableTh("name", __t("members.col.member"));
273
277
  html += sortableTh("role", __t("members.col.role"));
274
278
  if (showInstance) html += '<th>' + __t("members.col.instance") + '</th>';
@@ -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;yBAC5B,CAAC,CAAC,yBAAyB,CAAC;;;yBAG5B,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoe1C,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwe1C,CAAC"}
@@ -260,7 +260,7 @@ function connectOAuth(provider) {
260
260
  var left = (screen.width - w) / 2, top = (screen.height - h) / 2;
261
261
  var popup = window.open(url, "oauth_bind", "width=" + w + ",height=" + h + ",left=" + left + ",top=" + top);
262
262
  if (!popup) {
263
- showToast("Popup blocked — please allow popups and try again", "error");
263
+ showToast(__t("common.popupBlocked"), "error");
264
264
  return;
265
265
  }
266
266
 
@@ -4,5 +4,5 @@
4
4
  * Pattern: embedded LoginPage + registration form + registered instance list.
5
5
  * Follows gen-access-key-page.ts architecture.
6
6
  */
7
- export declare function buildAdminInstancesPageHTML(): string;
7
+ export declare function buildAdminInstancesPageHTML(locale?: string): string;
8
8
  //# sourceMappingURL=admin-instances-page.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin-instances-page.d.ts","sourceRoot":"","sources":["../../src/pages/admin-instances-page.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwMH,wBAAgB,2BAA2B,IAAI,MAAM,CAmVpD"}
1
+ {"version":3,"file":"admin-instances-page.d.ts","sourceRoot":"","sources":["../../src/pages/admin-instances-page.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAsTH,wBAAgB,2BAA2B,CAAC,MAAM,SAAO,GAAG,MAAM,CAwWjE"}