@memtensor/memos-local-openclaw-plugin 1.0.4-beta.9 → 1.0.4

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 (99) hide show
  1. package/.env.example +7 -0
  2. package/README.md +94 -27
  3. package/dist/capture/index.js +3 -1
  4. package/dist/capture/index.js.map +1 -1
  5. package/dist/client/connector.d.ts +5 -0
  6. package/dist/client/connector.d.ts.map +1 -1
  7. package/dist/client/connector.js +89 -8
  8. package/dist/client/connector.js.map +1 -1
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +2 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/hub/server.d.ts +2 -0
  13. package/dist/hub/server.d.ts.map +1 -1
  14. package/dist/hub/server.js +240 -35
  15. package/dist/hub/server.js.map +1 -1
  16. package/dist/hub/user-manager.d.ts +9 -0
  17. package/dist/hub/user-manager.d.ts.map +1 -1
  18. package/dist/hub/user-manager.js +26 -2
  19. package/dist/hub/user-manager.js.map +1 -1
  20. package/dist/ingest/chunker.d.ts +2 -1
  21. package/dist/ingest/chunker.d.ts.map +1 -1
  22. package/dist/ingest/chunker.js +14 -10
  23. package/dist/ingest/chunker.js.map +1 -1
  24. package/dist/ingest/providers/index.js +2 -2
  25. package/dist/ingest/providers/index.js.map +1 -1
  26. package/dist/recall/engine.d.ts.map +1 -1
  27. package/dist/recall/engine.js +22 -4
  28. package/dist/recall/engine.js.map +1 -1
  29. package/dist/shared/llm-call.d.ts.map +1 -1
  30. package/dist/shared/llm-call.js +2 -1
  31. package/dist/shared/llm-call.js.map +1 -1
  32. package/dist/sharing/types.d.ts +1 -1
  33. package/dist/sharing/types.d.ts.map +1 -1
  34. package/dist/skill/evolver.d.ts +2 -0
  35. package/dist/skill/evolver.d.ts.map +1 -1
  36. package/dist/skill/evolver.js +56 -5
  37. package/dist/skill/evolver.js.map +1 -1
  38. package/dist/skill/generator.d.ts +2 -0
  39. package/dist/skill/generator.d.ts.map +1 -1
  40. package/dist/skill/generator.js +45 -3
  41. package/dist/skill/generator.js.map +1 -1
  42. package/dist/skill/installer.d.ts +26 -0
  43. package/dist/skill/installer.d.ts.map +1 -1
  44. package/dist/skill/installer.js +80 -4
  45. package/dist/skill/installer.js.map +1 -1
  46. package/dist/skill/upgrader.d.ts +2 -0
  47. package/dist/skill/upgrader.d.ts.map +1 -1
  48. package/dist/skill/upgrader.js +139 -1
  49. package/dist/skill/upgrader.js.map +1 -1
  50. package/dist/skill/validator.d.ts +3 -0
  51. package/dist/skill/validator.d.ts.map +1 -1
  52. package/dist/skill/validator.js +75 -0
  53. package/dist/skill/validator.js.map +1 -1
  54. package/dist/storage/sqlite.d.ts +57 -0
  55. package/dist/storage/sqlite.d.ts.map +1 -1
  56. package/dist/storage/sqlite.js +290 -35
  57. package/dist/storage/sqlite.js.map +1 -1
  58. package/dist/telemetry.d.ts.map +1 -1
  59. package/dist/telemetry.js +27 -8
  60. package/dist/telemetry.js.map +1 -1
  61. package/dist/types.d.ts +10 -0
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js +4 -0
  64. package/dist/types.js.map +1 -1
  65. package/dist/viewer/html.d.ts.map +1 -1
  66. package/dist/viewer/html.js +564 -225
  67. package/dist/viewer/html.js.map +1 -1
  68. package/dist/viewer/server.d.ts +9 -0
  69. package/dist/viewer/server.d.ts.map +1 -1
  70. package/dist/viewer/server.js +357 -108
  71. package/dist/viewer/server.js.map +1 -1
  72. package/index.ts +411 -52
  73. package/openclaw.plugin.json +1 -1
  74. package/package.json +2 -1
  75. package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
  76. package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
  77. package/prebuilds/linux-x64/better_sqlite3.node +0 -0
  78. package/prebuilds/win32-x64/better_sqlite3.node +0 -0
  79. package/src/capture/index.ts +4 -1
  80. package/src/client/connector.ts +92 -8
  81. package/src/config.ts +2 -1
  82. package/src/hub/server.ts +235 -35
  83. package/src/hub/user-manager.ts +42 -6
  84. package/src/ingest/chunker.ts +19 -13
  85. package/src/ingest/providers/index.ts +2 -2
  86. package/src/recall/engine.ts +20 -4
  87. package/src/shared/llm-call.ts +2 -1
  88. package/src/sharing/types.ts +1 -1
  89. package/src/skill/evolver.ts +58 -6
  90. package/src/skill/generator.ts +44 -5
  91. package/src/skill/installer.ts +107 -4
  92. package/src/skill/upgrader.ts +139 -1
  93. package/src/skill/validator.ts +79 -0
  94. package/src/storage/sqlite.ts +318 -40
  95. package/src/telemetry.ts +27 -9
  96. package/src/types.ts +11 -0
  97. package/src/viewer/html.ts +564 -225
  98. package/src/viewer/server.ts +333 -105
  99. package/telemetry.credentials.json +5 -0
@@ -157,7 +157,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
157
157
  .search-bar input::placeholder{color:var(--text-muted)}
158
158
  .search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}
159
159
  .search-bar .search-icon{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:14px;pointer-events:none}
160
- .search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}
160
+ .search-meta{font-size:12px;color:var(--text-sec);padding:0 2px}.search-meta:not(:empty){margin-bottom:14px}
161
161
  .scope-select{padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);color:var(--text);font-size:13px;min-width:110px;outline:none}
162
162
  .sharing-inline-meta{font-size:12px;color:var(--text-muted);margin:-8px 0 14px 2px}
163
163
  .sharing-sidebar-card{margin:14px 0 18px;border:1px solid var(--border);background:var(--bg-card);border-radius:12px;padding:12px;box-shadow:var(--shadow-sm)}
@@ -251,9 +251,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
251
251
  .au-status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:middle;flex-shrink:0;transition:all .3s}
252
252
  .au-status-dot.online{background:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.6),0 0 16px rgba(34,197,94,.2);animation:dotBreathe 2s ease-in-out infinite}
253
253
  .au-status-dot.offline{background:#6b7280;box-shadow:none}
254
- .au-status-text{font-size:11px;font-weight:600;letter-spacing:.02em}
255
- .au-status-text.online{color:#22c55e;text-shadow:0 0 8px rgba(34,197,94,.3)}
256
- .au-status-text.offline{color:#6b7280}
254
+ .au-status-text{font-size:10px;font-weight:600;letter-spacing:.04em;padding:3px 10px;border-radius:6px;white-space:nowrap}
255
+ .au-status-text.online{color:#22c55e;background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.18);text-shadow:0 0 8px rgba(34,197,94,.2)}
256
+ .au-status-text.offline{color:#6b7280;background:rgba(107,114,128,.06);border:1px solid rgba(107,114,128,.1)}
257
+ [data-theme="light"] .au-status-text.online{background:rgba(34,197,94,.06);border-color:rgba(34,197,94,.15)}
258
+ [data-theme="light"] .au-status-text.offline{background:rgba(0,0,0,.03);border-color:rgba(0,0,0,.06)}
257
259
  .au-group-header{font-size:13px;font-weight:700;color:var(--text-sec);margin:20px 0 10px;display:flex;align-items:center;gap:8px;letter-spacing:.02em}
258
260
  .au-group-header:first-child{margin-top:0}
259
261
  .au-group-header .au-group-dot{display:inline-block;width:8px;height:8px;border-radius:50%}
@@ -288,7 +290,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
288
290
  .admin-card-tag.tag-version{background:rgba(139,92,246,.1);color:#8b5cf6}
289
291
  .admin-card-tag.tag-visibility{background:rgba(99,102,241,.08);color:var(--pri)}
290
292
  .admin-card-tag.tag-group{background:rgba(139,92,246,.08);color:#8b5cf6}
291
- .admin-card-preview{font-size:12px;color:var(--text-sec);line-height:1.5;margin:8px 0;padding:10px 12px;background:rgba(99,102,241,.02);border-radius:10px;border:1px solid rgba(99,102,241,.08);max-height:60px;overflow:hidden;white-space:pre-wrap;word-break:break-all}
293
+ .admin-card-preview{font-size:12px;color:var(--text-sec);line-height:1.5;margin:8px 0;padding:10px 12px;background:rgba(99,102,241,.02);border-radius:10px;border:1px solid rgba(99,102,241,.08);max-height:120px;overflow:hidden;white-space:pre-wrap;word-break:break-all;position:relative;-webkit-mask-image:linear-gradient(to bottom,#000 88%,transparent 100%);mask-image:linear-gradient(to bottom,#000 88%,transparent 100%)}
292
294
  .admin-card-actions{display:inline-flex;gap:6px;margin-left:auto;align-items:center;flex-shrink:0}
293
295
  .admin-card-time{font-size:11px;color:var(--text-muted)}
294
296
  .admin-card-detail{display:none;margin-top:0;padding:20px 24px 24px;border-top:1px dashed rgba(99,102,241,.12);background:linear-gradient(180deg,rgba(99,102,241,.02) 0%,transparent 60%);animation:adminDetailIn .25s ease}
@@ -323,7 +325,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
323
325
  .adm-msg-side.assistant .adm-msg-role{color:var(--green)}
324
326
  .adm-msg-time{font-size:9px;color:var(--text-muted)}
325
327
  .adm-msg-body{flex:1;min-width:0;padding:12px 16px;font-size:13px;line-height:1.75;color:var(--text);white-space:pre-wrap;word-break:break-word}
326
- .adm-msg-body.collapsed{max-height:120px;overflow:hidden;-webkit-mask-image:linear-gradient(180deg,#000 65%,transparent);mask-image:linear-gradient(180deg,#000 65%,transparent)}
328
+ .adm-msg-body.collapsed{max-height:120px;overflow:hidden;-webkit-mask-image:linear-gradient(180deg,#000 88%,transparent);mask-image:linear-gradient(180deg,#000 88%,transparent)}
327
329
  .adm-msg-toggle{display:none;padding:0 16px 8px;font-size:11px;color:var(--pri);cursor:pointer;transition:color .15s}
328
330
  .adm-msg-toggle:hover{color:var(--pri-dark)}
329
331
  .admin-card-expand-btn{font-size:12px;color:var(--pri);cursor:pointer;background:none;border:none;padding:2px 6px;font-family:inherit}
@@ -331,17 +333,21 @@ input,textarea,select{font-family:inherit;font-size:inherit}
331
333
  .admin-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap}
332
334
  .admin-toolbar h3{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;margin:0;margin-right:auto;line-height:32px}
333
335
  .admin-toolbar select{box-sizing:border-box;height:32px;font-size:12px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);color:var(--text);vertical-align:middle;margin:0;padding:0 10px}
334
- .admin-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:700;padding:3px 10px;border-radius:999px;letter-spacing:.03em;text-transform:uppercase}
335
- .admin-badge.admin{background:linear-gradient(135deg,rgba(34,197,94,.15),rgba(16,185,129,.1));color:#22c55e;box-shadow:0 0 8px rgba(34,197,94,.1)}
336
- .admin-badge.member{background:rgba(99,102,241,.08);color:var(--text-muted)}
337
- .admin-badge.pending{background:linear-gradient(135deg,rgba(251,191,36,.2),rgba(245,158,11,.1));color:#fbbf24;box-shadow:0 0 8px rgba(251,191,36,.1)}
338
- .admin-badge.public{background:rgba(99,102,241,.1);color:var(--pri)}
339
- .admin-badge.group{background:rgba(139,92,246,.1);color:var(--violet)}
336
+ .admin-badge{display:inline-flex;align-items:center;gap:4px;font-size:9px;font-weight:700;padding:3px 10px;border-radius:6px;letter-spacing:.06em;text-transform:uppercase;position:relative;backdrop-filter:blur(4px);transition:all .25s}
337
+ .admin-badge.admin{background:linear-gradient(135deg,rgba(34,197,94,.12),rgba(16,185,129,.06));color:#22c55e;border:1px solid rgba(34,197,94,.2);box-shadow:0 0 12px rgba(34,197,94,.08),inset 0 1px 0 rgba(255,255,255,.05)}
338
+ .admin-badge.admin:hover{box-shadow:0 0 20px rgba(34,197,94,.15),inset 0 1px 0 rgba(255,255,255,.08)}
339
+ .admin-badge.member{background:rgba(99,102,241,.06);color:var(--text-muted);border:1px solid rgba(99,102,241,.12)}
340
+ .admin-badge.pending{background:linear-gradient(135deg,rgba(251,191,36,.12),rgba(245,158,11,.06));color:#fbbf24;border:1px solid rgba(251,191,36,.2);box-shadow:0 0 12px rgba(251,191,36,.08)}
341
+ .admin-badge.public{background:rgba(99,102,241,.08);color:var(--pri);border:1px solid rgba(99,102,241,.15)}
342
+ .admin-badge.group{background:rgba(139,92,246,.08);color:#8b5cf6;border:1px solid rgba(139,92,246,.15)}
343
+ .admin-badge.owner{background:linear-gradient(135deg,rgba(251,191,36,.12),rgba(245,158,11,.08));color:#f59e0b;border:1px solid rgba(251,191,36,.25);box-shadow:0 0 12px rgba(251,191,36,.1),inset 0 1px 0 rgba(255,255,255,.06)}
344
+ .au-badges{display:flex;align-items:center;gap:6px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end}
340
345
  .admin-empty{font-size:13px;color:var(--text-muted);padding:48px 24px;text-align:center;border:1px dashed rgba(99,102,241,.15);border-radius:16px;background:rgba(99,102,241,.02)}
341
346
  .admin-empty .ae-icon{font-size:32px;display:block;margin-bottom:10px;opacity:.4}
342
- [data-theme="light"] .admin-badge.admin{background:rgba(5,150,105,.1);color:#059669}
343
- [data-theme="light"] .admin-badge.member{background:rgba(0,0,0,.06);color:#6b7280}
344
- [data-theme="light"] .admin-badge.pending{background:rgba(245,158,11,.1);color:#d97706}
347
+ [data-theme="light"] .admin-badge.admin{background:rgba(5,150,105,.08);color:#059669;border-color:rgba(5,150,105,.18)}
348
+ [data-theme="light"] .admin-badge.member{background:rgba(0,0,0,.04);color:#6b7280;border-color:rgba(0,0,0,.08)}
349
+ [data-theme="light"] .admin-badge.pending{background:rgba(245,158,11,.08);color:#d97706;border-color:rgba(245,158,11,.18)}
350
+ [data-theme="light"] .admin-badge.owner{background:rgba(245,158,11,.08);color:#b45309;border-color:rgba(245,158,11,.2)}
345
351
  [data-theme="light"] .admin-header{background:linear-gradient(135deg,rgba(99,102,241,.04) 0%,rgba(6,182,212,.03) 50%,rgba(139,92,246,.04) 100%)}
346
352
  [data-theme="light"] .admin-stat-box{background:rgba(255,255,255,.8)}
347
353
  [data-theme="light"] .admin-pending-section{background:linear-gradient(135deg,rgba(251,191,36,.03),rgba(245,158,11,.015))}
@@ -742,7 +748,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
742
748
  [data-theme="light"] .nav-tabs{background:rgba(0,0,0,.05)}
743
749
  [data-theme="light"] .nav-tabs .tab.active{background:#fff;border-color:rgba(0,0,0,.1);box-shadow:0 1px 3px rgba(0,0,0,.08);color:var(--text)}
744
750
  .analytics-view,.settings-view,.logs-view,.migrate-view,.admin-view{flex:1;min-width:0;flex-direction:column;gap:20px}
745
- .feed-wrap,.tasks-view,.skills-view,.analytics-view,.settings-view,.logs-view,.migrate-view,.admin-view{max-width:960px;margin:0 auto}
751
+ .feed-wrap,.tasks-view,.skills-view,.analytics-view,.logs-view,.migrate-view,.admin-view,.settings-view{max-width:960px}
746
752
 
747
753
  /* ─── Logs ─── */
748
754
  .logs-toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 0}
@@ -801,6 +807,10 @@ input,textarea,select{font-family:inherit;font-size:inherit}
801
807
  .recall-score.high{background:rgba(34,197,94,.12);color:#22c55e}
802
808
  .recall-score.mid{background:rgba(251,191,36,.12);color:#f59e0b}
803
809
  .recall-score.low{background:rgba(248,113,113,.1);color:var(--text-muted)}
810
+ .recall-origin{flex-shrink:0;font-size:9px;font-weight:600;padding:1px 5px;border-radius:4px}
811
+ .recall-origin.local-shared{background:rgba(59,130,246,.12);color:#3b82f6}
812
+ .recall-origin.hub-memory{background:rgba(139,92,246,.12);color:#8b5cf6}
813
+ .recall-origin.hub-remote{background:rgba(139,92,246,.12);color:#8b5cf6}
804
814
  .recall-summary-short{flex:1;color:var(--text-sec);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
805
815
  .recall-expand-icon{flex-shrink:0;font-size:10px;color:var(--text-muted);transition:transform .15s}
806
816
  .recall-item.expanded .recall-expand-icon{transform:rotate(90deg)}
@@ -969,8 +979,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
969
979
  .team-guide-steps li::marker{color:var(--pri);font-weight:700;font-size:11px}
970
980
  .team-guide-opt .btn-guide{font-size:11px;padding:5px 14px;border-radius:6px;font-weight:600;border:1px solid rgba(99,102,241,.25);background:rgba(99,102,241,.08);color:var(--pri);cursor:pointer;transition:background .15s,border-color .15s}
971
981
  .team-guide-opt .btn-guide:hover{background:rgba(99,102,241,.14);border-color:var(--pri)}
972
- .team-guide-dismiss{position:absolute;top:10px;right:12px;background:none;border:none;color:var(--text-muted);font-size:15px;cursor:pointer;padding:4px;line-height:1;opacity:.5;transition:opacity .15s}
973
- .team-guide-dismiss:hover{opacity:1}
974
982
  [data-theme="light"] .team-guide{background:linear-gradient(135deg,rgba(6,182,212,.03),rgba(79,70,229,.02));border-color:rgba(6,182,212,.15)}
975
983
  [data-theme="light"] .team-guide-opt{box-shadow:0 1px 3px rgba(0,0,0,.03)}
976
984
  [data-theme="light"] .team-guide-opt:hover{box-shadow:0 4px 16px rgba(0,0,0,.04)}
@@ -1213,7 +1221,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1213
1221
  </div>
1214
1222
  <button class="btn btn-ghost btn-sm" onclick="doLogout()" data-i18n="logout">Logout</button>
1215
1223
  </div>
1216
- </div>
1224
+ </div>
1217
1225
  </div>
1218
1226
 
1219
1227
  <div class="main-content">
@@ -1222,7 +1230,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1222
1230
  <div class="stat-card pri"><div class="stat-value" id="statTotal">-</div><div class="stat-label" data-i18n="stat.memories">Memories</div></div>
1223
1231
  <div class="stat-card green"><div class="stat-value" id="statSessions">-</div><div class="stat-label" data-i18n="stat.sessions">Sessions</div></div>
1224
1232
  <div class="stat-card amber"><div class="stat-value" id="statEmbeddings">-</div><div class="stat-label" data-i18n="stat.embeddings">Embeddings</div></div>
1225
- <div class="stat-card rose"><div class="stat-value" id="statTimeSpan">-</div><div class="stat-label" data-i18n="stat.days">Days</div></div>
1233
+ <div class="stat-card rose"><div class="stat-value" id="statAgents">-</div><div class="stat-label" data-i18n="stat.agents">Agents</div></div>
1226
1234
  </div>
1227
1235
  <div id="sidebarSharingSection" style="display:none">
1228
1236
  <div class="sharing-sidebar-card">
@@ -1245,7 +1253,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1245
1253
  <select id="filterOwner" class="filter-select" onchange="onOwnerFilterChange()">
1246
1254
  <option value="" data-i18n="filter.allagents">All agents</option>
1247
1255
  </select>
1248
- <select id="memorySearchScope" class="filter-select" onchange="onMemoryScopeChange()">
1256
+ <select id="memorySearchScope" class="filter-select" onchange="onMemoryScopeChange()" style="display:none">
1249
1257
  <option value="local" data-i18n="scope.thisAgent">This Agent</option>
1250
1258
  <option value="allLocal" data-i18n="scope.thisDevice">This Device</option>
1251
1259
  <option value="hub" data-i18n="scope.hub">Team</option>
@@ -1257,7 +1265,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1257
1265
  <button class="filter-chip active" data-role="" onclick="setRoleFilter(this,'')" data-i18n="filter.all">All</button>
1258
1266
  <button class="filter-chip" data-role="user" onclick="setRoleFilter(this,'user')">User</button>
1259
1267
  <button class="filter-chip" data-role="assistant" onclick="setRoleFilter(this,'assistant')">Assistant</button>
1260
- <button class="filter-chip" data-role="system" onclick="setRoleFilter(this,'system')">System</button>
1261
1268
  <span class="filter-sep"></span>
1262
1269
  <select id="filterSort" class="filter-select" onchange="applyFilters()">
1263
1270
  <option value="newest" data-i18n="filter.newest">Newest first</option>
@@ -1290,7 +1297,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1290
1297
  <button class="filter-chip" data-task-status="active" onclick="setTaskStatusFilter(this,'active')" data-i18n="tasks.status.active">Active</button>
1291
1298
  <button class="filter-chip" data-task-status="completed" onclick="setTaskStatusFilter(this,'completed')" data-i18n="tasks.status.completed">Completed</button>
1292
1299
  <button class="filter-chip" data-task-status="skipped" onclick="setTaskStatusFilter(this,'skipped')" data-i18n="tasks.status.skipped">Skipped</button>
1293
- <select id="taskSearchScope" class="scope-select" onchange="onTaskScopeChange()">
1300
+ <select id="taskSearchScope" class="scope-select" onchange="onTaskScopeChange()" style="display:none">
1294
1301
  <option value="local" data-i18n="scope.thisAgent">This Agent</option>
1295
1302
  <option value="allLocal" data-i18n="scope.thisDevice">This Device</option>
1296
1303
  <option value="hub" data-i18n="scope.hub">Team</option>
@@ -1332,13 +1339,13 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1332
1339
  <div class="search-bar">
1333
1340
  <span class="search-icon">🔍</span>
1334
1341
  <input type="text" id="skillSearchInput" placeholder="Search skills..." data-i18n-ph="skills.search.placeholder" oninput="debounceSkillSearch()">
1335
- <select id="skillSearchScope" class="scope-select" onchange="onSkillScopeChange()">
1342
+ <select id="skillSearchScope" class="scope-select" onchange="onSkillScopeChange()" style="display:none">
1336
1343
  <option value="local" data-i18n="scope.thisAgent">This Agent</option>
1337
1344
  <option value="allLocal" data-i18n="scope.thisDevice">This Device</option>
1338
1345
  <option value="hub" data-i18n="scope.hub">Team</option>
1339
1346
  </select>
1340
1347
  </div>
1341
- <div class="search-meta" id="skillSearchMeta"></div>
1348
+ <div class="search-meta" id="skillSearchMeta" style="display:none"></div>
1342
1349
  <div class="tasks-header">
1343
1350
  <div class="tasks-stats">
1344
1351
  <div class="tasks-stat"><span class="tasks-stat-value" id="skillsTotalCount">-</span><span class="tasks-stat-label" data-i18n="skills.total">Total Skills</span></div>
@@ -1352,8 +1359,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1352
1359
  <button class="filter-chip" data-skill-status="active" onclick="setSkillStatusFilter(this,'active')" data-i18n="skills.filter.active">Active</button>
1353
1360
  <button class="filter-chip" data-skill-status="draft" onclick="setSkillStatusFilter(this,'draft')" data-i18n="skills.filter.draft">Draft</button>
1354
1361
  <button class="filter-chip" data-skill-status="archived" onclick="setSkillStatusFilter(this,'archived')" data-i18n="skills.filter.archived">Archived</button>
1355
- <span class="filter-sep"></span>
1356
- <select id="skillVisibilityFilter" class="filter-select" onchange="loadSkills()">
1362
+ <select id="skillVisibilityFilter" class="filter-select" onchange="loadSkills()" style="display:none">
1357
1363
  <option value="" data-i18n="filter.allvisibility">All visibility</option>
1358
1364
  <option value="public" data-i18n="filter.public">Public</option>
1359
1365
  <option value="private" data-i18n="filter.private">Private</option>
@@ -1440,7 +1446,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1440
1446
  <select id="logToolFilter" onchange="onLogFilterChange()" style="font-size:12px;padding:4px 8px;border-radius:6px;border:1px solid var(--border);background:var(--card);color:var(--text);min-width:120px">
1441
1447
  <option value="" data-i18n="logs.allTools">All Tools</option>
1442
1448
  </select>
1443
- <button class="btn btn-sm btn-ghost" onclick="loadLogs()" style="font-size:12px">\u21BB <span data-i18n="logs.refresh">Refresh</span></button>
1449
+
1444
1450
  </div>
1445
1451
  <div class="logs-toolbar-right">
1446
1452
  <input type="checkbox" id="logAutoRefresh" style="display:none">
@@ -1636,9 +1642,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1636
1642
  </div>
1637
1643
  </div>
1638
1644
  <div class="settings-card-body">
1639
- <!-- team setup guide (inside Hub card) -->
1645
+ <!-- team setup guide (inside Hub card) — always visible when sharing is not configured -->
1640
1646
  <div class="team-guide" id="teamSetupGuide">
1641
- <button class="team-guide-dismiss" onclick="dismissTeamGuide()" title="Dismiss">&times;</button>
1642
1647
  <div class="team-guide-title">\u{1F680} <span data-i18n="guide.title">Get Started with Team Collaboration</span></div>
1643
1648
  <div class="team-guide-subtitle" data-i18n="guide.subtitle">MemOS supports team memory sharing. Choose one of the following options to enable collaboration, or continue using local-only mode.</div>
1644
1649
  <div class="team-guide-options">
@@ -1652,7 +1657,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1652
1657
  <li><span data-i18n="guide.join.s1">Ask your team admin for the Server Address and Team Token</span></li>
1653
1658
  <li><span data-i18n="guide.join.s2">Enable sharing above, select "Client" mode</span></li>
1654
1659
  <li><span data-i18n="guide.join.s3">Fill in Server Address and Team Token, click "Test Connection"</span></li>
1655
- <li><span data-i18n="guide.join.s4">Save settings and restart the OpenClaw gateway (page refreshes automatically)</span></li>
1660
+ <li><span data-i18n="guide.join.s4">Click "Save & Apply" the service restarts automatically (page refreshes)</span></li>
1656
1661
  </ol>
1657
1662
  <button class="btn-guide" onclick="guideGoToHub('client')" data-i18n="guide.join.btn">\u2192 Configure Client Mode</button>
1658
1663
  </div>
@@ -1664,7 +1669,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1664
1669
  <div class="team-guide-opt-desc" data-i18n="guide.hub.desc">Be the team server. Run it on this device so others can connect and share memories with you.</div>
1665
1670
  <ol class="team-guide-steps">
1666
1671
  <li><span data-i18n="guide.hub.s1">Enable sharing above, select "Server" mode</span></li>
1667
- <li><span data-i18n="guide.hub.s2">Set a team name, save settings, and restart the gateway (page refreshes automatically)</span></li>
1672
+ <li><span data-i18n="guide.hub.s2">Set a team name, click "Save & Apply" the service restarts automatically</span></li>
1668
1673
  <li><span data-i18n="guide.hub.s3">Share the Server Address and Team Token with your team members</span></li>
1669
1674
  <li><span data-i18n="guide.hub.s4">Approve join requests in the Admin Panel</span></li>
1670
1675
  </ol>
@@ -1725,7 +1730,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1725
1730
  <div style="font-weight:700;color:var(--text);margin-bottom:4px" data-i18n="settings.hub.clientSteps.title">Quick Setup (3 steps)</div>
1726
1731
  <div><span style="color:var(--accent)">1.</span> <span data-i18n="settings.hub.clientSteps.s1">Ask your team admin for the Server Address and Team Token</span></div>
1727
1732
  <div><span style="color:var(--accent)">2.</span> <span data-i18n="settings.hub.clientSteps.s2">Fill them in below, click "Test Connection" to verify</span></div>
1728
- <div><span style="color:var(--accent)">3.</span> <span data-i18n="settings.hub.clientSteps.s3">Click "Save Settings", then restart OpenClaw gateway (page refreshes automatically)</span></div>
1733
+ <div><span style="color:var(--accent)">3.</span> <span data-i18n="settings.hub.clientSteps.s3">Click "Save & Apply" the service will restart and page refreshes automatically</span></div>
1729
1734
  </div>
1730
1735
  <div class="settings-grid">
1731
1736
  <div class="settings-field full-width">
@@ -2082,8 +2087,8 @@ const I18N={
2082
2087
  'skills.load.error':'Failed to load skills',
2083
2088
  'skills.hub.title':'\u{1F310} Team Skills',
2084
2089
  'scope.local':'Local',
2085
- 'scope.thisAgent':'This Agent',
2086
- 'scope.thisDevice':'This Device',
2090
+ 'scope.thisAgent':'This Agent Only',
2091
+ 'scope.thisDevice':'All Local Agents',
2087
2092
  'scope.group':'Group',
2088
2093
  'scope.all':'All',
2089
2094
  'skills.visibility.public':'Shared Locally',
@@ -2121,6 +2126,13 @@ const I18N={
2121
2126
  'notif.userJoin':'New user requests to join the team',
2122
2127
  'notif.userOnline':'User came online',
2123
2128
  'notif.userOffline':'User went offline',
2129
+ 'notif.userLeft':'User has left the team',
2130
+ 'notif.membershipApproved':'Your team join request has been approved',
2131
+ 'notif.membershipRejected':'Your team join request has been declined',
2132
+ 'notif.membershipRemoved':'You have been removed from the team by the admin',
2133
+ 'notif.hubShutdown':'The team server has been shut down',
2134
+ 'notif.rolePromoted':'You have been promoted to admin',
2135
+ 'notif.roleDemoted':'You have been changed to member',
2124
2136
  'notif.clearAll':'Clear all',
2125
2137
  'notif.timeAgo.just':'just now',
2126
2138
  'notif.timeAgo.min':'{n}m ago',
@@ -2129,7 +2141,7 @@ const I18N={
2129
2141
  'stat.memories':'Memories',
2130
2142
  'stat.sessions':'Sessions',
2131
2143
  'stat.embeddings':'Embeddings',
2132
- 'stat.days':'Days',
2144
+ 'stat.agents':'Agents',
2133
2145
  'stat.active':'active',
2134
2146
  'stat.deduped':'deduped',
2135
2147
  'sidebar.sessions':'Sessions',
@@ -2228,6 +2240,9 @@ const I18N={
2228
2240
  'logs.recall.noHits':'No matching memories',
2229
2241
  'logs.recall.noneRelevant':'LLM filter: none relevant',
2230
2242
  'logs.recall.more':'{n} more...',
2243
+ 'recall.origin.localShared':'Local Shared',
2244
+ 'recall.origin.hubMemory':'Team Cache',
2245
+ 'recall.origin.hubRemote':'Team',
2231
2246
  'tab.import':'\u{1F4E5} Import',
2232
2247
  'tab.settings':'\u2699 Settings',
2233
2248
  'settings.modelconfig':'Model Configuration',
@@ -2268,12 +2283,12 @@ const I18N={
2268
2283
  'settings.test.ok':'Connected',
2269
2284
  'settings.test.fail':'Failed',
2270
2285
  'settings.session.expired':'Session expired, please refresh the page to log in again',
2271
- 'settings.save':'Save Settings',
2286
+ 'settings.save':'Save & Apply',
2272
2287
  'settings.reset':'Reset',
2273
2288
  'settings.saved':'Saved',
2274
- 'settings.restart.hint':'Some changes require restarting the OpenClaw gateway to take effect.',
2275
- 'settings.restart.autoRefresh':'Page will refresh automatically after the gateway restarts...',
2276
- 'settings.restart.waiting':'Configuration saved. Waiting for gateway to restart...',
2289
+ 'settings.restart.hint':'Changes will take effect after the service restarts automatically.',
2290
+ 'settings.restart.autoRefresh':'Service restarting, page will refresh automatically...',
2291
+ 'settings.restart.waiting':'Configuration saved. Service is restarting...',
2277
2292
  'settings.save.fail':'Failed to save settings',
2278
2293
  'settings.save.emb.required':'Embedding model is required. Please configure an embedding model before saving.',
2279
2294
  'settings.save.emb.fail':'Embedding model test failed, cannot save',
@@ -2400,16 +2415,16 @@ const I18N={
2400
2415
  'settings.hub.tokenCopied':'Team Token copied!',
2401
2416
  'settings.hub.hubSteps.title':'Quick Setup (3 steps)',
2402
2417
  'settings.hub.hubSteps.s1':'Fill in Team Name below (or keep default)',
2403
- 'settings.hub.hubSteps.s2':'Click "Save Settings", then restart OpenClaw gateway',
2418
+ 'settings.hub.hubSteps.s2':'Click "Save & Apply" the service will restart automatically',
2404
2419
  'settings.hub.hubSteps.s3':'Share the Server Address and Team Token below with your team members',
2405
2420
  'settings.hub.clientSteps.title':'Quick Setup (3 steps)',
2406
2421
  'settings.hub.clientSteps.s1':'Ask your team admin for the Server Address and Team Token',
2407
2422
  'settings.hub.clientSteps.s2':'Fill them in below, click "Test Connection" to verify',
2408
- 'settings.hub.clientSteps.s3':'Click "Save Settings", then restart OpenClaw gateway (page refreshes automatically)',
2423
+ 'settings.hub.clientSteps.s3':'Click "Save & Apply" the service will restart and page refreshes automatically',
2409
2424
  'settings.hub.shareInfo.title':'Share this info with your team members:',
2410
2425
  'settings.hub.shareInfo.yourIP':'your-IP',
2411
2426
  'settings.hub.shareInfo.clickCopy':'Click to copy',
2412
- 'settings.hub.restartAlert':'Team sharing config saved! Please restart the OpenClaw gateway for changes to take effect.\\n\\nRun: openclaw gateway stop && openclaw gateway start',
2427
+ 'settings.hub.restartAlert':'Team sharing config saved! The service will restart automatically to apply changes.',
2413
2428
  'settings.hub.hubAddress':'Server Address',
2414
2429
  'settings.hub.hubAddress.hint':'Team server address, e.g. 192.168.1.100:18800',
2415
2430
  'settings.hub.teamTokenClient':'Team Token',
@@ -2430,6 +2445,10 @@ const I18N={
2430
2445
  'sidebar.hub':'\u{1F310} Team Sharing',
2431
2446
  'sharing.sidebar.connected':'Connected',
2432
2447
  'sharing.sidebar.disconnected':'Disconnected',
2448
+ 'sharing.sidebar.hubRunning':'Hub Running',
2449
+ 'sharing.sidebar.teamName':'Team',
2450
+ 'sharing.sidebar.members':'Members',
2451
+ 'sharing.sidebar.online':'online',
2433
2452
  'sharing.sidebar.pending':'Pending Approval',
2434
2453
  'sharing.sidebar.rejected':'Rejected',
2435
2454
  'sharing.sidebar.starting':'Starting...',
@@ -2439,11 +2458,21 @@ const I18N={
2439
2458
  'sharing.sidebar.targetHub':'Team Server:',
2440
2459
  'sharing.pendingApproval.hint':'Your join request has been submitted. Please wait for the team admin to approve.',
2441
2460
  'sharing.rejected.hint':'Your join request was rejected by the team admin. Please contact the admin or retry.',
2461
+ 'sharing.removed.hint':'You have been removed from the team by the admin. You can re-apply to join.',
2462
+ 'sharing.joinTeam':'Join Team',
2463
+ 'sharing.joinSent.pending':'Join request sent! Waiting for admin approval.',
2464
+ 'sharing.joinSent.active':'Successfully joined the team!',
2442
2465
  'sharing.retryJoin':'Retry Join',
2443
2466
  'sharing.retryJoin.hint':'Clears local data and re-submits the join request',
2444
2467
  'sharing.retryJoin.confirm':'This will clear your current connection and re-submit a join request. Continue?',
2468
+ 'sharing.leaveTeam':'Leave Team',
2469
+ 'sharing.leaveTeam.confirm':'You are about to leave team "{team}".\\n\\nWhat will happen:\\n\\u2022 You will disconnect from the team server\\n\\u2022 The team admin will be notified that you left\\n\\u2022 You will no longer receive shared memories, tasks, or skills\\n\\u2022 Your local data is preserved and not affected\\n\\u2022 You can rejoin later if the admin approves\\n\\nAre you sure?',
2470
+ 'sharing.leaveTeam.success':'You have left the team. Sharing has been disabled.',
2471
+ 'sharing.leaveTeam.fail':'Failed to leave team',
2472
+ 'sharing.team.default':'the team',
2445
2473
  'sharing.retryJoin.success':'Join request re-submitted. Waiting for admin approval.',
2446
2474
  'sharing.retryJoin.fail':'Failed to retry join',
2475
+ 'sharing.ownerRemoved':'(removed)',
2447
2476
  'sharing.cannotJoinSelf':'Cannot join your own server. Please enter a remote server address.',
2448
2477
  'scope.hub':'Team',
2449
2478
  'memory.detail.title':'Memory Detail',
@@ -2494,6 +2523,7 @@ const I18N={
2494
2523
  'admin.editName':'Edit Name',
2495
2524
  'admin.lastAdminHint':'Last admin — cannot remove or demote',
2496
2525
  'admin.ownerHint':'Hub owner — cannot be demoted or removed',
2526
+ 'admin.selfHint':'This is you',
2497
2527
  'admin.editNamePrompt':'Enter new username:',
2498
2528
  'confirm.promoteAdmin':'Promote this user to admin? They will be able to manage all team members and resources.',
2499
2529
  'confirm.demoteMember':'Demote this admin to member?',
@@ -2555,6 +2585,8 @@ const I18N={
2555
2585
  'toast.userApproved':'User approved',
2556
2586
  'sharing.approved.toast':'Your join request has been approved!',
2557
2587
  'sharing.rejected.toast':'Your join request was rejected by the admin.',
2588
+ 'sharing.hubOffline.toast':'Team server is offline. Will reconnect automatically when it comes back.',
2589
+ 'sharing.hubReconnected.toast':'Team server is back online! Connection restored.',
2558
2590
  'toast.userRejected':'User rejected',
2559
2591
  'toast.approveFail':'Approve failed',
2560
2592
  'toast.rejectFail':'Reject failed',
@@ -2647,9 +2679,9 @@ const I18N={
2647
2679
  'share.status.agents':'Local',
2648
2680
  'share.status.hub':'Team',
2649
2681
  'share.scope.title':'Sharing Scope',
2650
- 'share.scope.private':'This Agent Only',
2651
- 'share.scope.local':'All Local Agents',
2652
- 'share.scope.team':'Team',
2682
+ 'share.scope.private':'Private',
2683
+ 'share.scope.local':'Local Shared',
2684
+ 'share.scope.team':'Team Shared',
2653
2685
  'share.scope.current':'Current',
2654
2686
  'share.scope.teamDisabled':'Not connected to team server',
2655
2687
  'share.scope.teamIncludes':'Includes visibility to all local agents',
@@ -2714,9 +2746,10 @@ const I18N={
2714
2746
  'update.dismiss':'Dismiss',
2715
2747
  'sharing.disable.confirm.hub':'You are about to shut down the team server.\\n\\nWhat will happen:\\n\\u2022 All connected team members will be disconnected\\n\\u2022 They will no longer be able to sync memories, tasks, or skills\\n\\u2022 Shared data is preserved and will be available when you re-enable\\n\\nAre you sure?',
2716
2748
  'sharing.disable.confirm.client':'You are about to disconnect from the team.\\n\\nWhat will happen:\\n\\u2022 You will no longer receive shared memories, tasks, or skills from the team\\n\\u2022 Your local data is preserved and will not be affected\\n\\u2022 You can reconnect later by re-enabling sharing\\n\\nAre you sure?',
2717
- 'sharing.disable.restartAlert':'Sharing has been disabled. Please restart the OpenClaw gateway for the change to take effect.\\n\\nRun: openclaw gateway stop && openclaw gateway start',
2718
- 'sharing.switch.hubToClient':'You are about to switch from Server to Client mode.\\n\\nWhat will happen:\\n\\u2022 The Hub server will shut down after restart\\n\\u2022 All connected team members will be disconnected\\n\\u2022 Shared data on the Hub is preserved for future use\\n\\u2022 You will join the specified remote team as a client\\n\\nAre you sure?',
2719
- 'sharing.switch.clientToHub':'You are about to switch from Client to Server mode.\\n\\nWhat will happen:\\n\\u2022 You will disconnect from the current team\\n\\u2022 A new Hub server will start after restart\\n\\u2022 Your local data is not affected\\n\\nAre you sure?',
2749
+ 'sharing.disable.restartAlert':'Sharing has been disabled. The service will restart automatically to apply the change.',
2750
+ 'sharing.switch.hubToClient':'You are about to switch from Server to Client mode.\\n\\nWhat will happen:\\n\\u2022 The Hub server will shut down after the service restarts\\n\\u2022 All connected team members will be disconnected\\n\\u2022 Shared data on the Hub is preserved for future use\\n\\u2022 You will join the specified remote team as a client\\n\\nAre you sure?',
2751
+ 'sharing.switch.clientToHub':'You are about to switch from Client to Server mode.\\n\\nWhat will happen:\\n\\u2022 You will disconnect from the current team\\n\\u2022 A new Hub server will start after the service restarts\\n\\u2022 Your local data is not affected\\n\\nAre you sure?',
2752
+ 'sharing.switch.hubAddress':'You are about to leave the current team and join a different one.\\n\\nWhat will happen:\\n\\u2022 You will disconnect from the current team server\\n\\u2022 The current team admin will be notified that you left\\n\\u2022 You will join the new team server as a new member\\n\\u2022 Your local data is not affected\\n\\nAre you sure?',
2720
2753
  'admin.notEnabled.title':'Team sharing is not enabled',
2721
2754
  'admin.notEnabled.desc':'The Admin Panel is used to manage team members, shared memories, tasks, and skills. To use this feature, you need to enable team sharing first.',
2722
2755
  'admin.notEnabled.setupHub':'Set Up as Team Server',
@@ -2734,12 +2767,12 @@ const I18N={
2734
2767
  'guide.join.s1':'Ask your team admin for the Server Address and Team Token',
2735
2768
  'guide.join.s2':'Go to Settings \u2192 Team Sharing, enable sharing, select "Client" mode',
2736
2769
  'guide.join.s3':'Fill in Server Address and Team Token, click "Test Connection"',
2737
- 'guide.join.s4':'Save settings and restart the OpenClaw gateway (page refreshes automatically)',
2770
+ 'guide.join.s4':'Click "Save & Apply" the service restarts automatically (page refreshes)',
2738
2771
  'guide.join.btn':'\u2192 Configure Client Mode',
2739
2772
  'guide.hub.title':'Start Your Own Team Server',
2740
2773
  'guide.hub.desc':'Be the team server. Run it on this device so others can connect and share memories with you.',
2741
2774
  'guide.hub.s1':'Go to Settings \u2192 Team Sharing, enable sharing, select "Server" mode',
2742
- 'guide.hub.s2':'Set a team name, save settings, and restart the gateway (page refreshes automatically)',
2775
+ 'guide.hub.s2':'Set a team name, click "Save & Apply" the service restarts automatically',
2743
2776
  'guide.hub.s3':'Share the Server Address and Team Token with your team members',
2744
2777
  'guide.hub.s4':'Approve join requests in the Admin Panel',
2745
2778
  'guide.hub.btn':'\u2192 Configure Server Mode'
@@ -2791,8 +2824,8 @@ const I18N={
2791
2824
  'skills.load.error':'加载技能失败',
2792
2825
  'skills.hub.title':'\u{1F310} 团队共享技能',
2793
2826
  'scope.local':'本地',
2794
- 'scope.thisAgent':'当前智能体',
2795
- 'scope.thisDevice':'本机全部',
2827
+ 'scope.thisAgent':'仅本智能体',
2828
+ 'scope.thisDevice':'本机所有智能体',
2796
2829
  'scope.group':'团队',
2797
2830
  'scope.all':'全部',
2798
2831
  'skills.visibility.public':'本机共享',
@@ -2830,6 +2863,13 @@ const I18N={
2830
2863
  'notif.userJoin':'有新用户申请加入团队',
2831
2864
  'notif.userOnline':'用户上线了',
2832
2865
  'notif.userOffline':'用户下线了',
2866
+ 'notif.userLeft':'用户已退出团队',
2867
+ 'notif.membershipApproved':'你的团队加入申请已通过',
2868
+ 'notif.membershipRejected':'你的团队加入申请已被拒绝',
2869
+ 'notif.membershipRemoved':'你已被管理员移出团队',
2870
+ 'notif.hubShutdown':'团队服务已关闭',
2871
+ 'notif.rolePromoted':'你已被提升为管理员',
2872
+ 'notif.roleDemoted':'你已被设为普通成员',
2833
2873
  'notif.clearAll':'清除全部',
2834
2874
  'notif.timeAgo.just':'刚刚',
2835
2875
  'notif.timeAgo.min':'{n}分钟前',
@@ -2838,7 +2878,7 @@ const I18N={
2838
2878
  'stat.memories':'记忆',
2839
2879
  'stat.sessions':'会话',
2840
2880
  'stat.embeddings':'嵌入',
2841
- 'stat.days':'天数',
2881
+ 'stat.agents':'智能体',
2842
2882
  'stat.active':'活跃',
2843
2883
  'stat.deduped':'已去重',
2844
2884
  'sidebar.sessions':'会话列表',
@@ -2937,6 +2977,9 @@ const I18N={
2937
2977
  'logs.recall.noHits':'未匹配到记忆',
2938
2978
  'logs.recall.noneRelevant':'LLM 过滤:无相关记忆',
2939
2979
  'logs.recall.more':'还有 {n} 条...',
2980
+ 'recall.origin.localShared':'本机共享',
2981
+ 'recall.origin.hubMemory':'团队缓存',
2982
+ 'recall.origin.hubRemote':'团队',
2940
2983
  'tab.import':'\u{1F4E5} 导入',
2941
2984
  'tab.settings':'\u2699 设置',
2942
2985
  'settings.modelconfig':'模型配置',
@@ -2977,12 +3020,12 @@ const I18N={
2977
3020
  'settings.test.ok':'连接成功',
2978
3021
  'settings.test.fail':'连接失败',
2979
3022
  'settings.session.expired':'登录已过期,请刷新页面重新登录',
2980
- 'settings.save':'保存设置',
3023
+ 'settings.save':'保存并应用',
2981
3024
  'settings.reset':'重置',
2982
3025
  'settings.saved':'已保存',
2983
- 'settings.restart.hint':'部分设置修改后需要重启 OpenClaw 网关才能生效。',
2984
- 'settings.restart.autoRefresh':'网关重启后页面将自动刷新...',
2985
- 'settings.restart.waiting':'配置已保存,正在等待网关重启...',
3026
+ 'settings.restart.hint':'修改将在服务自动重启后生效。',
3027
+ 'settings.restart.autoRefresh':'服务重启中,页面将自动刷新...',
3028
+ 'settings.restart.waiting':'配置已保存,服务正在重启...',
2986
3029
  'settings.save.fail':'保存设置失败',
2987
3030
  'settings.save.emb.required':'嵌入模型为必填项,请先配置嵌入模型再保存。',
2988
3031
  'settings.save.emb.fail':'嵌入模型测试失败,无法保存',
@@ -3109,16 +3152,16 @@ const I18N={
3109
3152
  'settings.hub.tokenCopied':'团队令牌已复制!',
3110
3153
  'settings.hub.hubSteps.title':'快速配置(3 步)',
3111
3154
  'settings.hub.hubSteps.s1':'填写下方团队名称(或保持默认)',
3112
- 'settings.hub.hubSteps.s2':'点击"保存设置",然后重启 OpenClaw 网关',
3155
+ 'settings.hub.hubSteps.s2':'点击「保存并应用」,服务将自动重启',
3113
3156
  'settings.hub.hubSteps.s3':'将下方的服务器地址和团队令牌分享给团队成员',
3114
3157
  'settings.hub.clientSteps.title':'快速配置(3 步)',
3115
3158
  'settings.hub.clientSteps.s1':'向团队管理员获取服务器地址和团队令牌',
3116
3159
  'settings.hub.clientSteps.s2':'填入下方,点击"测试连接"验证连通性',
3117
- 'settings.hub.clientSteps.s3':'点击「保存设置」,然后重启 OpenClaw 网关(页面会自动刷新)',
3160
+ 'settings.hub.clientSteps.s3':'点击「保存并应用」,服务将自动重启(页面会自动刷新)',
3118
3161
  'settings.hub.shareInfo.title':'请将以下信息分享给团队成员:',
3119
3162
  'settings.hub.shareInfo.yourIP':'你的IP',
3120
3163
  'settings.hub.shareInfo.clickCopy':'点击复制',
3121
- 'settings.hub.restartAlert':'团队共享配置已保存!请重启 OpenClaw 网关使配置生效。\\n\\n执行命令:openclaw gateway stop && openclaw gateway start',
3164
+ 'settings.hub.restartAlert':'团队共享配置已保存!服务将自动重启以应用更改。',
3122
3165
  'settings.hub.hubAddress':'服务器地址',
3123
3166
  'settings.hub.hubAddress.hint':'团队服务器地址,如 192.168.1.100:18800',
3124
3167
  'settings.hub.teamTokenClient':'团队令牌',
@@ -3139,6 +3182,10 @@ const I18N={
3139
3182
  'sidebar.hub':'\u{1F310} 团队共享',
3140
3183
  'sharing.sidebar.connected':'已连接',
3141
3184
  'sharing.sidebar.disconnected':'已断开',
3185
+ 'sharing.sidebar.hubRunning':'服务运行中',
3186
+ 'sharing.sidebar.teamName':'团队',
3187
+ 'sharing.sidebar.members':'成员',
3188
+ 'sharing.sidebar.online':'在线',
3142
3189
  'sharing.sidebar.pending':'等待审核',
3143
3190
  'sharing.sidebar.rejected':'已拒绝',
3144
3191
  'sharing.sidebar.starting':'启动中...',
@@ -3148,11 +3195,21 @@ const I18N={
3148
3195
  'sharing.sidebar.targetHub':'团队服务器:',
3149
3196
  'sharing.pendingApproval.hint':'加入申请已提交,请等待团队管理员审核通过。',
3150
3197
  'sharing.rejected.hint':'您的加入申请已被团队管理员拒绝,请联系管理员或重新申请。',
3198
+ 'sharing.removed.hint':'您已被管理员从团队中移除,可以重新申请加入。',
3199
+ 'sharing.joinTeam':'加入团队',
3200
+ 'sharing.joinSent.pending':'加入申请已发送,等待管理员审批。',
3201
+ 'sharing.joinSent.active':'成功加入团队!',
3151
3202
  'sharing.retryJoin':'重新申请',
3152
3203
  'sharing.retryJoin.hint':'清除本地连接数据并重新提交加入申请',
3153
3204
  'sharing.retryJoin.confirm':'这将清除当前连接数据并重新提交加入申请,是否继续?',
3205
+ 'sharing.leaveTeam':'退出团队',
3206
+ 'sharing.leaveTeam.confirm':'你即将退出团队「{team}」。\\n\\n退出后将会:\\n\\u2022 断开与团队服务器的连接\\n\\u2022 团队管理员会收到你退出的通知\\n\\u2022 你将无法再接收团队共享的记忆、任务和技能\\n\\u2022 你的本地数据不受影响,会完整保留\\n\\u2022 之后可以重新申请加入(需管理员审批)\\n\\n确定要退出吗?',
3207
+ 'sharing.leaveTeam.success':'你已退出团队,团队共享已关闭。',
3208
+ 'sharing.leaveTeam.fail':'退出团队失败',
3209
+ 'sharing.team.default':'该团队',
3154
3210
  'sharing.retryJoin.success':'加入申请已重新提交,请等待管理员审核。',
3155
3211
  'sharing.retryJoin.fail':'重新申请失败',
3212
+ 'sharing.ownerRemoved':'(已移除)',
3156
3213
  'sharing.cannotJoinSelf':'不能加入自己的服务端,请输入远程服务器地址。',
3157
3214
  'scope.hub':'团队',
3158
3215
  'memory.detail.title':'记忆详情',
@@ -3203,6 +3260,7 @@ const I18N={
3203
3260
  'admin.editName':'编辑名称',
3204
3261
  'admin.lastAdminHint':'唯一管理员 — 无法删除或降级',
3205
3262
  'admin.ownerHint':'Hub 创建者 — 不可降级或移除',
3263
+ 'admin.selfHint':'这是你自己',
3206
3264
  'admin.editNamePrompt':'请输入新用户名:',
3207
3265
  'confirm.promoteAdmin':'确定要将此用户提升为管理员吗?管理员可以管理所有团队成员和资源。',
3208
3266
  'confirm.demoteMember':'确定要将此管理员降为普通成员吗?',
@@ -3264,6 +3322,8 @@ const I18N={
3264
3322
  'toast.userApproved':'用户已批准',
3265
3323
  'sharing.approved.toast':'您的加入申请已通过审核!',
3266
3324
  'sharing.rejected.toast':'您的加入申请已被管理员拒绝。',
3325
+ 'sharing.hubOffline.toast':'团队服务已离线,恢复后将自动重新连接。',
3326
+ 'sharing.hubReconnected.toast':'团队服务已恢复上线,连接已自动恢复!',
3267
3327
  'toast.userRejected':'用户已拒绝',
3268
3328
  'toast.approveFail':'批准失败',
3269
3329
  'toast.rejectFail':'拒绝失败',
@@ -3356,9 +3416,9 @@ const I18N={
3356
3416
  'share.status.agents':'本机',
3357
3417
  'share.status.hub':'团队',
3358
3418
  'share.scope.title':'共享范围',
3359
- 'share.scope.private':'仅本智能体',
3360
- 'share.scope.local':'本机所有智能体',
3361
- 'share.scope.team':'团队',
3419
+ 'share.scope.private':'私有',
3420
+ 'share.scope.local':'本机共享',
3421
+ 'share.scope.team':'团队共享',
3362
3422
  'share.scope.current':'当前',
3363
3423
  'share.scope.teamDisabled':'未连接团队服务器',
3364
3424
  'share.scope.teamIncludes':'包含本机所有智能体的可见性',
@@ -3423,9 +3483,10 @@ const I18N={
3423
3483
  'update.dismiss':'关闭',
3424
3484
  'sharing.disable.confirm.hub':'你即将关闭团队服务。\\n\\n关闭后将会:\\n\\u2022 所有已连接的团队成员将断开连接\\n\\u2022 他们将无法继续同步记忆、任务和技能\\n\\u2022 已共享的数据会保留,重新开启后仍可使用\\n\\n确定要关闭吗?',
3425
3485
  'sharing.disable.confirm.client':'你即将断开与团队的连接。\\n\\n断开后将会:\\n\\u2022 你将无法再接收团队共享的记忆、任务和技能\\n\\u2022 你的本地数据不受影响,会完整保留\\n\\u2022 之后可以随时重新开启共享来恢复连接\\n\\n确定要断开吗?',
3426
- 'sharing.disable.restartAlert':'共享已关闭。请重启 OpenClaw 网关使更改生效。\\n\\n执行命令:openclaw gateway stop && openclaw gateway start',
3427
- 'sharing.switch.hubToClient':'你即将从服务端模式切换为客户端模式。\\n\\n切换后将会:\\n\\u2022 Hub 服务将在重启后关闭\\n\\u2022 所有已连接的团队成员将断开连接\\n\\u2022 Hub 上的共享数据会保留,以后可恢复使用\\n\\u2022 你将作为客户端加入指定的远程团队\\n\\n确定要切换吗?',
3428
- 'sharing.switch.clientToHub':'你即将从客户端模式切换为服务端模式。\\n\\n切换后将会:\\n\\u2022 你将断开与当前团队的连接\\n\\u2022 重启后将启动新的 Hub 服务\\n\\u2022 你的本地数据不受影响\\n\\n确定要切换吗?',
3486
+ 'sharing.disable.restartAlert':'共享已关闭,服务将自动重启以应用更改。',
3487
+ 'sharing.switch.hubToClient':'你即将从服务端模式切换为客户端模式。\\n\\n切换后将会:\\n\\u2022 Hub 服务将在服务重启后关闭\\n\\u2022 所有已连接的团队成员将断开连接\\n\\u2022 Hub 上的共享数据会保留,以后可恢复使用\\n\\u2022 你将作为客户端加入指定的远程团队\\n\\n确定要切换吗?',
3488
+ 'sharing.switch.clientToHub':'你即将从客户端模式切换为服务端模式。\\n\\n切换后将会:\\n\\u2022 你将断开与当前团队的连接\\n\\u2022 服务重启后将启动新的 Hub 服务\\n\\u2022 你的本地数据不受影响\\n\\n确定要切换吗?',
3489
+ 'sharing.switch.hubAddress':'你即将离开当前团队并加入新的团队。\\n\\n操作后将会:\\n\\u2022 你将断开与当前团队服务器的连接\\n\\u2022 当前团队管理员会收到你离开的通知\\n\\u2022 你将作为新成员加入新的团队服务器\\n\\u2022 你的本地数据不受影响\\n\\n确定要切换吗?',
3429
3490
  'admin.notEnabled.title':'团队共享尚未开启',
3430
3491
  'admin.notEnabled.desc':'管理面板用于管理团队成员、共享的记忆、任务和技能。使用此功能前,需要先开启团队共享。',
3431
3492
  'admin.notEnabled.setupHub':'配置为团队服务端',
@@ -3443,12 +3504,12 @@ const I18N={
3443
3504
  'guide.join.s1':'向团队管理员索取服务器地址和团队令牌',
3444
3505
  'guide.join.s2':'前往「设置 → 团队共享」,开启共享,选择「客户端」模式',
3445
3506
  'guide.join.s3':'填写服务器地址和团队令牌,点击「测试连接」',
3446
- 'guide.join.s4':'保存设置并重启 OpenClaw 网关(页面会自动刷新)',
3507
+ 'guide.join.s4':'点击「保存并应用」,服务将自动重启(页面会自动刷新)',
3447
3508
  'guide.join.btn':'\u2192 配置客户端模式',
3448
3509
  'guide.hub.title':'自建团队服务',
3449
3510
  'guide.hub.desc':'将本机作为团队服务端,让其他成员连接过来共享记忆。',
3450
3511
  'guide.hub.s1':'前往「设置 → 团队共享」,开启共享,选择「服务端」模式',
3451
- 'guide.hub.s2':'设置团队名称,保存设置后重启网关(页面会自动刷新)',
3512
+ 'guide.hub.s2':'设置团队名称,点击「保存并应用」,服务将自动重启',
3452
3513
  'guide.hub.s3':'将服务器地址和团队令牌分享给团队成员',
3453
3514
  'guide.hub.s4':'在管理面板中审批加入请求',
3454
3515
  'guide.hub.btn':'\u2192 配置服务端模式'
@@ -3554,12 +3615,28 @@ async function doReset(){
3554
3615
  }
3555
3616
 
3556
3617
  var _sharingRole='client';
3618
+ var _loadedClientHubAddress='';
3557
3619
  function _genToken(len){
3558
3620
  var a=new Uint8Array(len||18);crypto.getRandomValues(a);
3559
3621
  return btoa(String.fromCharCode.apply(null,a)).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,'');
3560
3622
  }
3561
- function onSharingToggle(){
3562
- var on=document.getElementById('cfgSharingEnabled').checked;
3623
+ async function onSharingToggle(){
3624
+ var chk=document.getElementById('cfgSharingEnabled');
3625
+ var on=chk.checked;
3626
+ if(!on && sharingStatusCache && sharingStatusCache.enabled){
3627
+ var prevRole=sharingStatusCache.role;
3628
+ var confirmMsg=prevRole==='hub'?t('sharing.disable.confirm.hub'):t('sharing.disable.confirm.client');
3629
+ if(!(await confirmModal(confirmMsg,{danger:true}))){
3630
+ chk.checked=true;
3631
+ return;
3632
+ }
3633
+ var cfg={sharing:{enabled:false,role:prevRole}};
3634
+ chk.disabled=true;
3635
+ var result=await doSaveConfig(cfg, null, 'hubSaved');
3636
+ chk.disabled=false;
3637
+ if(!result){chk.checked=true;return;}
3638
+ return;
3639
+ }
3563
3640
  document.getElementById('sharingConfigPanel').style.display=on?'block':'none';
3564
3641
  var pw=document.getElementById('sharingPanelsWrap');
3565
3642
  if(pw) pw.style.display=on?'':'none';
@@ -3577,20 +3654,25 @@ function selectSharingRole(role){
3577
3654
  var tp=document.getElementById('sharingTeamPanel');
3578
3655
  var ap=document.getElementById('sharingAdminPanel');
3579
3656
  if(role==='client'){
3580
- if(sp) sp.style.display='none';
3657
+ if(sp) { sp.style.display='none'; sp.innerHTML=''; }
3581
3658
  if(tp) tp.style.display='none';
3582
3659
  if(ap) ap.style.display='none';
3583
3660
  }else{
3584
- if(sp) sp.style.display='';
3661
+ if(sp) { sp.style.display='none'; sp.innerHTML=''; }
3585
3662
  if(tp) tp.style.display='';
3586
3663
  if(ap) ap.style.display='';
3587
3664
  }
3665
+ _lastSettingsFingerprint='';
3666
+ setTimeout(function(){ loadSharingStatus(true); },200);
3588
3667
  if(role==='hub'){
3589
3668
  var tk=document.getElementById('cfgHubTeamToken');
3590
3669
  if(!tk.value.trim()) tk.value=_genToken(18);
3591
3670
  var tn=document.getElementById('cfgHubTeamName');
3592
3671
  if(!tn.value.trim()) tn.value='My Team';
3593
3672
  }
3673
+ var card=document.getElementById('settingsSharingConfig');
3674
+ var saveBtn=card&&card.querySelector('.settings-actions .btn-primary');
3675
+ if(saveBtn&&typeof _hubSaveBtnLabel==='function') saveBtn.textContent=_hubSaveBtnLabel();
3594
3676
  }
3595
3677
  var _cachedLocalIP='';
3596
3678
  function updateHubShareInfo(){
@@ -3625,13 +3707,6 @@ async function testHubConnection(){
3625
3707
  if(!addr){result.innerHTML='<span style="color:var(--rose)">\u274C '+t('settings.hub.test.noAddr')+'</span>';return;}
3626
3708
  btn.disabled=true;result.innerHTML=t('settings.hub.test.testing');
3627
3709
  try{
3628
- var ipsData=await fetch('/api/local-ips').then(function(r){return r.json();});
3629
- var localAddrs=['127.0.0.1','localhost','0.0.0.0'].concat(ipsData.ips||[]);
3630
- var parsed=new URL(addr.indexOf('://')>-1?addr:'http://'+addr);
3631
- if(localAddrs.indexOf(parsed.hostname)>=0){
3632
- result.innerHTML='<span style="color:var(--rose)">\u274C '+t('sharing.cannotJoinSelf')+'</span>';
3633
- btn.disabled=false;return;
3634
- }
3635
3710
  }catch(e){}
3636
3711
  try{
3637
3712
  var url=addr.match(/^https?:\\/\\//)?addr:'http://'+addr;
@@ -3681,13 +3756,20 @@ function switchView(view){
3681
3756
  else if(view==='skills') loadSkills();
3682
3757
  else if(view==='analytics') loadMetrics();
3683
3758
  else if(view==='logs') loadLogs();
3684
- else if(view==='settings'){loadConfig();loadModelHealth();}
3759
+ else if(view==='settings'){loadConfig().then(function(){
3760
+ var sharingOn=document.getElementById('cfgSharingEnabled');
3761
+ var sharingNotEnabled=!sharingOn||!sharingOn.checked;
3762
+ if(sharingNotEnabled){
3763
+ switchSettingsTab('hub',document.querySelector('.settings-tab-btn[data-tab="hub"]'));
3764
+ }
3765
+ });loadModelHealth();}
3685
3766
  else if(view==='import'){if(!window._migrateRunning) migrateScan(false);}
3686
- else if(view==='admin'){loadAdminData();}
3767
+ else if(view==='admin'){_lastAdminFingerprint='';loadAdminData();}
3687
3768
  }
3688
3769
 
3689
3770
  function onMemoryScopeChange(){
3690
3771
  memorySearchScope=document.getElementById('memorySearchScope')?.value||'local';
3772
+ try{localStorage.setItem('memos_memorySearchScope',memorySearchScope);}catch(e){}
3691
3773
  currentPage=1;
3692
3774
  activeSession=null;activeRole='';
3693
3775
  _lastMemoriesFingerprint='';
@@ -3696,7 +3778,7 @@ function onMemoryScopeChange(){
3696
3778
  var ownerSel=document.getElementById('filterOwner');
3697
3779
  var filterBar=document.getElementById('filterBar');
3698
3780
  var dateFilter=document.querySelector('.date-filter');
3699
- if(ownerSel) ownerSel.style.display=(isHub||isLocal)?'none':'';
3781
+ if(ownerSel){ownerSel.style.display=(isHub||isLocal)?'none':'';if(isHub||isLocal)ownerSel.value='';}
3700
3782
  if(filterBar) filterBar.style.display=isHub?'none':'';
3701
3783
  if(dateFilter) dateFilter.style.display=isHub?'none':'';
3702
3784
  if(document.getElementById('searchInput').value.trim()) doSearch(document.getElementById('searchInput').value);
@@ -3721,6 +3803,13 @@ function onTaskScopeChange(){
3721
3803
 
3722
3804
  var _clientPendingPollTimer=null;
3723
3805
  var _lastSharingConnStatus='';
3806
+ function _updateScopeSelectorsVisibility(hubAvailable){
3807
+ var ids=['memorySearchScope','taskSearchScope','skillSearchScope'];
3808
+ for(var i=0;i<ids.length;i++){
3809
+ var el=document.getElementById(ids[i]);
3810
+ if(el) el.style.display=hubAvailable?'':'none';
3811
+ }
3812
+ }
3724
3813
  async function loadSharingStatus(forcePending){
3725
3814
  try{
3726
3815
  const r=await fetch('/api/sharing/status');
@@ -3733,19 +3822,36 @@ async function loadSharingStatus(forcePending){
3733
3822
  if(!d||!d.enabled){
3734
3823
  if(_clientPendingPollTimer){clearInterval(_clientPendingPollTimer);_clientPendingPollTimer=null;}
3735
3824
  _lastSharingConnStatus='';
3825
+ _updateScopeSelectorsVisibility(false);
3736
3826
  return;
3737
3827
  }
3738
3828
  var conn=d.connection||{};
3739
3829
  var curStatus=conn.rejected?'rejected':conn.pendingApproval?'pending':conn.connected?'connected':'none';
3740
- if(_lastSharingConnStatus==='pending'&&curStatus==='rejected'){
3830
+ var hubActive=d.role==='hub'||curStatus==='connected';
3831
+ _updateScopeSelectorsVisibility(hubActive);
3832
+ if(_lastSharingConnStatus==='pending'&&curStatus==='rejected'&&d.role==='client'){
3741
3833
  toast(t('sharing.rejected.toast'),'error');
3742
3834
  }
3743
- if(_lastSharingConnStatus==='pending'&&curStatus==='connected'){
3835
+ if(_lastSharingConnStatus==='pending'&&curStatus==='connected'&&d.role==='client'){
3744
3836
  toast(t('sharing.approved.toast'),'success');
3837
+ loadMemories();loadTasks();loadSkills();
3838
+ if(_notifSSE){_notifSSE.close();_notifSSE=null;_notifSSEConnected=false;}
3839
+ connectNotifSSE();
3840
+ loadNotifications();
3841
+ }
3842
+ if(_lastSharingConnStatus==='connected'&&curStatus==='none'&&d.role==='client'){
3843
+ toast(t('sharing.hubOffline.toast'),'error');
3844
+ }
3845
+ if(_lastSharingConnStatus==='none'&&curStatus==='connected'&&d.role==='client'){
3846
+ toast(t('sharing.hubReconnected.toast'),'success');
3847
+ loadMemories();loadTasks();loadSkills();
3848
+ if(_notifSSE){_notifSSE.close();_notifSSE=null;_notifSSEConnected=false;}
3849
+ connectNotifSSE();
3850
+ loadNotifications();
3745
3851
  }
3746
3852
  _lastSharingConnStatus=curStatus;
3747
3853
  if(curStatus==='pending'&&!_clientPendingPollTimer){
3748
- _clientPendingPollTimer=setInterval(function(){loadSharingStatus(false);},10000);
3854
+ _clientPendingPollTimer=setInterval(function(){loadSharingStatus(false);},5000);
3749
3855
  }
3750
3856
  if(curStatus!=='pending'&&_clientPendingPollTimer){
3751
3857
  clearInterval(_clientPendingPollTimer);
@@ -3755,6 +3861,7 @@ async function loadSharingStatus(forcePending){
3755
3861
  renderSharingSidebar(null);
3756
3862
  renderSharingSettings(null);
3757
3863
  updateTeamGuide(null);
3864
+ _updateScopeSelectorsVisibility(false);
3758
3865
  }
3759
3866
  }
3760
3867
 
@@ -3766,7 +3873,8 @@ function renderSharingSidebar(data){
3766
3873
  var badgeEl=document.getElementById('sharingSidebarConnBadge');
3767
3874
  if(!statusEl||!hintEl) return;
3768
3875
  var conn=data&&data.connection||{};
3769
- var fp=JSON.stringify({e:!!data&&!!data.enabled,r:data&&data.role,pa:!!conn.pendingApproval,rj:!!conn.rejected,c:!!conn.connected,u:conn.user&&conn.user.username,tn:conn.teamName,cc:!!data&&!!data.clientConfigured,hu:data&&data.hubUrl});
3876
+ var hs=data&&data.hubStats||{};
3877
+ var fp=JSON.stringify({e:!!data&&!!data.enabled,r:data&&data.role,pa:!!conn.pendingApproval,rj:!!conn.rejected,c:!!conn.connected,u:conn.user&&conn.user.username,tn:conn.teamName,cc:!!data&&!!data.clientConfigured,hu:data&&data.hubUrl,tm:hs.totalMembers,om:hs.onlineMembers,pm:hs.pendingMembers});
3770
3878
  if(fp===_lastSidebarFingerprint) return;
3771
3879
  _lastSidebarFingerprint=fp;
3772
3880
  if(!data||!data.enabled){
@@ -3781,8 +3889,16 @@ function renderSharingSidebar(data){
3781
3889
  badgeEl.innerHTML='<span style="display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:2px 8px;border-radius:9999px;background:'+color+'15;color:'+color+'"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:'+color+(glow?';box-shadow:0 0 4px '+color:'')+'"></span>'+esc(text)+'</span>';
3782
3890
  }
3783
3891
  if(data.role==='hub'){
3784
- setBadge('#34d399',t('sharing.sidebar.connected'),true);
3785
- statusEl.innerHTML='';
3892
+ setBadge('#34d399',t('sharing.sidebar.hubRunning'),true);
3893
+ var hs=data.hubStats||{};
3894
+ var html='<div class="info-grid">';
3895
+ if(conn.teamName) html+='<span class="label">'+t('sharing.sidebar.teamName')+'</span><span class="value" style="font-weight:600">'+esc(conn.teamName)+'</span>';
3896
+ html+='<span class="label">'+t('sharing.sidebar.members')+'</span><span class="value">'+(hs.totalMembers||0)+' <span style="opacity:.5;font-size:10px">/ '+t('sharing.sidebar.online')+' '+(hs.onlineMembers||0)+'</span></span>';
3897
+ if(hs.pendingMembers>0){
3898
+ html+='<span class="label">'+t('sharing.sidebar.pending')+'</span><span class="value" style="color:var(--yellow,#fbbf24);font-weight:600">'+hs.pendingMembers+'</span>';
3899
+ }
3900
+ html+='</div>';
3901
+ statusEl.innerHTML=html;
3786
3902
  hintEl.textContent='';
3787
3903
  }else if(conn.pendingApproval&&conn.user){
3788
3904
  setBadge('#fbbf24',t('sharing.sidebar.pending'),false);
@@ -3798,8 +3914,18 @@ function renderSharingSidebar(data){
3798
3914
  html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username||'-')+'</span>';
3799
3915
  if(conn.teamName) html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName)+'</span>';
3800
3916
  html+='</div>';
3917
+ html+='<div style="margin-top:8px"><button class="btn btn-sm btn-primary" onclick="retryHubJoin()" style="font-size:11px">'+t('sharing.retryJoin')+'</button></div>';
3801
3918
  statusEl.innerHTML=html;
3802
3919
  hintEl.textContent=t('sharing.rejected.hint');
3920
+ }else if(conn.removed&&conn.user){
3921
+ setBadge('#ef4444',t('sharing.sidebar.disconnected'),false);
3922
+ var html='<div class="info-grid">';
3923
+ html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username||'-')+'</span>';
3924
+ if(conn.teamName) html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName)+'</span>';
3925
+ html+='</div>';
3926
+ html+='<div style="margin-top:8px"><button class="btn btn-sm btn-primary" onclick="retryHubJoin()" style="font-size:11px">'+t('sharing.retryJoin')+'</button></div>';
3927
+ statusEl.innerHTML=html;
3928
+ hintEl.textContent=t('sharing.removed.hint');
3803
3929
  }else if(conn.connected&&conn.user){
3804
3930
  var isAdmin=conn.user.role==='admin';
3805
3931
  setBadge('#34d399',t('sharing.sidebar.connected'),true);
@@ -3834,16 +3960,29 @@ function renderSharingSettings(data){
3834
3960
  if(!data||!data.enabled){
3835
3961
  statusEl.innerHTML='';teamEl.innerHTML='';adminEl.innerHTML='';
3836
3962
  if(panelsWrap) panelsWrap.style.display='none';
3963
+ var adminNavTab0=document.querySelector('.tab[data-view="admin"]');
3964
+ if(adminNavTab0) adminNavTab0.style.display='none';
3965
+ if(_activeView==='admin') switchView('memories');
3837
3966
  return;
3838
3967
  }
3839
3968
  if(panelsWrap) panelsWrap.style.display='';
3840
3969
  var conn=data.connection||{};
3841
3970
  var user=conn.user||{};
3842
3971
  var actualRole=data.role||_sharingRole||'client';
3843
- if(data.role) _sharingRole=data.role;
3972
+ var prevIsAdmin=!!window._isHubAdmin;
3844
3973
  var isAdmin=(data.admin&&data.admin.canManageUsers)||(conn.connected&&user.role==='admin')||(actualRole==='hub');
3845
3974
  window._isHubAdmin=isAdmin;
3846
3975
  if(isAdmin) startAdminPoll();
3976
+ var adminNavTab=document.querySelector('.tab[data-view="admin"]');
3977
+ if(adminNavTab){
3978
+ var showTab=(actualRole==='hub')||(conn.connected);
3979
+ adminNavTab.style.display=showTab?'':'none';
3980
+ if(!showTab&&_activeView==='admin') switchView('memories');
3981
+ }
3982
+ if(prevIsAdmin&&!isAdmin&&_activeView==='admin'){
3983
+ _lastAdminFingerprint='';
3984
+ loadAdminData();
3985
+ }
3847
3986
  var hubAdminBtn=document.getElementById('hubAdminEntryBtn');
3848
3987
 
3849
3988
  if(actualRole==='hub'){
@@ -3865,6 +4004,8 @@ function renderSharingSettings(data){
3865
4004
  connBadge='<span class="hic-badge pending"><span class="hic-dot amber"></span>'+t('sharing.sidebar.pending')+'</span>';
3866
4005
  }else if(conn.rejected){
3867
4006
  connBadge='<span class="hic-badge disconnected"><span class="hic-dot red"></span>'+t('sharing.sidebar.rejected')+'</span>';
4007
+ }else if(conn.removed){
4008
+ connBadge='<span class="hic-badge disconnected"><span class="hic-dot red"></span>'+t('sharing.sidebar.disconnected')+'</span>';
3868
4009
  }else if(conn.connected){
3869
4010
  connBadge='<span class="hic-badge connected"><span class="hic-dot green"></span>'+t('sharing.sidebar.connected')+'</span>';
3870
4011
  }else{
@@ -3880,13 +4021,21 @@ function renderSharingSettings(data){
3880
4021
  sh+='</div><div class="hic-empty" style="color:#ef4444">'+t('sharing.rejected.hint')+'</div>'+
3881
4022
  '<div style="margin-top:10px;padding:0 16px 14px"><button class="btn btn-sm btn-primary" onclick="retryHubJoin()">'+t('sharing.retryJoin')+'</button>'+
3882
4023
  '<span style="font-size:11px;color:var(--text-muted);margin-left:8px">'+t('sharing.retryJoin.hint')+'</span></div></div>';
4024
+ }else if(conn.removed){
4025
+ if(user.username) sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value">'+esc(user.username)+'</span>';
4026
+ sh+='</div><div class="hic-empty" style="color:#ef4444">'+t('sharing.removed.hint')+'</div>'+
4027
+ '<div style="margin-top:10px;padding:0 16px 14px"><button class="btn btn-sm btn-primary" onclick="retryHubJoin()">'+t('sharing.retryJoin')+'</button>'+
4028
+ '<span style="font-size:11px;color:var(--text-muted);margin-left:8px">'+t('sharing.retryJoin.hint')+'</span></div></div>';
3883
4029
  }else if(conn.connected&&user.username){
3884
4030
  sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value" style="display:flex;align-items:center;gap:6px">'+
3885
4031
  '<input type="text" id="hubUsernameInput" value="'+esc(user.username)+'" style="border:1px solid var(--border);border-radius:6px;padding:3px 8px;font-size:12px;background:var(--bg);color:var(--text);width:120px;font-family:inherit" />'+
3886
4032
  '<button class="btn btn-sm" onclick="updateHubUsername()" style="padding:2px 10px;font-size:11px">'+t('sharing.saveUsername')+'</button>'+
3887
4033
  '</span>';
3888
4034
  sh+='<span class="hic-label">'+t('sharing.team')+'</span><span class="hic-value">'+esc(conn.teamName||'-')+'</span>';
3889
- sh+='</div></div>';
4035
+ sh+='</div>'+
4036
+ '<div style="border-top:1px solid var(--border);margin-top:10px;padding:10px 16px 6px;display:flex;align-items:center;justify-content:flex-end">'+
4037
+ '<button class="btn btn-sm" onclick="leaveTeam()" style="color:#ef4444;border-color:rgba(239,68,68,.3);font-size:11px;padding:4px 12px">'+t('sharing.leaveTeam')+'</button>'+
4038
+ '</div></div>';
3890
4039
  }else{
3891
4040
  sh+='</div><div class="hic-empty" style="color:var(--text-muted)">'+t('sharing.disconnected.hint')+'</div>'+
3892
4041
  '<div style="margin-top:10px;padding:0 16px 14px"><button class="btn btn-sm btn-primary" id="btnRetryConn" onclick="retryConnection()">'+t('sharing.retryConnection')+'</button>'+
@@ -3905,6 +4054,7 @@ async function retryConnection(){
3905
4054
  var result=document.getElementById('retryConnResult');
3906
4055
  if(btn){btn.disabled=true;btn.textContent=t('sharing.retryConnection.loading');}
3907
4056
  if(result) result.innerHTML='<span style="color:var(--text-muted)">'+t('sharing.retryConnection.loading')+'</span>';
4057
+ toast(t('sharing.retryConnection.loading'),'info');
3908
4058
  try{
3909
4059
  await loadSharingStatus(false);
3910
4060
  var d=sharingStatusCache;
@@ -3912,9 +4062,11 @@ async function retryConnection(){
3912
4062
  toast(t('sharing.retryConnection.success'),'success');
3913
4063
  if(result) result.innerHTML='<span style="color:#22c55e">\\u2705 '+t('sharing.retryConnection.success')+'</span>';
3914
4064
  }else{
4065
+ toast(t('sharing.retryConnection.fail'),'error');
3915
4066
  if(result) result.innerHTML='<span style="color:#ef4444">'+t('sharing.retryConnection.fail')+'</span>';
3916
4067
  }
3917
4068
  }catch(e){
4069
+ toast(t('sharing.retryConnection.fail'),'error');
3918
4070
  if(result) result.innerHTML='<span style="color:#ef4444">'+t('sharing.retryConnection.fail')+'</span>';
3919
4071
  }
3920
4072
  if(btn){btn.disabled=false;btn.textContent=t('sharing.retryConnection');}
@@ -3927,19 +4079,36 @@ async function retryHubJoin(){
3927
4079
  var d=await r.json();
3928
4080
  if(d.ok){
3929
4081
  toast(t('sharing.retryJoin.success'),'success');
3930
- setTimeout(function(){location.reload();},1500);
4082
+ _lastSidebarFingerprint='';_lastSettingsFingerprint='';_lastSharingConnStatus='';
4083
+ setTimeout(function(){loadSharingStatus(true);},800);
3931
4084
  }else{
3932
4085
  toast(d.error||t('sharing.retryJoin.fail'),'error');
3933
4086
  }
3934
4087
  }catch(e){toast(t('sharing.retryJoin.fail')+': '+e.message,'error');}
3935
4088
  }
3936
4089
 
4090
+ async function leaveTeam(){
4091
+ var teamName=(sharingStatusCache&&sharingStatusCache.connection&&sharingStatusCache.connection.teamName)||'';
4092
+ var msg=t('sharing.leaveTeam.confirm').replace('{team}',teamName||t('sharing.team.default'));
4093
+ if(!(await confirmModal(msg,{danger:true}))) return;
4094
+ try{
4095
+ var r=await fetch('/api/sharing/leave',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
4096
+ var d=await r.json();
4097
+ if(d.ok){
4098
+ toast(t('sharing.leaveTeam.success'),'success');
4099
+ showRestartOverlay(t('settings.restart.waiting'));
4100
+ }else{
4101
+ toast(d.error||t('sharing.leaveTeam.fail'),'error');
4102
+ }
4103
+ }catch(e){toast(t('sharing.leaveTeam.fail')+': '+e.message,'error');}
4104
+ }
4105
+
3937
4106
  async function updateHubUsername(){
3938
4107
  var input=document.getElementById('hubUsernameInput');
3939
4108
  if(!input) return;
3940
4109
  var newName=input.value.trim();
3941
4110
  if(!newName||newName.length<2||newName.length>32){
3942
- toast(t('sharing.username.invalid'),'error');
4111
+ alertModal(t('sharing.username.invalid'));
3943
4112
  return;
3944
4113
  }
3945
4114
  try{
@@ -3950,17 +4119,17 @@ async function updateHubUsername(){
3950
4119
  });
3951
4120
  var d=await r.json();
3952
4121
  if(d.error==='username_taken'){
3953
- toast(t('sharing.username.taken'),'error');
4122
+ alertModal(t('sharing.username.taken'),{danger:true});
3954
4123
  return;
3955
4124
  }
3956
4125
  if(d.error){
3957
- toast(d.error,'error');
4126
+ alertModal(d.error,{danger:true});
3958
4127
  return;
3959
4128
  }
3960
4129
  toast(t('sharing.username.updated'),'success');
3961
4130
  loadSharingStatus(false);
3962
4131
  }catch(e){
3963
- toast(t('sharing.username.error'),'error');
4132
+ alertModal(t('sharing.username.error'),{danger:true});
3964
4133
  }
3965
4134
  }
3966
4135
 
@@ -4006,7 +4175,7 @@ async function approveSharingUser(userId,username){
4006
4175
  try{
4007
4176
  const r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4008
4177
  const d=await r.json();
4009
- if(d.ok){toast(t('toast.userApproved'),'success');loadSharingPendingUsers();loadSharingStatus(true);} else {toast(d.error||t('toast.approveFail'),'error');}
4178
+ if(d.ok){toast(t('toast.userApproved'),'success');loadSharingPendingUsers();loadSharingStatus(true);_lastAdminFingerprint='';loadAdminData();} else {toast(d.error||t('toast.approveFail'),'error');}
4010
4179
  }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
4011
4180
  }
4012
4181
 
@@ -4014,23 +4183,15 @@ async function rejectSharingUser(userId,username){
4014
4183
  try{
4015
4184
  const r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4016
4185
  const d=await r.json();
4017
- if(d.ok){toast(t('toast.userRejected'),'success');loadSharingPendingUsers();} else {toast(d.error||t('toast.rejectFail'),'error');}
4186
+ if(d.ok){toast(t('toast.userRejected'),'success');loadSharingPendingUsers();_lastAdminFingerprint='';loadAdminData();} else {toast(d.error||t('toast.rejectFail'),'error');}
4018
4187
  }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
4019
4188
  }
4020
4189
 
4021
4190
  /* ─── Team Setup Guide ─── */
4022
- var TEAM_GUIDE_DISMISSED_KEY='memos-team-guide-dismissed';
4023
4191
  function updateTeamGuide(sharingData){
4024
4192
  var el=document.getElementById('teamSetupGuide');
4025
4193
  if(!el) return;
4026
- if(localStorage.getItem(TEAM_GUIDE_DISMISSED_KEY)==='1'){el.style.display='none';return;}
4027
- var isConfigured=sharingData&&sharingData.enabled;
4028
- el.style.display=isConfigured?'none':'block';
4029
- }
4030
- function dismissTeamGuide(){
4031
- localStorage.setItem(TEAM_GUIDE_DISMISSED_KEY,'1');
4032
- var el=document.getElementById('teamSetupGuide');
4033
- if(el) el.style.display='none';
4194
+ el.style.display='block';
4034
4195
  }
4035
4196
  function guideGoToHub(role){
4036
4197
  switchSettingsTab('hub',document.querySelector('.settings-tab-btn[data-tab="hub"]'));
@@ -4063,7 +4224,7 @@ function adminPaginateHtml(total,page,refilterFn){
4063
4224
  if(end<pages) html+=(end<pages-1?'<span class="pg-info">...</span>':'')+'<button class="pg-btn" onclick="'+refilterFn+'Page('+(pages-1)+')">'+pages+'</button>';
4064
4225
  html+='<button class="pg-btn'+(page>=pages-1?' disabled':'')+'" onclick="'+refilterFn+'Page('+(page+1)+')">\\u2192</button>';
4065
4226
  html+='<span class="pg-info">'+total+' '+t('pagination.total')+'</span>';
4066
- html+='</div>';
4227
+ html+='</div>';
4067
4228
  return html;
4068
4229
  }
4069
4230
 
@@ -4125,12 +4286,12 @@ async function loadAdminData(){
4125
4286
  var fetches;
4126
4287
  if(isAdmin){
4127
4288
  fetches=await Promise.all([
4128
- fetch('/api/sharing/users').then(function(r){return r.json();}),
4129
- fetch('/api/admin/shared-tasks').then(function(r){return r.json();}),
4130
- fetch('/api/admin/shared-skills').then(function(r){return r.json();}),
4131
- fetch('/api/sharing/pending-users').then(function(r){return r.json();}),
4132
- fetch('/api/admin/shared-memories').then(function(r){return r.json();})
4133
- ]);
4289
+ fetch('/api/sharing/users').then(function(r){return r.json();}),
4290
+ fetch('/api/admin/shared-tasks').then(function(r){return r.json();}),
4291
+ fetch('/api/admin/shared-skills').then(function(r){return r.json();}),
4292
+ fetch('/api/sharing/pending-users').then(function(r){return r.json();}),
4293
+ fetch('/api/admin/shared-memories').then(function(r){return r.json();})
4294
+ ]);
4134
4295
  }else{
4135
4296
  fetches=await Promise.all([
4136
4297
  Promise.resolve({users:[]}),
@@ -4147,7 +4308,7 @@ async function loadAdminData(){
4147
4308
  var _newMemories=Array.isArray(memoriesR.memories)?memoriesR.memories:[];
4148
4309
  var pending=isAdmin?(Array.isArray(pendingR.users)?pendingR.users:[]):[];
4149
4310
  var _fp=_newUsers.length+':'+_newTasks.length+':'+_newSkills.length+':'+_newMemories.length+':'+pending.length
4150
- +':'+_newUsers.map(function(u){return u.id+'|'+(u.isOnline?1:0)+'|'+(u.role||'')}).join(',')
4311
+ +':'+_newUsers.map(function(u){return u.id+'|'+(u.isOnline?1:0)+'|'+(u.role||'')+'|'+(u.status||'')+'|'+(u.username||'')+'|'+(u.memoryCount||0)+'|'+(u.taskCount||0)+'|'+(u.skillCount||0)}).join(',')
4151
4312
  +':'+_newMemories.map(function(m){return m.id}).join(',')
4152
4313
  +':'+_newTasks.map(function(t){return t.id+'|'+(t.status||'')}).join(',')
4153
4314
  +':'+_newSkills.map(function(s){return s.id+'|'+(s.status||'')}).join(',')
@@ -4200,11 +4361,15 @@ function updateAdminTabsVisibility(){
4200
4361
  if(subEl) subEl.textContent=isAdmin?t('admin.subtitle'):t('admin.subtitle.member');
4201
4362
  }
4202
4363
 
4364
+ var _lastAdminStatsFp='';
4203
4365
  function renderAdminStats(pendingCount){
4204
4366
  var el=document.getElementById('adminStats');
4205
4367
  if(!el) return;
4206
4368
  var isAdmin=!!window._isHubAdmin;
4207
4369
  var onlineCount=adminDataCache.users.filter(function(u){return !!u.isOnline;}).length;
4370
+ var sfp=onlineCount+':'+adminDataCache.users.length+':'+pendingCount+':'+(adminDataCache.memories||[]).length+':'+adminDataCache.tasks.length+':'+adminDataCache.skills.length+':'+isAdmin;
4371
+ if(sfp===_lastAdminStatsFp) return;
4372
+ _lastAdminStatsFp=sfp;
4208
4373
  el.innerHTML=
4209
4374
  (isAdmin?'<div class="admin-stat-box"><span class="as-icon">\u{1F465}</span><div class="val">'+onlineCount+' / '+adminDataCache.users.length+'</div><div class="lbl">'+t('admin.stat.activeUsers')+'</div></div>'+
4210
4375
  '<div class="admin-stat-box"><span class="as-icon">\u{23F3}</span><div class="val">'+pendingCount+'</div><div class="lbl">'+t('admin.stat.pending')+'</div></div>':'')+
@@ -4226,9 +4391,10 @@ function auRelativeTime(ts){
4226
4391
  return t('notif.timeAgo.day').replace('{n}',Math.floor(diff/86400000));
4227
4392
  }
4228
4393
 
4229
- function renderAdminUserCard(u,adminCount){
4394
+ function renderAdminUserCard(u,adminCount,myUserId){
4230
4395
  var uid=escAttr(u.id);
4231
4396
  var uname=escAttr(u.username||'');
4397
+ var isSelf=!!(myUserId&&u.id===myUserId);
4232
4398
  var online=!!u.isOnline;
4233
4399
  var statusCls=online?'online':'offline';
4234
4400
 
@@ -4262,7 +4428,9 @@ function renderAdminUserCard(u,adminCount){
4262
4428
  var infoHtml='<div class="au-info">'+infoRows.join('')+'</div>';
4263
4429
 
4264
4430
  var actions='';
4265
- if(u.isOwner){
4431
+ if(isSelf){
4432
+ actions+='<span style="font-size:11px;color:var(--text-muted);padding:4px 0">'+t('admin.selfHint')+'</span>';
4433
+ }else if(u.isOwner){
4266
4434
  actions+='<span style="font-size:11px;color:var(--text-muted);padding:4px 0">'+t('admin.ownerHint')+'</span>';
4267
4435
  }else if(u.role!=='admin'){
4268
4436
  actions+='<button class="btn btn-sm btn-ghost" onclick="adminToggleRole(&quot;'+uid+'&quot;,&quot;admin&quot;)" style="color:var(--accent)">'+t('admin.promoteAdmin')+'</button>';
@@ -4273,12 +4441,13 @@ function renderAdminUserCard(u,adminCount){
4273
4441
  }else{
4274
4442
  actions+='<span style="font-size:11px;color:var(--text-muted);padding:4px 0">'+t('admin.lastAdminHint')+'</span>';
4275
4443
  }
4276
- var ownerBadge=u.isOwner?' <span style="font-size:9px;background:linear-gradient(135deg,#fbbf24,#f59e0b);color:#000;padding:2px 8px;border-radius:6px;font-weight:800;vertical-align:middle;margin-left:4px;letter-spacing:.04em;text-transform:uppercase;box-shadow:0 2px 8px rgba(251,191,36,.3)">Owner</span>':'';
4444
+ var badgesHtml='<div class="au-badges">'+statusLabel+
4445
+ '<span class="admin-badge '+(u.role==='admin'?'admin':'member')+'">'+esc(u.role||'member').toUpperCase()+'</span>'+
4446
+ (u.isOwner?'<span class="admin-badge owner">OWNER</span>':'')+
4447
+ '</div>';
4277
4448
 
4278
- return '<div class="admin-card au-card au-'+statusCls+'"><div class="admin-card-header"><div style="flex:1;min-width:0;display:flex;align-items:center;gap:8px;flex-wrap:wrap">'+
4279
- '<div style="flex:1;min-width:0">'+titleDisplay+editRow+'</div>'+statusLabel+
4280
- '</div>'+
4281
- '<div style="display:flex;align-items:center;gap:6px"><span class="admin-badge '+(u.role==='admin'?'admin':'member')+'">'+esc(u.role||'member')+'</span>'+ownerBadge+'</div></div>'+
4449
+ return '<div class="admin-card au-card au-'+statusCls+'"><div class="admin-card-header"><div style="flex:1;min-width:0">'+titleDisplay+editRow+'</div>'+
4450
+ badgesHtml+'</div>'+
4282
4451
  contribHtml+infoHtml+
4283
4452
  (actions?'<div class="admin-card-actions" style="border-top:1px solid rgba(99,102,241,.08);padding-top:12px;margin-top:6px">'+actions+'</div>':'')+
4284
4453
  '</div>';
@@ -4313,6 +4482,7 @@ function renderAdminUsers(users,pending){
4313
4482
  offlineUsers.sort(function(a,b){return (b.lastActiveAt||0)-(a.lastActiveAt||0);});
4314
4483
  var sorted=onlineUsers.concat(offlineUsers);
4315
4484
  var adminCount=users.filter(function(x){return x.role==='admin';}).length;
4485
+ var myUserId=sharingStatusCache&&sharingStatusCache.connection&&sharingStatusCache.connection.user?sharingStatusCache.connection.user.id:null;
4316
4486
 
4317
4487
  if(sorted.length===0){
4318
4488
  html+='<div class="admin-empty"><span class="ae-icon">\u{1F465}</span>'+t('admin.noActiveUsers')+'</div>';
@@ -4321,13 +4491,13 @@ function renderAdminUsers(users,pending){
4321
4491
  if(onlineUsers.length===0){
4322
4492
  html+='<div style="font-size:12px;color:var(--text-muted);padding:8px 0 12px">\u2014</div>';
4323
4493
  }else{
4324
- for(var i=0;i<onlineUsers.length;i++) html+=renderAdminUserCard(onlineUsers[i],adminCount);
4494
+ for(var i=0;i<onlineUsers.length;i++) html+=renderAdminUserCard(onlineUsers[i],adminCount,myUserId);
4325
4495
  }
4326
4496
  html+='<div class="au-group-header"><span class="au-group-dot offline"></span>'+t('admin.offlineUsers')+' <span class="au-group-count">('+offlineUsers.length+')</span></div>';
4327
4497
  if(offlineUsers.length===0){
4328
4498
  html+='<div style="font-size:12px;color:var(--text-muted);padding:8px 0 12px">\u2014</div>';
4329
4499
  }else{
4330
- for(var j=0;j<offlineUsers.length;j++) html+=renderAdminUserCard(offlineUsers[j],adminCount);
4500
+ for(var j=0;j<offlineUsers.length;j++) html+=renderAdminUserCard(offlineUsers[j],adminCount,myUserId);
4331
4501
  }
4332
4502
  }
4333
4503
  el.innerHTML=html;
@@ -4338,7 +4508,7 @@ async function adminApproveUser(userId,username){
4338
4508
  try{
4339
4509
  var r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4340
4510
  var d=await r.json();
4341
- if(d.ok){toast(t('toast.userApproved'),'success');loadAdminData();}else{toast(d.error||t('toast.approveFail'),'error');}
4511
+ if(d.ok){toast(t('toast.userApproved'),'success');_lastAdminFingerprint='';loadAdminData();}else{toast(d.error||t('toast.approveFail'),'error');}
4342
4512
  }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
4343
4513
  }
4344
4514
  async function adminRejectUser(userId){
@@ -4346,7 +4516,7 @@ async function adminRejectUser(userId){
4346
4516
  try{
4347
4517
  var r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
4348
4518
  var d=await r.json();
4349
- if(d.ok){toast(t('toast.userRejected'),'success');loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
4519
+ if(d.ok){toast(t('toast.userRejected'),'success');_lastAdminFingerprint='';loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
4350
4520
  }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
4351
4521
  }
4352
4522
  async function adminToggleRole(userId,newRole){
@@ -4355,7 +4525,14 @@ async function adminToggleRole(userId,newRole){
4355
4525
  try{
4356
4526
  var r=await fetch('/api/sharing/change-role',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,role:newRole})});
4357
4527
  var d=await r.json();
4358
- if(d.ok){toast(t('toast.roleChanged'),'success');loadAdminData();}
4528
+ if(d.ok){
4529
+ toast(t('toast.roleChanged'),'success');
4530
+ _lastAdminFingerprint='';
4531
+ _lastSettingsFingerprint='';
4532
+ _lastSidebarFingerprint='';
4533
+ await loadSharingStatus(false);
4534
+ loadAdminData();
4535
+ }
4359
4536
  else if(d.error==='cannot_demote_owner'){toast(t('admin.ownerHint'),'error');}
4360
4537
  else{toast(d.error||t('toast.roleChangeFail'),'error');}
4361
4538
  }catch(e){toast(t('toast.roleChangeFail')+': '+e.message,'error');}
@@ -4381,13 +4558,24 @@ async function adminSaveEditName(userId){
4381
4558
  var inputEl=document.getElementById('au_input_'+userId);
4382
4559
  if(!inputEl) return;
4383
4560
  var newName=inputEl.value.trim();
4384
- if(!newName||newName.length<2||newName.length>32){toast(t('toast.invalidUsername'),'warn');return;}
4561
+ if(!newName||newName.length<2||newName.length>32){
4562
+ alertModal(t('toast.invalidUsername'),{title:t('admin.editName')});
4563
+ return;
4564
+ }
4385
4565
  inputEl.disabled=true;
4386
4566
  try{
4387
4567
  var r=await fetch('/api/sharing/rename-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:newName})});
4388
4568
  var d=await r.json();
4389
- if(d.ok){toast(t('toast.usernameChanged'),'success');loadAdminData();}else{inputEl.disabled=false;toast(d.error||t('toast.renameFail'),'error');}
4390
- }catch(e){inputEl.disabled=false;toast(t('toast.renameFail')+': '+e.message,'error');}
4569
+ if(d.ok){toast(t('toast.usernameChanged'),'success');adminCancelEditName(userId);loadAdminData();}
4570
+ else{
4571
+ inputEl.disabled=false;
4572
+ if(d.error==='username_taken'){
4573
+ alertModal(t('sharing.username.taken'),{title:t('admin.editName'),danger:true});
4574
+ }else{
4575
+ alertModal(d.error||t('toast.renameFail'),{title:t('admin.editName'),danger:true});
4576
+ }
4577
+ }
4578
+ }catch(e){inputEl.disabled=false;alertModal(t('toast.renameFail'),{title:t('admin.editName'),danger:true});}
4391
4579
  }
4392
4580
 
4393
4581
  async function adminRemoveUser(userId,username){
@@ -4396,7 +4584,7 @@ async function adminRemoveUser(userId,username){
4396
4584
  try{
4397
4585
  var r=await fetch('/api/sharing/remove-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,cleanResources:clean})});
4398
4586
  var d=await r.json();
4399
- if(d.ok){toast(t('toast.userRemoved'),'success');loadAdminData();}
4587
+ if(d.ok){toast(t('toast.userRemoved'),'success');_lastAdminFingerprint='';loadAdminData();}
4400
4588
  else if(d.error==='cannot_remove_owner'){toast(t('admin.ownerHint'),'error');}
4401
4589
  else{toast(d.error||t('toast.removeFail'),'error');}
4402
4590
  }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
@@ -4465,7 +4653,7 @@ function renderAdminTasks(tasks){
4465
4653
  '<div class="admin-card-header"><div class="admin-card-title">'+esc(tk.title||tk.id)+'</div></div>'+
4466
4654
  '<div class="admin-card-tags">'+
4467
4655
  '<div class="admin-card-tags-left">'+
4468
- '<span class="admin-card-tag tag-owner">\u{1F464} '+esc(tk.ownerName||tk.sourceUserId||'unknown')+'</span>'+
4656
+ '<span class="admin-card-tag tag-owner">\u{1F464} '+fmtOwner(tk)+'</span>'+
4469
4657
  (tk.status?'<span class="admin-card-tag tag-status">'+esc(tk.status)+'</span>':'')+
4470
4658
  (tk.chunkCount!=null?'<span class="admin-card-tag tag-kind">\u{1F4DD} '+tk.chunkCount+' '+t('admin.chunks')+'</span>':'')+
4471
4659
  '</div>'+
@@ -4527,7 +4715,7 @@ function renderAdminSkills(skills){
4527
4715
  '<div class="admin-card-header"><div class="admin-card-title">'+esc(s.name||s.id)+'</div></div>'+
4528
4716
  '<div class="admin-card-tags">'+
4529
4717
  '<div class="admin-card-tags-left">'+
4530
- '<span class="admin-card-tag tag-owner">\u{1F464} '+esc(s.ownerName||s.sourceUserId||'unknown')+'</span>'+
4718
+ '<span class="admin-card-tag tag-owner">\u{1F464} '+fmtOwner(s)+'</span>'+
4531
4719
  (s.status?'<span class="admin-card-tag tag-status">'+esc(s.status)+'</span>':'')+
4532
4720
  (s.version!=null?'<span class="admin-card-tag tag-version">v'+s.version+'</span>':'')+
4533
4721
  (qs!=null?'<span class="admin-card-tag tag-kind">\u2605 '+Number(qs).toFixed(1)+'</span>':'')+
@@ -4592,7 +4780,7 @@ function renderAdminMemories(memories){
4592
4780
  '<div class="admin-card-header"><div class="admin-card-title">'+esc(m.summary||m.content?.slice(0,80)||m.id)+'</div></div>'+
4593
4781
  '<div class="admin-card-tags">'+
4594
4782
  '<div class="admin-card-tags-left">'+
4595
- '<span class="admin-card-tag tag-owner">\u{1F464} '+esc(m.ownerName||m.sourceUserId||'unknown')+'</span>'+
4783
+ '<span class="admin-card-tag tag-owner">\u{1F464} '+fmtOwner(m)+'</span>'+
4596
4784
  (m.role?'<span class="admin-card-tag tag-role">'+esc(m.role)+'</span>':'')+
4597
4785
  (m.kind?'<span class="admin-card-tag tag-kind">'+esc(m.kind)+'</span>':'')+
4598
4786
  '</div>'+
@@ -4636,7 +4824,7 @@ function toggleAdminMemoryCard(cardId,idx){
4636
4824
  (m.kind?'<span class="meta-item">'+t('admin.kind')+esc(m.kind)+'</span>':'')+
4637
4825
  (m.role?'<span class="meta-item">'+t('admin.role')+esc(m.role)+'</span>':'')+
4638
4826
  (m.visibility?'<span class="meta-item">'+t('admin.visibility')+esc(m.visibility)+'</span>':'')+
4639
- '<span class="meta-item">'+t('admin.owner')+esc(m.ownerName||m.sourceUserId||'unknown')+'</span>'+
4827
+ '<span class="meta-item">'+t('admin.owner')+fmtOwner(m)+'</span>'+
4640
4828
  (m.groupName?'<span class="meta-item">'+t('admin.group')+esc(m.groupName)+'</span>':'')+
4641
4829
  '<span class="meta-item">'+new Date(m.updatedAt||m.createdAt||0).toLocaleString(dateLoc())+'</span>'+
4642
4830
  '</div>';
@@ -4692,7 +4880,7 @@ async function toggleAdminTaskCard(cardId,idx){
4692
4880
  var metaHtml='<div class="admin-card-detail-meta admin-task-meta">'+
4693
4881
  (tk.status?'<span class="meta-item"><span class="task-status-badge '+tk.status+'">'+esc(tk.status)+'</span></span>':'')+
4694
4882
  (tk.visibility?'<span class="meta-item">'+t('admin.visibility')+esc(tk.visibility)+'</span>':'')+
4695
- '<span class="meta-item">'+t('admin.owner')+esc(tk.ownerName||'unknown')+'</span>'+
4883
+ '<span class="meta-item">'+t('admin.owner')+fmtOwner(tk)+'</span>'+
4696
4884
  (tk.groupName?'<span class="meta-item">'+t('admin.group')+esc(tk.groupName)+'</span>':'')+
4697
4885
  (task.chunks&&task.chunks.length?'<span class="meta-item">\u{1F4AC} '+task.chunks.length+' '+t('tasks.chunks.label')+'</span>':'')+
4698
4886
  (task.startedAt?'<span class="meta-item">\u{1F4C5} '+formatDateTimeSeconds(task.startedAt)+'</span>':'')+
@@ -4779,7 +4967,7 @@ async function toggleAdminSkillCard(cardId,idx){
4779
4967
  (localSkill.status?'<span class="meta-item"><span class="skill-badge status-'+localSkill.status+'">'+esc(localSkill.status)+'</span></span>':'')+
4780
4968
  (sk.visibility?'<span class="meta-item">'+t('admin.visibility')+esc(sk.visibility||'hub')+'</span>':'')+
4781
4969
  (qs!=null?'<span class="meta-item"><span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\u2605 '+Number(qs).toFixed(1)+'/10</span></span>':'')+
4782
- '<span class="meta-item">'+t('admin.owner')+esc(sk.ownerName||'unknown')+'</span>'+
4970
+ '<span class="meta-item">'+t('admin.owner')+fmtOwner(sk)+'</span>'+
4783
4971
  (sk.groupName?'<span class="meta-item">'+t('admin.group')+esc(sk.groupName)+'</span>':'')+
4784
4972
  '<span class="meta-item">'+t('admin.updated')+new Date(sk.updatedAt||sk.createdAt||0).toLocaleString(dateLoc())+'</span>'+
4785
4973
  '</div>';
@@ -4848,7 +5036,7 @@ function renderSharingMemorySearchResults(data,query){
4848
5036
  '<div class="summary">'+(idx+1)+'. '+esc(hit.summary||'(no summary)')+'</div>'+
4849
5037
  '<div class="excerpt">'+esc(hit.excerpt||'')+'</div>'+
4850
5038
  '<div class="hub-hit-meta">'+
4851
- '<span class="meta-chip">owner: '+esc(hit.ownerName||'unknown')+'</span>'+
5039
+ '<span class="meta-chip">owner: '+fmtOwner(hit)+'</span>'+
4852
5040
  (hit.groupName?'<span class="meta-chip">group: '+esc(hit.groupName)+'</span>':'')+
4853
5041
  '<span class="meta-chip">visibility: '+esc(hit.visibility||'hub')+'</span>'+
4854
5042
  '</div>'+
@@ -4933,7 +5121,7 @@ function openHubTaskDetailFromCache(cacheKey,idx){
4933
5121
  var meta=[
4934
5122
  '<span class="meta-item">\\u{1F310} '+t('scope.hub')+'</span>',
4935
5123
  task.status?'<span class="meta-item"><span class="task-status-badge '+task.status+'">'+esc(task.status)+'</span></span>':'',
4936
- '<span class="meta-item">'+t('admin.owner')+esc(task.ownerName||'unknown')+'</span>',
5124
+ '<span class="meta-item">'+t('admin.owner')+fmtOwner(task)+'</span>',
4937
5125
  task.groupName?'<span class="meta-item">'+t('admin.group')+esc(task.groupName)+'</span>':'',
4938
5126
  task.visibility?'<span class="meta-item">'+t('admin.visibility')+esc(task.visibility)+'</span>':'',
4939
5127
  task.chunkCount!=null?'<span class="meta-item">\\u{1F4DD} '+esc(String(task.chunkCount))+' '+t('tasks.chunks.label')+'</span>':'',
@@ -4964,14 +5152,14 @@ function openHubSkillDetailFromCache(cacheKey,idx){
4964
5152
  skill.status?'<span class="meta-item"><span class="skill-badge status-'+skill.status+'">'+esc(skill.status)+'</span></span>':'',
4965
5153
  '<span class="meta-item">visibility: '+esc(skill.visibility||'hub')+'</span>',
4966
5154
  qsBadge,
4967
- '<span class="meta-item">'+t('admin.owner')+esc(skill.ownerName||'unknown')+'</span>',
5155
+ '<span class="meta-item">'+t('admin.owner')+fmtOwner(skill)+'</span>',
4968
5156
  skill.groupName?'<span class="meta-item">'+t('admin.group')+esc(skill.groupName)+'</span>':'',
4969
5157
  (skill.updatedAt||skill.createdAt)?'<span class="meta-item">'+t('admin.updated')+new Date(skill.updatedAt||skill.createdAt).toLocaleString(dateLoc())+'</span>':'',
4970
5158
  ].filter(Boolean);
4971
5159
  document.getElementById('skillDetailMeta').innerHTML=meta.join('');
4972
5160
  document.getElementById('skillDetailDesc').textContent=skill.description||'';
4973
5161
  document.getElementById('skillFilesList').innerHTML='';
4974
- document.getElementById('skillDetailContent').innerHTML=skill.content?'<pre>'+esc(skill.content)+'</pre>':'';
5162
+ document.getElementById('skillDetailContent').innerHTML=skill.content?renderSkillMarkdown(skill.content):'';
4975
5163
  document.getElementById('skillVersionsList').innerHTML='';
4976
5164
  document.getElementById('skillRelatedTasks').innerHTML='';
4977
5165
  var visBtn=document.getElementById('skillVisibilityBtn');
@@ -5014,6 +5202,7 @@ function openScopeSelectorModal(resourceType, resourceId, currentScope, onConfir
5014
5202
  var existing=document.getElementById('scopeSelectorOverlay');
5015
5203
  if(existing) existing.remove();
5016
5204
  var teamEnabled=sharingStatusCache&&sharingStatusCache.enabled;
5205
+ var teamConnected=teamEnabled&&sharingStatusCache.connection&&sharingStatusCache.connection.connected;
5017
5206
  var overlay=document.createElement('div');
5018
5207
  overlay.id='scopeSelectorOverlay';
5019
5208
  overlay.style.cssText='position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);backdrop-filter:blur(6px);z-index:10000;display:flex;align-items:center;justify-content:center;animation:fadeIn 0.12s ease';
@@ -5027,7 +5216,7 @@ function openScopeSelectorModal(resourceType, resourceId, currentScope, onConfir
5027
5216
  for(var i=0;i<scopes.length;i++){
5028
5217
  var sc=scopes[i];
5029
5218
  var isCurrent=sc===currentScope;
5030
- var isDisabled=sc==='team'&&!teamEnabled;
5219
+ var isDisabled=sc==='team'&&(!teamEnabled||!teamConnected);
5031
5220
  var color=getScopeColor(sc);
5032
5221
  var cursor=isDisabled?'not-allowed':'pointer';
5033
5222
  var opacity=isDisabled?'0.4':'1';
@@ -5096,7 +5285,8 @@ async function confirmScopeSelection(){
5096
5285
  if(st.onConfirm) st.onConfirm(newScope);
5097
5286
  else loadAll();
5098
5287
  }else{
5099
- toast(d.error||t('share.scope.changeFail'),'error');
5288
+ var errMsg=d.error==='inactive_memory'?t('share.scope.inactiveDisabled'):(d.message||d.error||t('share.scope.changeFail'));
5289
+ toast(errMsg,'error');
5100
5290
  }
5101
5291
  }catch(e){toast(t('share.scope.changeFail')+': '+e.message,'error');}
5102
5292
  }
@@ -5347,6 +5537,13 @@ function parseMemoryAddEntries(out){
5347
5537
  return results;
5348
5538
  }
5349
5539
 
5540
+ function recallOriginBadge(origin){
5541
+ if(origin==='local-shared') return '<span class="recall-origin local-shared">'+t('recall.origin.localShared')+'</span>';
5542
+ if(origin==='hub-memory') return '<span class="recall-origin hub-memory">'+t('recall.origin.hubMemory')+'</span>';
5543
+ if(origin==='hub-remote') return '<span class="recall-origin hub-remote">'+t('recall.origin.hubRemote')+'</span>';
5544
+ return '';
5545
+ }
5546
+
5350
5547
  function buildLogSummary(lg){
5351
5548
  let inputObj=null;
5352
5549
  try{inputObj=JSON.parse(lg.input);}catch(_){}
@@ -5371,8 +5568,9 @@ function buildLogSummary(lg){
5371
5568
  var scoreClass=c.score>=0.7?'high':c.score>=0.5?'mid':'low';
5372
5569
  var shortText=escapeHtml(c.summary||c.content||c.original_excerpt||'');
5373
5570
  var fullText=escapeHtml(c.content||c.original_excerpt||c.summary||'');
5571
+ var oBadge=recallOriginBadge(c.origin);
5374
5572
  html+='<div class="recall-item" onclick="event.stopPropagation();this.classList.toggle(\\\'expanded\\\')">';
5375
- html+='<div class="recall-item-head"><span class="recall-score '+scoreClass+'">'+c.score.toFixed(2)+'</span><span class="log-msg-role '+(c.role||'user')+'">'+(c.role||'user')+'</span><span class="recall-summary-short">'+shortText+'</span><span class="recall-expand-icon">\u25B6</span></div>';
5573
+ html+='<div class="recall-item-head"><span class="recall-score '+scoreClass+'">'+c.score.toFixed(2)+'</span><span class="log-msg-role '+(c.role||'user')+'">'+(c.role||'user')+'</span>'+oBadge+'<span class="recall-summary-short">'+shortText+'</span><span class="recall-expand-icon">\u25B6</span></div>';
5376
5574
  html+='<div class="recall-summary-full">'+fullText+'</div>';
5377
5575
  html+='</div>';
5378
5576
  });
@@ -5385,8 +5583,9 @@ function buildLogSummary(lg){
5385
5583
  var scoreClass=f.score>=0.7?'high':f.score>=0.5?'mid':'low';
5386
5584
  var shortText=escapeHtml(f.summary||f.content||f.original_excerpt||'');
5387
5585
  var fullText=escapeHtml(f.content||f.original_excerpt||f.summary||'');
5586
+ var oBadge=recallOriginBadge(f.origin);
5388
5587
  html+='<div class="recall-item" onclick="event.stopPropagation();this.classList.toggle(\\\'expanded\\\')">';
5389
- html+='<div class="recall-item-head"><span class="recall-score '+scoreClass+'">'+f.score.toFixed(2)+'</span><span class="log-msg-role '+(f.role||'user')+'">'+(f.role||'user')+'</span><span class="recall-summary-short">'+shortText+'</span><span class="recall-expand-icon">\u25B6</span></div>';
5588
+ html+='<div class="recall-item-head"><span class="recall-score '+scoreClass+'">'+f.score.toFixed(2)+'</span><span class="log-msg-role '+(f.role||'user')+'">'+(f.role||'user')+'</span>'+oBadge+'<span class="recall-summary-short">'+shortText+'</span><span class="recall-expand-icon">\u25B6</span></div>';
5390
5589
  html+='<div class="recall-summary-full">'+fullText+'</div>';
5391
5590
  html+='</div>';
5392
5591
  });
@@ -5450,8 +5649,9 @@ function buildRecallDetailHtml(rd){
5450
5649
  var scoreClass=c.score>=0.7?'high':c.score>=0.5?'mid':'low';
5451
5650
  var shortText=escapeHtml(c.summary||c.content||c.original_excerpt||'');
5452
5651
  var fullText=escapeHtml(c.content||c.original_excerpt||c.summary||'');
5652
+ var oBadge=recallOriginBadge(c.origin);
5453
5653
  html+='<div class="recall-item" onclick="event.stopPropagation();this.classList.toggle(\\\'expanded\\\')">';
5454
- html+='<div class="recall-item-head"><span class="recall-idx">'+(i+1)+'</span><span class="recall-score '+scoreClass+'">'+c.score.toFixed(3)+'</span><span class="log-msg-role '+(c.role||'user')+'">'+(c.role||'user')+'</span><span class="recall-summary-short">'+shortText+'</span><span class="recall-expand-icon">\u25B6</span></div>';
5654
+ html+='<div class="recall-item-head"><span class="recall-idx">'+(i+1)+'</span><span class="recall-score '+scoreClass+'">'+c.score.toFixed(3)+'</span><span class="log-msg-role '+(c.role||'user')+'">'+(c.role||'user')+'</span>'+oBadge+'<span class="recall-summary-short">'+shortText+'</span><span class="recall-expand-icon">\u25B6</span></div>';
5455
5655
  html+='<div class="recall-summary-full">'+fullText+'</div>';
5456
5656
  html+='</div>';
5457
5657
  });
@@ -5465,8 +5665,9 @@ function buildRecallDetailHtml(rd){
5465
5665
  var scoreClass=f.score>=0.7?'high':f.score>=0.5?'mid':'low';
5466
5666
  var shortText=escapeHtml(f.summary||f.content||f.original_excerpt||'');
5467
5667
  var fullText=escapeHtml(f.content||f.original_excerpt||f.summary||'');
5668
+ var oBadge=recallOriginBadge(f.origin);
5468
5669
  html+='<div class="recall-item" onclick="event.stopPropagation();this.classList.toggle(\\\'expanded\\\')">';
5469
- html+='<div class="recall-item-head"><span class="recall-idx">'+(i+1)+'</span><span class="recall-score '+scoreClass+'">'+f.score.toFixed(3)+'</span><span class="log-msg-role '+(f.role||'user')+'">'+(f.role||'user')+'</span><span class="recall-summary-short">'+shortText+'</span><span class="recall-expand-icon">\u25B6</span></div>';
5670
+ html+='<div class="recall-item-head"><span class="recall-idx">'+(i+1)+'</span><span class="recall-score '+scoreClass+'">'+f.score.toFixed(3)+'</span><span class="log-msg-role '+(f.role||'user')+'">'+(f.role||'user')+'</span>'+oBadge+'<span class="recall-summary-short">'+shortText+'</span><span class="recall-expand-icon">\u25B6</span></div>';
5470
5671
  html+='<div class="recall-summary-full">'+fullText+'</div>';
5471
5672
  html+='</div>';
5472
5673
  });
@@ -6042,7 +6243,7 @@ async function loadSkills(silent){
6042
6243
  '<div class="summary">'+esc(skill.name)+'</div>'+
6043
6244
  '<div class="excerpt">'+esc(skill.description||'')+'</div>'+
6044
6245
  '<div class="hub-skill-meta">'+
6045
- '<span class="meta-chip">owner: '+esc(skill.ownerName||'unknown')+'</span>'+
6246
+ '<span class="meta-chip">owner: '+fmtOwner(skill)+'</span>'+
6046
6247
  (skill.groupName?'<span class="meta-chip">group: '+esc(skill.groupName)+'</span>':'')+
6047
6248
  '<span class="meta-chip">visibility: '+esc(skill.visibility||'hub')+'</span>'+
6048
6249
  (skill.version!=null?'<span class="meta-chip">v'+skill.version+'</span>':'')+
@@ -6089,12 +6290,12 @@ async function loadHubTasks(){
6089
6290
  return '<div class="task-card" onclick="openHubTaskDetailFromCache(\\x27hub\\x27,'+idx+')" style="cursor:pointer">'+
6090
6291
  '<div class="task-card-top">'+
6091
6292
  '<div class="task-card-title">'+esc(task.title||'(no title)')+'</div>'+
6092
- '<div class="task-card-badges"><span class="scope-badge team">\\u{1F310} '+t('scope.hub')+'</span></div>'+
6293
+ '<div class="task-card-badges">'+renderScopeBadge('team')+'</div>'+
6093
6294
  '</div>'+
6094
6295
  (task.summary?'<div class="task-card-summary">'+esc(task.summary)+'</div>':'')+
6095
6296
  '<div class="task-card-bottom">'+
6096
6297
  (timeStr?'<span class="tag"><span class="icon">\\u{1F4C5}</span> '+timeStr+'</span>':'')+
6097
- '<span class="tag"><span class="icon">\\u{1F464}</span> '+esc(task.ownerName||'unknown')+'</span>'+
6298
+ '<span class="tag"><span class="icon">\\u{1F464}</span> '+fmtOwner(task)+'</span>'+
6098
6299
  (task.chunkCount!=null?'<span class="tag"><span class="icon">\\u{1F4DD}</span> '+task.chunkCount+' '+t('tasks.chunks.label')+'</span>':'')+
6099
6300
  '</div>'+
6100
6301
  '</div>';
@@ -6127,12 +6328,12 @@ async function loadHubSkills(hubList, localIds){
6127
6328
  '<div class="summary">'+esc(skill.name)+'</div>'+
6128
6329
  '<div class="excerpt">'+esc(skill.description||'')+'</div>'+
6129
6330
  '<div class="hub-skill-meta">'+
6130
- '<span class="meta-chip">owner: '+esc(skill.ownerName||'unknown')+'</span>'+
6331
+ '<span class="meta-chip">owner: '+fmtOwner(skill)+'</span>'+
6131
6332
  (skill.groupName?'<span class="meta-chip">group: '+esc(skill.groupName)+'</span>':'')+
6132
6333
  '<span class="meta-chip">visibility: '+esc(skill.visibility||'hub')+'</span>'+
6133
6334
  (skill.version!=null?'<span class="meta-chip">v'+skill.version+'</span>':'')+
6134
6335
  '</div>'+
6135
- '<div class="hub-skill-actions"><button class="btn btn-sm" onclick="event.stopPropagation();pullHubSkill(\\''+escAttr(skill.sourceSkillId)+'\\')">'+t('skill.pullToLocal')+'</button></div>'+
6336
+ '<div class="hub-skill-actions"><button class="btn btn-sm" onclick="event.stopPropagation();pullHubSkill(\\''+escAttr(skill.id)+'\\')">'+t('skill.pullToLocal')+'</button></div>'+
6136
6337
  '</div>';
6137
6338
  }).join('');
6138
6339
  }catch(e){
@@ -6466,6 +6667,7 @@ async function loadConfig(){
6466
6667
  document.getElementById('cfgHubTeamName').value=hub.teamName||'';
6467
6668
  document.getElementById('cfgHubTeamToken').value=hub.teamToken||'';
6468
6669
  document.getElementById('cfgClientHubAddress').value=client.hubAddress||'';
6670
+ _loadedClientHubAddress=client.hubAddress||'';
6469
6671
  document.getElementById('cfgClientTeamToken').value=client.teamToken||'';
6470
6672
  document.getElementById('cfgClientNickname').value=client.nickname||'';
6471
6673
  document.getElementById('cfgClientUserToken').value=client.userToken||'';
@@ -6511,20 +6713,24 @@ function flashSaved(id){
6511
6713
  }
6512
6714
 
6513
6715
  async function doSaveConfig(cfg, btnEl, savedId){
6514
- btnEl.disabled=true;btnEl.textContent=t('settings.test.loading');
6515
- function done(){btnEl.disabled=false;btnEl.textContent=t('settings.save');}
6716
+ if(btnEl){btnEl.disabled=true;btnEl.textContent=t('settings.test.loading');}
6717
+ function done(){if(btnEl){btnEl.disabled=false;btnEl.textContent=t('settings.save');}}
6516
6718
  try{
6517
6719
  const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});
6518
- if(r.status===401){done();toast(t('settings.session.expired'),'error');return false;}
6720
+ if(r.status===401){done();toast(t('settings.session.expired'),'error');return null;}
6519
6721
  if(!r.ok) throw new Error(await r.text());
6722
+ var data=await r.json().catch(function(){return {ok:true};});
6520
6723
  flashSaved(savedId);
6521
- toast(t('settings.saved'),'success');
6522
- done();
6523
- return true;
6724
+ if(data&&data.restart){
6725
+ showRestartOverlay(t('settings.restart.waiting'));
6726
+ }else{
6727
+ done();
6728
+ }
6729
+ return data;
6524
6730
  }catch(e){
6525
6731
  toast(t('settings.save.fail')+': '+e.message,'error');
6526
6732
  done();
6527
- return false;
6733
+ return null;
6528
6734
  }
6529
6735
  }
6530
6736
 
@@ -6619,11 +6825,19 @@ async function saveModelsConfig(){
6619
6825
  await doSaveConfig(cfg, saveBtn, 'modelsSaved');
6620
6826
  }
6621
6827
 
6828
+ function _hubSaveBtnLabel(){
6829
+ var on=document.getElementById('cfgSharingEnabled');
6830
+ if(on&&on.checked&&_sharingRole==='client'){
6831
+ var prevClient=sharingStatusCache&&sharingStatusCache.enabled&&sharingStatusCache.role==='client';
6832
+ return prevClient?t('settings.save'):t('sharing.joinTeam');
6833
+ }
6834
+ return t('settings.save');
6835
+ }
6622
6836
  async function saveHubConfig(){
6623
6837
  var card=document.getElementById('settingsSharingConfig');
6624
6838
  var saveBtn=card.querySelector('.settings-actions .btn-primary');
6625
6839
  saveBtn.disabled=true;saveBtn.textContent=t('settings.test.loading');
6626
- function done(){saveBtn.disabled=false;saveBtn.textContent=t('settings.save');}
6840
+ function done(){saveBtn.disabled=false;saveBtn.textContent=_hubSaveBtnLabel();}
6627
6841
 
6628
6842
  const cfg={};
6629
6843
  var sharingEnabled=document.getElementById('cfgSharingEnabled').checked;
@@ -6637,7 +6851,7 @@ async function saveHubConfig(){
6637
6851
  var hubTeamName=document.getElementById('cfgHubTeamName').value.trim();
6638
6852
  var hubTeamToken=document.getElementById('cfgHubTeamToken').value.trim();
6639
6853
  var hubAdminName=document.getElementById('cfgHubAdminName').value.trim();
6640
- cfg.sharing.hub={port:hubPort?Number(hubPort):18800};
6854
+ if(hubPort) cfg.sharing.hub={port:Number(hubPort)}; else cfg.sharing.hub={};
6641
6855
  if(hubTeamName) cfg.sharing.hub.teamName=hubTeamName;
6642
6856
  if(hubTeamToken) cfg.sharing.hub.teamToken=hubTeamToken;
6643
6857
  cfg.sharing.client={hubAddress:'',userToken:'',teamToken:''};
@@ -6654,15 +6868,9 @@ async function saveHubConfig(){
6654
6868
  if(clientNickname) cfg.sharing.client.nickname=clientNickname;
6655
6869
  if(clientTeamToken) cfg.sharing.client.teamToken=clientTeamToken;
6656
6870
  if(clientUserToken) cfg.sharing.client.userToken=clientUserToken;
6657
- cfg.sharing.hub={port:18800,teamName:'',teamToken:''};
6871
+ cfg.sharing.hub={teamName:'',teamToken:''};
6658
6872
  if(clientAddr){
6659
6873
  try{
6660
- var ips=await fetch('/api/local-ips').then(function(r){return r.json();});
6661
- var localAddrs=['127.0.0.1','localhost','0.0.0.0'].concat(ips.ips||[]);
6662
- var parsed=new URL(clientAddr.indexOf('://')>-1?clientAddr:'http://'+clientAddr);
6663
- if(localAddrs.indexOf(parsed.hostname)>=0){
6664
- done();toast(t('sharing.cannotJoinSelf'),'error');return;
6665
- }
6666
6874
  }catch(e){}
6667
6875
  try{
6668
6876
  var testUrl=clientAddr.indexOf('://')>-1?clientAddr:'http://'+clientAddr;
@@ -6672,8 +6880,8 @@ async function saveHubConfig(){
6672
6880
  if(!td.ok){
6673
6881
  var errMsg=td.error==='cannot_join_self'?t('sharing.cannotJoinSelf'):(td.error||t('settings.hub.test.fail'));
6674
6882
  done();toast(errMsg,'error');return;
6675
- }
6676
- }catch(e){
6883
+ }
6884
+ }catch(e){
6677
6885
  done();toast(t('settings.hub.test.fail')+': '+String(e),'error');return;
6678
6886
  }
6679
6887
  }
@@ -6689,25 +6897,40 @@ async function saveHubConfig(){
6689
6897
  var switchMsg=prevRole==='hub'?t('sharing.switch.hubToClient'):t('sharing.switch.clientToHub');
6690
6898
  if(!(await confirmModal(switchMsg,{danger:true}))){done();return;}
6691
6899
  }
6900
+ if(prevSharingEnabled&&sharingEnabled&&prevRole==='client'&&_sharingRole==='client'){
6901
+ var newAddr=(document.getElementById('cfgClientHubAddress').value||'').trim();
6902
+ if(_loadedClientHubAddress&&newAddr&&newAddr!==_loadedClientHubAddress){
6903
+ if(!(await confirmModal(t('sharing.switch.hubAddress'),{danger:true}))){done();return;}
6904
+ }
6905
+ }
6692
6906
 
6693
- var ok=await doSaveConfig(cfg, saveBtn, 'hubSaved');
6694
- if(ok){
6907
+ var result=await doSaveConfig(cfg, saveBtn, 'hubSaved');
6908
+ if(result){
6695
6909
  if(sharingEnabled&&_sharingRole==='hub'){
6696
6910
  var adminNameEl=document.getElementById('cfgHubAdminName');
6697
6911
  if(adminNameEl&&adminNameEl.value.trim()){
6698
6912
  try{await fetch('/api/sharing/update-username',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:adminNameEl.value.trim()})});}catch(e){}
6699
6913
  }
6700
6914
  }
6915
+ if(sharingEnabled&&_sharingRole==='client'&&result.joinStatus){
6916
+ if(result.joinStatus==='pending'){
6917
+ toast(t('sharing.joinSent.pending'),'success');
6918
+ }else if(result.joinStatus==='active'){
6919
+ toast(t('sharing.joinSent.active'),'success');
6920
+ }else{
6921
+ toast(t('settings.saved'),'success');
6922
+ }
6923
+ }else{
6924
+ toast(t('settings.saved'),'success');
6925
+ }
6701
6926
  _lastSidebarFingerprint='';
6702
6927
  _lastSettingsFingerprint='';
6703
- loadSharingStatus(false);
6704
- var enabledChanged=(!!prevSharingEnabled)!==(!!sharingEnabled);
6705
- var roleChanged=prevRole!==_sharingRole;
6706
- var needsRestart=enabledChanged||roleChanged;
6928
+ _lastSharingConnStatus='';
6929
+ _lastAdminFingerprint='';
6930
+ _lastAdminStatsFp='';
6707
6931
  if(sharingEnabled) updateHubShareInfo();
6708
- if(needsRestart){
6709
- setTimeout(function(){showRestartOverlay(t('settings.restart.waiting'));},300);
6710
- }
6932
+ loadSharingStatus(true);
6933
+ if(_activeView==='admin') loadAdminData();
6711
6934
  }
6712
6935
  }
6713
6936
 
@@ -6797,6 +7020,8 @@ function renderSkillMarkdown(md){
6797
7020
  function closeSkillDetail(event){
6798
7021
  if(event && event.target!==document.getElementById('skillDetailOverlay')) return;
6799
7022
  document.getElementById('skillDetailOverlay').classList.remove('show');
7023
+ currentSkillId='';
7024
+ currentSkillDetail=null;
6800
7025
  }
6801
7026
 
6802
7027
  async function deleteSkill(skillId){
@@ -7131,22 +7356,29 @@ var _livePollBusy=false;
7131
7356
  async function _livePollTick(){
7132
7357
  if(_livePollBusy||document.hidden) return;
7133
7358
  _livePollBusy=true;
7359
+ var _savedScrollY=window.scrollY;
7360
+ var _scrollTargets=['memoryList','tasksList','skillsList','adminUsersPanel','adminMemoriesPanel','adminTasksPanel','adminSkillsPanel'];
7361
+ var _savedScrollMap={};
7362
+ _scrollTargets.forEach(function(id){var el=document.getElementById(id);if(el&&el.scrollTop)_savedScrollMap[id]=el.scrollTop;});
7134
7363
  try{
7135
- if(sharingStatusCache&&sharingStatusCache.enabled&&_lastSharingConnStatus!=='rejected') loadSharingStatus(false);
7136
- if(!_notifSSEConnected) pollNotifCount();
7137
- pollAdminPending();
7138
- if(_activeView==='admin') loadAdminData();
7364
+ if(sharingStatusCache&&sharingStatusCache.enabled&&_lastSharingConnStatus!=='rejected') await loadSharingStatus(false);
7365
+ if(!_notifSSEConnected) await pollNotifCount();
7366
+ await pollAdminPending();
7367
+ if(_activeView==='admin') await loadAdminData();
7139
7368
  else if(_activeView==='memories'){
7140
7369
  var _searchVal=(document.getElementById('searchInput')||{}).value||'';
7141
7370
  if(!_searchVal.trim()){
7142
- if(memorySearchScope==='hub') loadHubMemories(true);
7143
- else{loadStats();loadMemories(null,true);}
7371
+ if(memorySearchScope==='hub') await loadHubMemories(true);
7372
+ else{var _pollOwner=memorySearchScope==='local'?_currentAgentOwner:undefined;await loadStats(_pollOwner);await loadMemories(null,true);}
7144
7373
  }
7145
7374
  }
7146
- else if(_activeView==='tasks') loadTasks(true);
7147
- else if(_activeView==='skills') loadSkills(true);
7148
- else if(_activeView==='analytics') loadMetrics();
7375
+ else if(_activeView==='tasks') await loadTasks(true);
7376
+ else if(_activeView==='skills') await loadSkills(true);
7377
+ else if(_activeView==='analytics') await loadMetrics();
7149
7378
  }catch(e){}
7379
+ await new Promise(function(r){requestAnimationFrame(r);});
7380
+ window.scrollTo(0,_savedScrollY);
7381
+ for(var _sid in _savedScrollMap){var _sel=document.getElementById(_sid);if(_sel)_sel.scrollTop=_savedScrollMap[_sid];}
7150
7382
  _livePollBusy=false;
7151
7383
  }
7152
7384
 
@@ -7190,6 +7422,9 @@ function connectNotifSSE(){
7190
7422
  _notifUnread=d.unreadCount||0;
7191
7423
  renderNotifBadge();
7192
7424
  if(_notifUnread>prev&&_notifPanelOpen) loadNotifications();
7425
+ if(_notifUnread>prev&&_activeView==='memories'&&memorySearchScope!=='hub'){
7426
+ syncTeamShareRemovedFromNotifications().then(function(){ loadMemories(currentPage,true); });
7427
+ }
7193
7428
  }
7194
7429
  if(d.type==='cleared'){
7195
7430
  _notifUnread=0;_notifCache=[];
@@ -7239,7 +7474,12 @@ function notifTimeAgo(ts){
7239
7474
  function notifIcon(resource,type){
7240
7475
  if(type==='user_online') return '\\u{1F7E2}';
7241
7476
  if(type==='user_offline') return '\\u{1F534}';
7477
+ if(type==='user_left') return '\\u{1F6AA}';
7242
7478
  if(type==='user_join_request') return '\\u{1F464}';
7479
+ if(type==='membership_removed') return '\\u{26D4}';
7480
+ if(type==='hub_shutdown') return '\\u{1F6D1}';
7481
+ if(type==='role_promoted') return '\\u{2B06}';
7482
+ if(type==='role_demoted') return '\\u{2B07}';
7243
7483
  if(resource==='memory') return '\\u{1F4DD}';
7244
7484
  if(resource==='task') return '\\u{1F4CB}';
7245
7485
  if(resource==='skill') return '\\u{1F9E0}';
@@ -7265,6 +7505,27 @@ function notifTypeText(n){
7265
7505
  if(n.type==='user_offline'){
7266
7506
  return t('notif.userOffline');
7267
7507
  }
7508
+ if(n.type==='user_left'){
7509
+ return t('notif.userLeft');
7510
+ }
7511
+ if(n.type==='membership_approved'){
7512
+ return t('notif.membershipApproved');
7513
+ }
7514
+ if(n.type==='membership_rejected'){
7515
+ return t('notif.membershipRejected');
7516
+ }
7517
+ if(n.type==='membership_removed'){
7518
+ return t('notif.membershipRemoved');
7519
+ }
7520
+ if(n.type==='hub_shutdown'){
7521
+ return t('notif.hubShutdown');
7522
+ }
7523
+ if(n.type==='role_promoted'){
7524
+ return t('notif.rolePromoted');
7525
+ }
7526
+ if(n.type==='role_demoted'){
7527
+ return t('notif.roleDemoted');
7528
+ }
7268
7529
  return n.message||n.type;
7269
7530
  }
7270
7531
 
@@ -7299,6 +7560,21 @@ function renderNotifBadge(){
7299
7560
  }
7300
7561
  }
7301
7562
 
7563
+ var _notifKnownTypes={membership_approved:1,membership_rejected:1,membership_removed:1,hub_shutdown:1,user_left:1,user_online:1,user_offline:1,user_join_request:1,role_promoted:1,role_demoted:1,resource_removed:1,resource_shared:1,resource_unshared:1};
7564
+ function notifDisplayTitle(n){
7565
+ if(_notifKnownTypes[n.type]) return notifTypeText(n);
7566
+ return n.title||notifTypeText(n);
7567
+ }
7568
+ function notifDisplayDetail(n){
7569
+ if(_notifKnownTypes[n.type]){
7570
+ if(n.type==='resource_removed'||n.type==='resource_shared'||n.type==='resource_unshared') return n.title||'';
7571
+ var m=n.title&&n.title.match(/["\u201C]([^"\u201D]+)["\u201D]/);
7572
+ if(m) return m[1];
7573
+ if(n.type==='user_left'||n.type==='user_online'||n.type==='user_offline'||n.type==='user_join_request') return n.title||'';
7574
+ return '';
7575
+ }
7576
+ return n.title||'';
7577
+ }
7302
7578
  function renderNotifPanel(){
7303
7579
  var body=document.getElementById('notifPanelBody');
7304
7580
  if(!body) return;
@@ -7308,11 +7584,12 @@ function renderNotifPanel(){
7308
7584
  }
7309
7585
  body.innerHTML=_notifCache.map(function(n){
7310
7586
  var cls='notif-item'+(n.read?'':' unread');
7587
+ var detail=notifDisplayDetail(n);
7311
7588
  return '<div class="'+cls+'" onclick="markNotifRead(&quot;'+esc(n.id)+'&quot;)">'+
7312
7589
  '<div class="notif-item-icon">'+notifIcon(n.resource,n.type)+'</div>'+
7313
7590
  '<div class="notif-item-body">'+
7314
- '<div class="notif-item-title">'+esc(notifTypeText(n))+'</div>'+
7315
- '<div class="notif-item-name">'+esc(n.title)+'</div>'+
7591
+ '<div class="notif-item-title">'+esc(notifDisplayTitle(n))+'</div>'+
7592
+ (detail?'<div class="notif-item-name">'+esc(detail)+'</div>':'')+
7316
7593
  '<div class="notif-item-time">'+notifTimeAgo(n.createdAt)+'</div>'+
7317
7594
  '</div>'+
7318
7595
  '<div class="notif-item-dot"></div>'+
@@ -7366,6 +7643,7 @@ async function loadAll(){
7366
7643
  startLivePoller();
7367
7644
  }
7368
7645
 
7646
+ var _lastStatsFp='';
7369
7647
  async function loadStats(ownerFilter){
7370
7648
  let d;
7371
7649
  try{
@@ -7380,24 +7658,17 @@ async function loadStats(ownerFilter){
7380
7658
  const dedupB=d.dedupBreakdown||{};
7381
7659
  const activeCount=dedupB.active||tm;
7382
7660
  const inactiveCount=(dedupB.duplicate||0)+(dedupB.merged||0);
7661
+ var agentCount=(d.owners&&d.owners.length)?d.owners.length:1;
7662
+ var sfp=tm+':'+(d.totalSessions||0)+':'+(d.totalEmbeddings||0)+':'+agentCount+':'+(d.embeddingProvider||'none')+':'+(ownerFilter||'');
7663
+ if(sfp===_lastStatsFp) return;
7664
+ _lastStatsFp=sfp;
7383
7665
  document.getElementById('statTotal').textContent=tm;
7384
7666
  if(inactiveCount>0){
7385
7667
  document.getElementById('statTotal').title=activeCount+' '+t('stat.active')+', '+inactiveCount+' '+t('stat.deduped');
7386
7668
  }
7387
7669
  document.getElementById('statSessions').textContent=d.totalSessions||0;
7388
7670
  document.getElementById('statEmbeddings').textContent=d.totalEmbeddings||0;
7389
- let days=0;
7390
- if(d.timeRange&&d.timeRange.earliest!=null&&d.timeRange.latest!=null){
7391
- let e=Number(d.timeRange.earliest), l=Number(d.timeRange.latest);
7392
- if(Number.isFinite(e)&&Number.isFinite(l)){
7393
- if(e<1e12) e*=1000;
7394
- if(l<1e12) l*=1000;
7395
- days=Math.round((l-e)/86400000);
7396
- days=Math.max(0,Math.min(36500,days));
7397
- if(days===0) days=1;
7398
- }
7399
- }
7400
- document.getElementById('statTimeSpan').textContent=days;
7671
+ document.getElementById('statAgents').textContent=agentCount;
7401
7672
 
7402
7673
  const provEl=document.getElementById('embeddingStatus');
7403
7674
  if(d.embeddingProvider && d.embeddingProvider!=='none'){
@@ -7492,18 +7763,38 @@ function getFilterParams(){
7492
7763
  const scope=memorySearchScope||'local';
7493
7764
  if(scope==='local'){
7494
7765
  p.set('owner',_currentAgentOwner);
7495
- }else{
7496
- const owner=document.getElementById('filterOwner').value;
7497
- if(owner) p.set('owner',owner);
7766
+ }else if(scope==='allLocal'){
7767
+ const owner=document.getElementById('filterOwner').value;
7768
+ if(owner) p.set('owner',owner);
7498
7769
  }
7499
7770
  return p;
7500
7771
  }
7501
7772
 
7773
+ /** Hub admin removed a shared memory — badge-only: clear team_shared_chunks (never touches chunks/embeddings/hub_memories recall data). */
7774
+ async function syncTeamShareRemovedFromNotifications(){
7775
+ try{
7776
+ var r=await fetch('/api/sharing/notifications');
7777
+ var d=await r.json();
7778
+ var list=d.notifications||[];
7779
+ for(var i=0;i<list.length;i++){
7780
+ var n=list[i];
7781
+ if(n.type!=='resource_removed'||n.resource!=='memory'||!n.message) continue;
7782
+ try{
7783
+ var meta=JSON.parse(n.message);
7784
+ if(meta.sourceChunkId){
7785
+ await fetch('/api/sharing/sync-hub-removal',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sourceChunkId:meta.sourceChunkId,memoryId:meta.memoryId||''})});
7786
+ }
7787
+ }catch(e){}
7788
+ }
7789
+ }catch(e){}
7790
+ }
7791
+
7502
7792
  async function loadMemories(page,silent){
7503
7793
  if(page) currentPage=page;
7504
7794
  const list=document.getElementById('memoryList');
7505
7795
  if(!silent) list.innerHTML='<div class="spinner"></div>';
7506
7796
  try{
7797
+ if(!silent) await syncTeamShareRemovedFromNotifications();
7507
7798
  const p=getFilterParams();
7508
7799
  p.set('limit',PAGE_SIZE);
7509
7800
  p.set('page',currentPage);
@@ -7524,11 +7815,11 @@ async function loadMemories(page,silent){
7524
7815
  renderPagination();
7525
7816
  }catch(e){
7526
7817
  if(!silent){
7527
- list.innerHTML='';
7528
- totalPages=1;totalCount=0;
7818
+ list.innerHTML='';
7819
+ totalPages=1;totalCount=0;
7529
7820
  _lastMemoriesFingerprint='';
7530
- renderMemories([]);
7531
- renderPagination();
7821
+ renderMemories([]);
7822
+ renderPagination();
7532
7823
  }
7533
7824
  }
7534
7825
  }
@@ -7555,9 +7846,9 @@ async function loadHubMemories(silent){
7555
7846
  }catch(e){
7556
7847
  if(!silent){
7557
7848
  _lastMemoriesFingerprint='';
7558
- document.getElementById('searchMeta').textContent='0'+t('search.meta.results');
7559
- renderMemories([]);
7560
- document.getElementById('pagination').innerHTML='';
7849
+ document.getElementById('searchMeta').textContent='0'+t('search.meta.results');
7850
+ renderMemories([]);
7851
+ document.getElementById('pagination').innerHTML='';
7561
7852
  }
7562
7853
  }
7563
7854
  }
@@ -7678,7 +7969,7 @@ function renderMemories(items){
7678
7969
  const mergeBadge=mc>0?'<span class="merge-badge">\\u{1F504} '+t('card.evolved')+' '+mc+t('card.times')+'</span>':'';
7679
7970
  const updatedAt=(m.updated_at&&m.updated_at>m.created_at)?'<span class="card-updated">'+t('card.updated')+' '+new Date(m.updated_at).toLocaleString(dateLoc())+'</span>':'';
7680
7971
  const ds=m.dedup_status||'active';
7681
- const isInactive=ds==='merged';
7972
+ const isInactive=ds==='merged'||ds==='duplicate';
7682
7973
  const dedupBadge=ds==='duplicate'?'<span class="dedup-badge duplicate">'+t('card.dedupDuplicate')+'</span>':ds==='merged'?'<span class="dedup-badge merged">'+t('card.dedupMerged')+'</span>':'';
7683
7974
  const isImported=sid.startsWith('openclaw-import-')||sid.startsWith('openclaw-session-');
7684
7975
  const importBadge=isImported?'<span class="import-badge">\u{1F990} '+t('card.imported')+'</span>':'';
@@ -7687,8 +7978,8 @@ function renderMemories(items){
7687
7978
  const localManaged=!!m.localSharingManaged;
7688
7979
  const memShared=m.sharingVisibility||null;
7689
7980
  const isHubScope=memorySearchScope==='hub';
7690
- const memScope=isHubScope?'team':memShared?'team':isPublicMem?'local':'private';
7691
- const memScopeBadge=renderScopeBadge(memScope);
7981
+ const memScope=memShared?'team':isPublicMem?'local':'private';
7982
+ const memScopeBadge=isHubScope?renderScopeBadge('team'):renderScopeBadge(memScope);
7692
7983
  let dedupInfo='';
7693
7984
  if(ds==='duplicate'||ds==='merged'){
7694
7985
  const reason=m.dedup_reason?'<span style="font-size:11px;color:var(--text-muted)">'+t('card.dedupReason')+esc(m.dedup_reason)+'</span>':'';
@@ -7873,6 +8164,11 @@ function esc(s){
7873
8164
  if(!s)return'';
7874
8165
  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
7875
8166
  }
8167
+ function fmtOwner(item){
8168
+ var name=item.ownerName||item.sourceUserId||'unknown';
8169
+ if(item.ownerStatus==='removed') return esc(name)+' <span style="color:#ef4444;font-size:0.9em">'+t('sharing.ownerRemoved')+'</span>';
8170
+ return esc(name);
8171
+ }
7876
8172
 
7877
8173
  function renderSummaryHtml(raw){
7878
8174
  if(!raw)return'';
@@ -8599,18 +8895,25 @@ function confirmModal(message,opts){
8599
8895
  _confirmResolve=resolve;
8600
8896
  var overlay=document.getElementById('confirmOverlay');
8601
8897
  document.getElementById('confirmTitle').textContent=opts.title||t('confirm.title')||'\u786E\u8BA4';
8602
- document.getElementById('confirmBody').textContent=message||'';
8898
+ document.getElementById('confirmBody').innerText=message||'';
8603
8899
  var okBtn=document.getElementById('confirmOkBtn');
8604
8900
  okBtn.textContent=opts.okText||t('confirm.ok')||'\u786E\u5B9A';
8605
8901
  okBtn.className='btn-confirm-ok'+(opts.danger?' danger':'');
8606
- document.getElementById('confirmCancelBtn').textContent=opts.cancelText||t('confirm.cancel')||'\u53D6\u6D88';
8902
+ var cancelBtn=document.getElementById('confirmCancelBtn');
8903
+ cancelBtn.textContent=opts.cancelText||t('confirm.cancel')||'\u53D6\u6D88';
8904
+ cancelBtn.style.display=opts.hideCancel?'none':'';
8607
8905
  overlay.classList.add('show');
8608
8906
  });
8609
8907
  }
8610
8908
  function confirmModalClose(result){
8611
8909
  document.getElementById('confirmOverlay').classList.remove('show');
8910
+ document.getElementById('confirmCancelBtn').style.display='';
8612
8911
  if(_confirmResolve){var r=_confirmResolve;_confirmResolve=null;r(result);}
8613
8912
  }
8913
+ function alertModal(message,opts){
8914
+ opts=opts||{};
8915
+ return confirmModal(message,Object.assign({},opts,{hideCancel:true,okText:opts.okText||t('confirm.ok')||'\u77E5\u9053\u4E86'}));
8916
+ }
8614
8917
 
8615
8918
  /* ─── Theme ─── */
8616
8919
  const VIEWER_THEME_KEY='memos-viewer-theme';
@@ -8633,14 +8936,35 @@ function showRestartOverlay(msg){
8633
8936
  /* ─── Update check ─── */
8634
8937
  function waitForGatewayAndReload(maxAttempts,attempt){
8635
8938
  attempt=attempt||0;
8939
+ var phase=arguments.length>2?arguments[2]:'waitDown';
8940
+ var MAX_WAIT_DOWN=8;
8636
8941
  function forceReload(){window.location.href=window.location.pathname+'?_t='+Date.now();}
8637
8942
  if(attempt>=maxAttempts){forceReload();return;}
8943
+ var delay=phase==='waitDown'?1500:2500;
8638
8944
  setTimeout(function(){
8639
8945
  fetch('/api/auth/status').then(function(r){
8640
- if(r.ok||r.status===401||r.status===403) forceReload();
8641
- else waitForGatewayAndReload(maxAttempts,attempt+1);
8642
- }).catch(function(){waitForGatewayAndReload(maxAttempts,attempt+1);});
8643
- },3000);
8946
+ if(phase==='waitDown'){
8947
+ if(r.ok||r.status===401||r.status===403){
8948
+ if(attempt>=MAX_WAIT_DOWN){
8949
+ forceReload();
8950
+ }else{
8951
+ waitForGatewayAndReload(maxAttempts,attempt+1,'waitDown');
8952
+ }
8953
+ }else{
8954
+ waitForGatewayAndReload(maxAttempts,0,'waitUp');
8955
+ }
8956
+ }else{
8957
+ if(r.ok||r.status===401||r.status===403) forceReload();
8958
+ else waitForGatewayAndReload(maxAttempts,attempt+1,'waitUp');
8959
+ }
8960
+ }).catch(function(){
8961
+ if(phase==='waitDown'){
8962
+ waitForGatewayAndReload(maxAttempts,0,'waitUp');
8963
+ }else{
8964
+ waitForGatewayAndReload(maxAttempts,attempt+1,'waitUp');
8965
+ }
8966
+ });
8967
+ },delay);
8644
8968
  }
8645
8969
  function doUpdateInstall(packageSpec,btnEl,statusEl){
8646
8970
  btnEl.disabled=true;
@@ -8674,9 +8998,12 @@ async function checkForUpdate(){
8674
8998
  const d=await r.json();
8675
8999
  if(!d.updateAvailable)return;
8676
9000
  const pkgSpec=d.installCommand?d.installCommand.replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/,''):(d.packageName+'@'+d.latest);
9001
+ var bannerWrap=document.createElement('div');
9002
+ bannerWrap.id='updateBannerWrap';
9003
+ bannerWrap.style.cssText='background:linear-gradient(135deg,rgba(99,102,241,.08),rgba(139,92,246,.06));border-bottom:1px solid rgba(99,102,241,.18);backdrop-filter:blur(8px);animation:slideIn .3s ease';
8677
9004
  var banner=document.createElement('div');
8678
9005
  banner.id='updateBanner';
8679
- banner.style.cssText='display:flex;align-items:center;gap:12px;padding:10px 32px;max-width:1400px;margin:0 auto;width:100%;font-size:13px;font-weight:500;box-sizing:border-box;animation:slideIn .3s ease;background:linear-gradient(135deg,rgba(99,102,241,.08),rgba(139,92,246,.06));color:var(--pri);border-bottom:1px solid rgba(99,102,241,.18);backdrop-filter:blur(8px)';
9006
+ banner.style.cssText='display:flex;align-items:center;gap:12px;padding:10px 32px;width:100%;max-width:1400px;margin:0 auto;font-size:13px;font-weight:500;box-sizing:border-box;color:var(--pri)';
8680
9007
  var textNode=document.createElement('div');
8681
9008
  textNode.style.cssText='display:flex;align-items:center;gap:8px;flex-shrink:0;font-size:13px';
8682
9009
  textNode.innerHTML='<span style="font-size:15px">\u2728</span> '+t('update.available')+' <span style="padding:2px 8px;border-radius:6px;background:rgba(99,102,241,.1);font-size:12px;font-weight:600">v'+esc(d.current)+'</span> <span style="opacity:.5">\u2192</span> <span style="padding:2px 8px;border-radius:6px;background:rgba(52,211,153,.12);color:var(--green);font-size:12px;font-weight:600">v'+esc(d.latest)+'</span>';
@@ -8696,18 +9023,30 @@ async function checkForUpdate(){
8696
9023
  btnClose.innerHTML='&times;';
8697
9024
  btnClose.onmouseenter=function(){this.style.opacity='1'};
8698
9025
  btnClose.onmouseleave=function(){this.style.opacity='.5'};
8699
- btnClose.onclick=function(){banner.remove()};
9026
+ btnClose.onclick=function(){bannerWrap.remove()};
9027
+ var spacerL=document.createElement('div');
9028
+ spacerL.style.cssText='flex:1';
9029
+ banner.appendChild(spacerL);
8700
9030
  banner.appendChild(textNode);
8701
9031
  banner.appendChild(statusDiv);
8702
9032
  banner.appendChild(spacer);
8703
9033
  banner.appendChild(btnClose);
9034
+ bannerWrap.appendChild(banner);
8704
9035
  var tb=document.querySelector('.topbar');
8705
- if(tb&&tb.parentNode){tb.parentNode.insertBefore(banner,tb);}
8706
- else{document.body.insertBefore(banner,document.body.firstChild);}
9036
+ if(tb&&tb.parentNode){tb.parentNode.insertBefore(bannerWrap,tb);}
9037
+ else{document.body.insertBefore(bannerWrap,document.body.firstChild);}
8707
9038
  }catch(e){}
8708
9039
  }
8709
9040
 
8710
9041
  /* ─── Init ─── */
9042
+ try{
9043
+ var savedScope=localStorage.getItem('memos_memorySearchScope');
9044
+ if(savedScope&&(savedScope==='local'||savedScope==='allLocal'||savedScope==='hub')){
9045
+ memorySearchScope=savedScope;
9046
+ var scopeEl=document.getElementById('memorySearchScope');
9047
+ if(scopeEl) scopeEl.value=savedScope;
9048
+ }
9049
+ }catch(e){}
8711
9050
  document.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});
8712
9051
  document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';currentPage=1;if(memorySearchScope==='hub')loadHubMemories();else loadMemories();}});
8713
9052
  applyI18n();