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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +23 -23
  2. package/dist/capture/index.d.ts +1 -1
  3. package/dist/capture/index.d.ts.map +1 -1
  4. package/dist/capture/index.js +28 -2
  5. package/dist/capture/index.js.map +1 -1
  6. package/dist/client/connector.d.ts +1 -2
  7. package/dist/client/connector.d.ts.map +1 -1
  8. package/dist/client/connector.js +18 -19
  9. package/dist/client/connector.js.map +1 -1
  10. package/dist/client/hub.d.ts.map +1 -1
  11. package/dist/client/hub.js +22 -0
  12. package/dist/client/hub.js.map +1 -1
  13. package/dist/client/skill-sync.d.ts +7 -0
  14. package/dist/client/skill-sync.d.ts.map +1 -1
  15. package/dist/client/skill-sync.js +10 -0
  16. package/dist/client/skill-sync.js.map +1 -1
  17. package/dist/hub/server.d.ts.map +1 -1
  18. package/dist/hub/server.js +101 -81
  19. package/dist/hub/server.js.map +1 -1
  20. package/dist/hub/user-manager.d.ts +2 -0
  21. package/dist/hub/user-manager.d.ts.map +1 -1
  22. package/dist/hub/user-manager.js +5 -1
  23. package/dist/hub/user-manager.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +5 -2
  26. package/dist/index.js.map +1 -1
  27. package/dist/storage/sqlite.d.ts +54 -20
  28. package/dist/storage/sqlite.d.ts.map +1 -1
  29. package/dist/storage/sqlite.js +185 -101
  30. package/dist/storage/sqlite.js.map +1 -1
  31. package/dist/tools/memory-search.d.ts +3 -1
  32. package/dist/tools/memory-search.d.ts.map +1 -1
  33. package/dist/tools/memory-search.js +3 -1
  34. package/dist/tools/memory-search.js.map +1 -1
  35. package/dist/viewer/html.d.ts.map +1 -1
  36. package/dist/viewer/html.js +1619 -629
  37. package/dist/viewer/html.js.map +1 -1
  38. package/dist/viewer/server.d.ts +14 -8
  39. package/dist/viewer/server.d.ts.map +1 -1
  40. package/dist/viewer/server.js +545 -141
  41. package/dist/viewer/server.js.map +1 -1
  42. package/index.ts +355 -41
  43. package/package.json +1 -1
  44. package/skill/memos-memory-guide/SKILL.md +64 -26
  45. package/src/capture/index.ts +29 -1
  46. package/src/client/connector.ts +15 -21
  47. package/src/client/hub.ts +18 -0
  48. package/src/client/skill-sync.ts +14 -0
  49. package/src/hub/server.ts +88 -74
  50. package/src/hub/user-manager.ts +7 -3
  51. package/src/index.ts +7 -2
  52. package/src/storage/sqlite.ts +192 -122
  53. package/src/tools/memory-search.ts +2 -1
  54. package/src/viewer/html.ts +1619 -629
  55. package/src/viewer/server.ts +506 -128
@@ -221,13 +221,71 @@ input,textarea,select{font-family:inherit;font-size:inherit}
221
221
  .admin-tab.active .at-count{background:rgba(99,102,241,.15)}
222
222
  .admin-panel{display:none}
223
223
  .admin-panel.active{display:block}
224
- .admin-card{border:1px solid var(--border);border-radius:12px;padding:14px 16px;background:var(--bg-card);margin-bottom:10px;transition:all .2s;position:relative;overflow:hidden}
224
+ .admin-card{border:1px solid var(--border);border-radius:12px;padding:16px 18px;background:var(--bg-card);margin-bottom:10px;transition:all .2s;position:relative;overflow:hidden}
225
+ .admin-card-clickable{cursor:pointer}
225
226
  .admin-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px;background:var(--pri);opacity:.5}
226
227
  .admin-card:hover{border-color:rgba(99,102,241,.25);box-shadow:0 2px 12px rgba(0,0,0,.04);transform:translateY(-1px)}
227
228
  .admin-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
228
- .admin-card-title{font-size:14px;font-weight:700;color:var(--text)}
229
+ .admin-card-title{font-size:14px;font-weight:700;color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
229
230
  .admin-card-meta{font-size:12px;color:var(--text-muted);line-height:1.5}
230
- .admin-card-actions{display:flex;gap:8px;margin-top:10px}
231
+ .au-contrib{display:flex;gap:16px;padding:10px 0;margin:6px 0;border-top:1px solid var(--border);border-bottom:1px solid var(--border)}
232
+ .au-contrib-item{font-size:12px;color:var(--text-sec);display:flex;align-items:center;gap:4px}
233
+ .au-contrib-num{font-size:18px;font-weight:700;line-height:1}
234
+ .au-info{display:flex;flex-wrap:wrap;gap:6px 14px;padding:8px 0;font-size:12px}
235
+ .au-info-item{color:var(--text-muted);white-space:nowrap}
236
+ .admin-card-tags{display:flex;gap:5px;margin:8px 0;align-items:center}
237
+ .admin-card-tags-left{display:flex;flex-wrap:wrap;gap:5px;align-items:center;flex:1;min-width:0}
238
+ .admin-card-tag{display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:6px;font-size:11px;font-weight:500}
239
+ .admin-card-tag.tag-role{background:rgba(99,102,241,.1);color:var(--pri)}
240
+ .admin-card-tag.tag-kind{background:rgba(245,158,11,.1);color:#f59e0b}
241
+ .admin-card-tag.tag-owner{background:rgba(34,197,94,.1);color:#22c55e}
242
+ .admin-card-tag.tag-status{background:rgba(6,182,212,.1);color:#06b6d4}
243
+ .admin-card-tag.tag-version{background:rgba(139,92,246,.1);color:#8b5cf6}
244
+ .admin-card-tag.tag-visibility{background:rgba(99,102,241,.08);color:var(--pri)}
245
+ .admin-card-tag.tag-group{background:rgba(139,92,246,.08);color:#8b5cf6}
246
+ .admin-card-preview{font-size:12px;color:var(--text-sec);line-height:1.5;margin:8px 0;padding:8px 10px;background:var(--bg);border-radius:8px;border:1px solid var(--border);max-height:60px;overflow:hidden;white-space:pre-wrap;word-break:break-all}
247
+ .admin-card-actions{display:inline-flex;gap:4px;margin-left:auto;align-items:center;flex-shrink:0}
248
+ .admin-card-time{font-size:11px;color:var(--text-muted)}
249
+ .admin-card-detail{display:none;margin-top:0;padding:20px 24px 24px;border-top:1px dashed var(--border);background:linear-gradient(180deg,rgba(99,102,241,.015) 0%,var(--bg) 60%);animation:adminDetailIn .25s ease}
250
+ @keyframes adminDetailIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}
251
+ .admin-card.expanded .admin-card-detail{display:block}
252
+ .admin-card.expanded{border-color:rgba(99,102,241,.3);box-shadow:0 4px 24px rgba(99,102,241,.08),0 0 0 1px rgba(99,102,241,.06)}
253
+ .admin-card-detail-meta{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:18px;font-size:12px;color:var(--text-muted)}
254
+ .admin-card-detail-meta .meta-item{display:inline-flex;align-items:center;gap:4px;padding:5px 12px;background:var(--bg-card);border:1px solid var(--border);border-radius:20px;font-size:11px;color:var(--text-sec);transition:border-color .15s,background .15s}
255
+ .admin-card-detail-meta .meta-item:hover{border-color:rgba(99,102,241,.25);background:rgba(99,102,241,.03)}
256
+ .admin-card-detail-section{margin-top:20px}
257
+ .admin-card-detail-section .detail-label{font-size:11px;font-weight:700;color:var(--pri);margin-bottom:12px;text-transform:uppercase;letter-spacing:.06em;display:flex;align-items:center;gap:8px}
258
+ .admin-card-detail-section .detail-label::before{content:'';width:3px;height:14px;border-radius:2px;background:linear-gradient(180deg,var(--pri),rgba(99,102,241,.3))}
259
+ .admin-card-detail-content{font-size:13px;line-height:1.75;color:var(--text-sec);white-space:pre-wrap;word-break:break-all;max-height:400px;overflow-y:auto;padding:16px 20px;background:var(--bg-card);border-radius:10px;border:1px solid var(--border);scrollbar-width:thin;scrollbar-color:rgba(99,102,241,.2) transparent}
260
+ .admin-card-detail-content::-webkit-scrollbar{width:5px}
261
+ .admin-card-detail-content::-webkit-scrollbar-thumb{background:rgba(99,102,241,.2);border-radius:4px}
262
+ .admin-card-detail-content::-webkit-scrollbar-track{background:transparent}
263
+ .admin-task-meta .meta-item .task-status-badge{border:none;padding:0;font-size:11px}
264
+ .admin-task-summary-body{font-size:13.5px;line-height:1.8;color:var(--text);padding:16px 20px;background:var(--bg-card);border-radius:10px;border:1px solid var(--border)}
265
+ .admin-task-summary-body .summary-section-title{font-size:12px;font-weight:700;color:var(--pri);margin:16px 0 6px;padding-bottom:5px;border-bottom:1px dashed var(--border)}
266
+ .admin-task-summary-body .summary-section-title:first-child{margin-top:0}
267
+ .admin-task-summary-body ul{margin:6px 0 12px;padding-left:18px}
268
+ .admin-task-summary-body li{margin:4px 0;color:var(--text-sec);line-height:1.7;font-size:13px}
269
+ .admin-task-summary-body p{margin:6px 0;font-size:13px;line-height:1.7}
270
+ .admin-task-chunks{display:flex;flex-direction:column;gap:0;padding:0;border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--bg-card)}
271
+ .adm-msg{display:flex;gap:0;border-bottom:1px solid var(--border)}
272
+ .adm-msg:last-child{border-bottom:none}
273
+ .adm-msg-side{width:144px;flex-shrink:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;padding:12px 10px;border-right:1px solid var(--border)}
274
+ .adm-msg-side.user{background:rgba(99,102,241,.04)}
275
+ .adm-msg-side.assistant{background:rgba(52,211,153,.04)}
276
+ .adm-msg-role{font-size:10px;font-weight:700;letter-spacing:.03em;text-transform:uppercase}
277
+ .adm-msg-side.user .adm-msg-role{color:var(--pri)}
278
+ .adm-msg-side.assistant .adm-msg-role{color:var(--green)}
279
+ .adm-msg-time{font-size:9px;color:var(--text-muted)}
280
+ .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}
281
+ .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)}
282
+ .adm-msg-toggle{display:none;padding:0 16px 8px;font-size:11px;color:var(--pri);cursor:pointer;transition:color .15s}
283
+ .adm-msg-toggle:hover{color:var(--pri-dark)}
284
+ .admin-card-expand-btn{font-size:12px;color:var(--pri);cursor:pointer;background:none;border:none;padding:2px 6px;font-family:inherit}
285
+ .admin-card-clickable{cursor:pointer}
286
+ .admin-toolbar{display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap}
287
+ .admin-toolbar h3{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;margin:0;margin-right:auto;line-height:32px}
288
+ .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}
231
289
  .admin-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:700;padding:3px 10px;border-radius:999px;letter-spacing:.02em}
232
290
  .admin-badge.admin{background:rgba(52,199,89,.15);color:#34c759}
233
291
  .admin-badge.member{background:rgba(142,142,147,.12);color:var(--text-muted)}
@@ -240,6 +298,19 @@ input,textarea,select{font-family:inherit;font-size:inherit}
240
298
  [data-theme="light"] .admin-badge.member{background:rgba(0,0,0,.06);color:#6b7280}
241
299
  [data-theme="light"] .admin-badge.pending{background:rgba(245,158,11,.1);color:#d97706}
242
300
  [data-theme="light"] .admin-header{background:linear-gradient(135deg,rgba(99,102,241,.05) 0%,rgba(139,92,246,.04) 50%,rgba(6,182,212,.03) 100%)}
301
+ .confirm-overlay{display:none;position:fixed;inset:0;align-items:center;justify-content:center;background:rgba(0,0,0,.45);backdrop-filter:blur(4px);z-index:9999;padding:20px}
302
+ .confirm-overlay.show{display:flex}
303
+ .confirm-panel{width:min(400px,90vw);background:var(--bg-card);border:1px solid var(--border);border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,.2);overflow:hidden;animation:confirmIn .2s ease}
304
+ @keyframes confirmIn{from{opacity:0;transform:scale(.95) translateY(8px)}to{opacity:1;transform:scale(1) translateY(0)}}
305
+ .confirm-panel-header{padding:20px 24px 0;font-size:15px;font-weight:700;color:var(--text)}
306
+ .confirm-panel-body{padding:14px 24px 20px;font-size:13px;line-height:1.65;color:var(--text-sec);white-space:pre-wrap;word-break:break-word}
307
+ .confirm-panel-footer{display:flex;justify-content:flex-end;gap:10px;padding:0 24px 20px}
308
+ .confirm-panel-footer .btn-confirm-cancel{padding:8px 20px;border:1px solid var(--border);border-radius:10px;background:var(--bg);color:var(--text-sec);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s;font-family:inherit}
309
+ .confirm-panel-footer .btn-confirm-cancel:hover{background:var(--bg-card);border-color:var(--text-muted)}
310
+ .confirm-panel-footer .btn-confirm-ok{padding:8px 20px;border:none;border-radius:10px;background:var(--pri);color:#fff;font-size:13px;font-weight:600;cursor:pointer;transition:all .15s;font-family:inherit}
311
+ .confirm-panel-footer .btn-confirm-ok:hover{opacity:.9;box-shadow:0 2px 8px rgba(99,102,241,.3)}
312
+ .confirm-panel-footer .btn-confirm-ok.danger{background:var(--rose)}
313
+ .confirm-panel-footer .btn-confirm-ok.danger:hover{box-shadow:0 2px 8px rgba(244,63,94,.3)}
243
314
  .task-detail-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
244
315
  .shared-memory-overlay,.shared-memory-overlay.show{display:none}
245
316
  .shared-memory-overlay.show{display:flex;position:fixed;inset:0;align-items:center;justify-content:center;background:rgba(0,0,0,.55);z-index:1200;padding:24px}
@@ -270,6 +341,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
270
341
  .card-content.show{max-height:600px;overflow-y:auto}
271
342
  .card-content pre{white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.25);padding:14px;border-radius:10px;font-size:12px;font-family:ui-monospace,monospace;margin-top:10px;border:1px solid var(--border);color:var(--text-sec)}
272
343
  .card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}
344
+ .card-actions-inline{display:inline-flex;align-items:center;gap:4px;margin-left:auto;flex-shrink:0}
273
345
  .vscore-badge{display:inline-flex;align-items:center;background:rgba(59,130,246,.15);color:#60a5fa;font-size:10px;font-weight:700;padding:4px 10px;border-radius:8px;margin-left:auto}
274
346
  .merge-badge{display:inline-flex;align-items:center;gap:4px;background:rgba(16,185,129,.12);color:#10b981;font-size:10px;font-weight:600;padding:3px 10px;border-radius:8px}
275
347
  .merge-history{margin-top:12px;padding:12px 14px;background:rgba(0,0,0,.15);border-radius:10px;border:1px solid var(--border);font-size:12px;line-height:1.7;color:var(--text-sec);max-height:200px;overflow-y:auto}
@@ -409,8 +481,36 @@ input,textarea,select{font-family:inherit;font-size:inherit}
409
481
  .toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}
410
482
  .toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}
411
483
  .toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(99,102,241,.15)}
484
+ .toast.warn{background:rgba(245,158,11,.1);color:#f59e0b;border-color:rgba(245,158,11,.3)}
412
485
  @keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}
413
486
 
487
+ .notif-bell{position:relative;cursor:pointer;font-size:16px;padding:5px 7px;border-radius:8px;background:none;border:1px solid transparent;color:var(--text-sec);transition:all .2s}
488
+ .notif-bell:hover{background:var(--bg-card-hover);color:var(--text)}
489
+ .notif-bell .notif-badge{position:absolute;top:1px;right:1px;min-width:16px;height:16px;line-height:16px;text-align:center;font-size:10px;font-weight:700;color:#fff;background:var(--rose);border-radius:8px;padding:0 4px;display:none;pointer-events:none}
490
+ .notif-bell .notif-badge.show{display:block;animation:notifPop .3s ease}
491
+ @keyframes notifPop{0%{transform:scale(0)}60%{transform:scale(1.2)}100%{transform:scale(1)}}
492
+ .notif-panel{position:absolute;top:calc(100% + 8px);right:0;width:380px;max-height:440px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);z-index:200;display:none;flex-direction:column;overflow:hidden;backdrop-filter:blur(16px)}
493
+ .notif-panel.show{display:flex;animation:notifSlide .2s ease}
494
+ @keyframes notifSlide{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
495
+ .notif-panel-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border);font-size:13px;font-weight:600;color:var(--text)}
496
+ .notif-panel-header .notif-mark-all{font-size:11px;font-weight:500;color:var(--pri);cursor:pointer;background:none;border:none;padding:4px 8px;border-radius:6px;transition:background .15s}
497
+ .notif-panel-header .notif-mark-all:hover{background:var(--pri-glow)}
498
+ .notif-panel-body{overflow-y:auto;flex:1;max-height:380px}
499
+ .notif-item{display:flex;gap:10px;padding:12px 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s;align-items:flex-start}
500
+ .notif-item:last-child{border-bottom:none}
501
+ .notif-item:hover{background:var(--bg-card-hover)}
502
+ .notif-item.unread{background:rgba(99,102,241,.04)}
503
+ .notif-item-icon{font-size:18px;flex-shrink:0;width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:8px;background:rgba(244,63,94,.08)}
504
+ .notif-item-body{flex:1;min-width:0}
505
+ .notif-item-title{font-size:12px;color:var(--text-sec);margin-bottom:2px}
506
+ .notif-item-name{font-size:13px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
507
+ .notif-item-time{font-size:11px;color:var(--text-muted);margin-top:2px}
508
+ .notif-item-dot{flex-shrink:0;width:8px;height:8px;border-radius:50%;background:var(--pri);margin-top:6px;display:none}
509
+ .notif-item.unread .notif-item-dot{display:block}
510
+ .notif-empty{text-align:center;padding:32px 16px;color:var(--text-muted);font-size:13px}
511
+ [data-theme="light"] .notif-panel{background:rgba(255,255,255,.96)}
512
+ [data-theme="light"] .notif-item.unread{background:rgba(99,102,241,.06)}
513
+
414
514
  .empty{text-align:center;padding:64px 20px;color:var(--text-sec)}
415
515
  .empty .icon{font-size:52px;margin-bottom:16px;opacity:.5}
416
516
  .empty p{font-size:15px;font-weight:500}
@@ -484,28 +584,35 @@ input,textarea,select{font-family:inherit;font-size:inherit}
484
584
  .task-detail-summary .summary-section-title:first-child{margin-top:0}
485
585
  .task-detail-summary ul{margin:4px 0 8px 0;padding-left:20px}
486
586
  .task-detail-summary li{margin:3px 0;color:var(--text-sec);line-height:1.6}
487
- .task-detail-chunks-title{font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px}
488
- .task-detail-chunks{display:flex;flex-direction:column;gap:14px;padding:8px 0}
489
- .task-chunk-item{display:flex;flex-direction:column;max-width:82%;font-size:13px;line-height:1.6}
490
- .task-chunk-item.role-user{align-self:flex-end;align-items:flex-end}
491
- .task-chunk-item.role-assistant,.task-chunk-item.role-tool{align-self:flex-start;align-items:flex-start}
492
- .task-chunk-role{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;margin-bottom:3px;padding:0 4px}
587
+ .task-detail-chunks-title{font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px;display:flex;align-items:center;gap:8px}
588
+ .task-detail-chunks-title::before{content:'';width:3px;height:14px;border-radius:2px;background:linear-gradient(180deg,var(--pri),rgba(99,102,241,.3))}
589
+ .task-detail-chunks{display:flex;flex-direction:column;gap:12px;padding:8px 0}
590
+ .task-chunk-item{display:flex;gap:12px;align-items:flex-start;width:100%;font-size:13px;line-height:1.6}
591
+ .task-chunk-item.role-user{flex-direction:row-reverse}
592
+ .task-chunk-avatar{width:32px;height:32px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0;font-weight:700}
593
+ .role-user .task-chunk-avatar{background:linear-gradient(135deg,var(--pri),var(--pri-dark));color:#fff}
594
+ .role-assistant .task-chunk-avatar{background:linear-gradient(135deg,var(--green),#059669);color:#fff}
595
+ .role-tool .task-chunk-avatar{background:linear-gradient(135deg,var(--amber),#d97706);color:#fff}
596
+ .task-chunk-body{flex:1;min-width:0;max-width:85%}
597
+ .task-chunk-header{display:flex;align-items:center;gap:8px;margin-bottom:4px}
598
+ .role-user .task-chunk-header{flex-direction:row-reverse}
599
+ .task-chunk-role{font-size:11px;font-weight:700;letter-spacing:.02em}
493
600
  .task-chunk-role.user{color:var(--pri)}
494
601
  .task-chunk-role.assistant{color:var(--green)}
495
602
  .task-chunk-role.tool{color:var(--amber)}
496
- .task-chunk-bubble{padding:12px 16px;border-radius:16px;white-space:pre-wrap;word-break:break-word;max-height:none;overflow:hidden;position:relative;transition:all .2s}
603
+ .task-chunk-time{font-size:10px;color:var(--text-muted)}
604
+ .task-chunk-bubble{padding:14px 18px;border-radius:12px;white-space:pre-wrap;word-break:break-word;max-height:none;overflow:hidden;position:relative;transition:all .2s}
497
605
  .task-chunk-bubble.collapsed{max-height:200px}
498
- .task-chunk-expand{display:none;align-items:center;justify-content:center;gap:4px;margin-top:4px;padding:4px 12px;font-size:12px;font-weight:600;color:var(--text-sec);cursor:pointer;user-select:none;border-radius:8px;transition:all .15s}
606
+ .task-chunk-expand{display:none;align-items:center;justify-content:center;gap:4px;margin-top:6px;padding:4px 12px;font-size:12px;font-weight:600;color:var(--text-sec);cursor:pointer;user-select:none;border-radius:8px;transition:all .15s}
499
607
  .task-chunk-expand:hover{color:var(--pri);background:rgba(99,102,241,.08)}
500
608
  .task-chunk-expand .expand-arrow{display:inline-block;font-size:10px;transition:transform .2s}
501
609
  .task-chunk-expand.is-expanded .expand-arrow{transform:rotate(180deg)}
502
- .role-user .task-chunk-bubble{background:var(--pri);color:#000;border-bottom-right-radius:4px}
503
- .role-assistant .task-chunk-bubble{background:var(--bg-card);border:1px solid var(--border);color:var(--text-sec);border-bottom-left-radius:4px}
504
- .role-tool .task-chunk-bubble{background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);color:var(--text-sec);border-bottom-left-radius:4px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
505
- .task-chunk-bubble:hover{filter:brightness(1.05)}
506
- .task-chunk-time{font-size:10px;color:var(--text-muted);margin-top:3px;padding:0 4px}
507
- [data-theme="light"] .role-user .task-chunk-bubble{background:var(--pri);color:#fff}
508
- [data-theme="light"] .role-assistant .task-chunk-bubble{background:#f0f0f0;border:none;color:#333}
610
+ .role-user .task-chunk-bubble{background:linear-gradient(135deg,rgba(99,102,241,.12),rgba(99,102,241,.06));border:1px solid rgba(99,102,241,.2);color:var(--text);border-radius:12px 12px 4px 12px}
611
+ .role-assistant .task-chunk-bubble{background:var(--bg-card);border:1px solid var(--border);color:var(--text-sec);border-radius:12px 12px 12px 4px}
612
+ .role-tool .task-chunk-bubble{background:rgba(245,158,11,.06);border:1px solid rgba(245,158,11,.15);color:var(--text-sec);border-radius:12px 12px 12px 4px;font-family:'SF Mono',Monaco,Consolas,monospace;font-size:12px}
613
+ .task-chunk-bubble:hover{border-color:rgba(99,102,241,.3);box-shadow:0 2px 8px rgba(0,0,0,.1)}
614
+ [data-theme="light"] .role-user .task-chunk-bubble{background:linear-gradient(135deg,rgba(79,70,229,.08),rgba(79,70,229,.04));border-color:rgba(79,70,229,.15);color:#111827}
615
+ [data-theme="light"] .role-assistant .task-chunk-bubble{background:#f8f9fb;border-color:#e2e4e9;color:#4b5563}
509
616
  [data-theme="light"] .task-detail-panel{background:#fff}
510
617
  [data-theme="light"] .task-card{background:#fff}
511
618
  [data-theme="light"] .tasks-stat{background:#fff}
@@ -521,7 +628,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
521
628
  .skill-card.archived::before{background:var(--text-muted)}
522
629
  .skill-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:6px}
523
630
  .skill-card-name{font-size:15px;font-weight:700;color:var(--text);flex:1}
524
- .skill-card-badges{display:flex;gap:6px;align-items:center}
631
+ .task-card-badges,.skill-card-badges{display:flex;gap:6px;align-items:center}
525
632
  .skill-badge{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;padding:3px 10px;border-radius:20px}
526
633
  .skill-badge.version{color:var(--violet);background:rgba(139,92,246,.15)}
527
634
  .skill-badge.installed{color:var(--green);background:var(--green-bg)}
@@ -1043,13 +1150,20 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1043
1150
  <button class="tab" data-view="analytics" onclick="switchView('analytics')" data-i18n="tab.analytics">\u{1F4CA} Analytics</button>
1044
1151
  <button class="tab" data-view="logs" onclick="switchView('logs')" data-i18n="tab.logs">\u{1F4DD} Logs</button>
1045
1152
  <button class="tab" data-view="import" onclick="switchView('import')" data-i18n="tab.import">\u{1F4E5} Import</button>
1046
- <button class="tab" data-view="admin" onclick="switchView('admin')" data-i18n="tab.admin">\u{1F6E1} Admin</button>
1153
+ <button class="tab" data-view="admin" onclick="switchView('admin')" data-i18n="tab.admin">\u{1F6E1} Admin<span id="adminPendingBadge" style="display:none;background:var(--rose);color:#fff;font-size:10px;font-weight:700;padding:1px 5px;border-radius:8px;margin-left:4px;vertical-align:top"></span></button>
1047
1154
  <button class="tab" data-view="settings" onclick="switchView('settings')" data-i18n="tab.settings">\u2699 Settings</button>
1048
1155
  </nav>
1049
1156
  </div>
1050
1157
  <div class="actions">
1051
1158
  <button class="btn btn-icon" onclick="toggleLang()" aria-label="Switch language" style="font-size:12px;font-weight:700;padding:4px 8px"><span data-i18n="lang.switch">EN</span></button>
1052
1159
  <button class="btn btn-icon theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
1160
+ <div style="position:relative;display:inline-block" id="notifBellWrap">
1161
+ <button class="notif-bell" onclick="toggleNotifPanel(event)" title="Notifications" aria-label="Notifications">\u{1F514}<span class="notif-badge" id="notifBadge"></span></button>
1162
+ <div class="notif-panel" id="notifPanel">
1163
+ <div class="notif-panel-header"><span data-i18n="notif.title">\u{1F514} Notifications</span><div style="display:flex;gap:6px"><button class="notif-mark-all" onclick="markAllNotifsRead()" data-i18n="notif.markAll">Mark all read</button><button class="notif-mark-all" onclick="clearAllNotifs()" style="color:var(--rose)" data-i18n="notif.clearAll">Clear all</button></div></div>
1164
+ <div class="notif-panel-body" id="notifPanelBody"><div class="notif-empty" data-i18n="notif.empty">No notifications</div></div>
1165
+ </div>
1166
+ </div>
1053
1167
  <button class="btn btn-ghost btn-sm" onclick="loadAll()" data-i18n="refresh">\u21BB Refresh</button>
1054
1168
  <button class="btn btn-ghost btn-sm" onclick="doLogout()" data-i18n="logout">Logout</button>
1055
1169
  </div>
@@ -1086,7 +1200,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1086
1200
  </select>
1087
1201
  <select id="memorySearchScope" class="filter-select" onchange="onMemoryScopeChange()">
1088
1202
  <option value="local" data-i18n="scope.local">Local</option>
1089
- <option value="all" data-i18n="scope.hub">Hub</option>
1203
+ <option value="all" data-i18n="scope.hub">Team</option>
1090
1204
  </select>
1091
1205
  </div>
1092
1206
  <div class="search-meta" id="searchMeta"></div>
@@ -1130,7 +1244,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1130
1244
  <button class="filter-chip" data-task-status="skipped" onclick="setTaskStatusFilter(this,'skipped')" data-i18n="tasks.status.skipped">Skipped</button>
1131
1245
  <select id="taskSearchScope" class="scope-select" onchange="onTaskScopeChange()">
1132
1246
  <option value="local" data-i18n="scope.local">Local</option>
1133
- <option value="all" data-i18n="scope.hub">Hub</option>
1247
+ <option value="all" data-i18n="scope.hub">Team</option>
1134
1248
  </select>
1135
1249
  <button class="btn btn-sm btn-ghost" onclick="loadTasks()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
1136
1250
  </div>
@@ -1172,7 +1286,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1172
1286
  <input type="text" id="skillSearchInput" placeholder="Search skills..." data-i18n-ph="skills.search.placeholder" oninput="debounceSkillSearch()">
1173
1287
  <select id="skillSearchScope" class="scope-select" onchange="onSkillScopeChange()">
1174
1288
  <option value="local" data-i18n="scope.local">Local</option>
1175
- <option value="all" data-i18n="scope.hub">Hub</option>
1289
+ <option value="all" data-i18n="scope.hub">Team</option>
1176
1290
  </select>
1177
1291
  </div>
1178
1292
  <div class="search-meta" id="skillSearchMeta"></div>
@@ -1200,7 +1314,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1200
1314
  </div>
1201
1315
  <div class="tasks-list" id="skillsList"><div class="spinner"></div></div>
1202
1316
  <div id="hubSkillsSection" style="display:none;margin-top:16px">
1203
- <div class="section-title" style="margin-bottom:12px" data-i18n="skills.hub.title">\u{1F310} Hub Skills</div>
1317
+ <div class="section-title" style="margin-bottom:12px" data-i18n="skills.hub.title">\u{1F310} Team Skills</div>
1204
1318
  <div class="tasks-list" id="hubSkillsList"></div>
1205
1319
  </div>
1206
1320
  </div>
@@ -1209,6 +1323,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1209
1323
  <div class="task-detail-header">
1210
1324
  <h2 id="skillDetailTitle"></h2>
1211
1325
  <div style="display:flex;gap:8px;align-items:center">
1326
+ <span id="skillScopeBadge"></span>
1212
1327
  <button class="skill-vis-btn" id="skillVisibilityBtn" onclick="toggleSkillVisibility()"></button>
1213
1328
  <button class="skill-download-btn" id="skillDownloadBtn" onclick="downloadSkill()" data-i18n="skills.download">\u2B07 Download</button>
1214
1329
  <button class="btn btn-icon" onclick="closeSkillDetail()" title="Close">\u2715</button>
@@ -1216,7 +1331,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1216
1331
  </div>
1217
1332
  <div class="task-detail-meta" id="skillDetailMeta"></div>
1218
1333
  <div class="skill-detail-desc" id="skillDetailDesc"></div>
1219
- <div id="skillShareActions" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0"></div>
1220
1334
  <div class="task-detail-chunks-title" data-i18n="skills.files">Skill Files</div>
1221
1335
  <div class="skill-files-list" id="skillFilesList"></div>
1222
1336
  <div class="task-detail-chunks-title" id="skillContentTitle" data-i18n="skills.content">SKILL.md Content</div>
@@ -1250,10 +1364,18 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1250
1364
  <div class="analytics-section" id="toolPerfSection" style="position:relative">
1251
1365
  <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
1252
1366
  <h3 style="margin-bottom:0"><span class="icon">\u26A1</span> <span data-i18n="chart.toolperf">Tool Response Time</span> <span style="font-size:10px;color:var(--text-muted);font-weight:500;text-transform:none;letter-spacing:0;margin-left:4px">(per minute avg)</span></h3>
1253
- <div style="display:flex;gap:6px;align-items:center">
1367
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
1254
1368
  <button class="range-btn tool-range active" data-mins="60" onclick="setToolMinutes(60)">1h</button>
1255
1369
  <button class="range-btn tool-range" data-mins="360" onclick="setToolMinutes(360)">6h</button>
1256
1370
  <button class="range-btn tool-range" data-mins="1440" onclick="setToolMinutes(1440)">24h</button>
1371
+ <button class="range-btn tool-range" data-mins="4320" onclick="setToolMinutes(4320)">3d</button>
1372
+ <button class="range-btn tool-range" data-mins="10080" onclick="setToolMinutes(10080)">7d</button>
1373
+ <button class="range-btn tool-range" data-mins="43200" onclick="setToolMinutes(43200)">30d</button>
1374
+ <span style="color:var(--border);margin:0 2px">|</span>
1375
+ <input type="datetime-local" id="toolRangeFrom" style="font-size:11px;padding:3px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-card);color:var(--text)" title="From">
1376
+ <span style="font-size:11px;color:var(--text-muted)">–</span>
1377
+ <input type="datetime-local" id="toolRangeTo" style="font-size:11px;padding:3px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-card);color:var(--text)" title="To">
1378
+ <button class="range-btn" onclick="applyCustomToolRange()" data-i18n="chart.apply">Apply</button>
1257
1379
  </div>
1258
1380
  </div>
1259
1381
  <div id="toolChart" style="width:100%;height:280px;position:relative;overflow:hidden;border-radius:12px"></div>
@@ -1284,7 +1406,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1284
1406
  <div class="settings-view" id="settingsView">
1285
1407
  <div class="settings-tabs-bar">
1286
1408
  <button class="settings-tab-btn active" data-tab="models" onclick="switchSettingsTab('models',this)"><span class="stab-dot"></span><span data-i18n="settings.models">AI Models</span></button>
1287
- <button class="settings-tab-btn" data-tab="hub" onclick="switchSettingsTab('hub',this)"><span class="stab-dot"></span><span data-i18n="settings.hub">Hub & Team</span></button>
1409
+ <button class="settings-tab-btn" data-tab="hub" onclick="switchSettingsTab('hub',this)"><span class="stab-dot"></span><span data-i18n="settings.hub">Team Sharing</span></button>
1288
1410
  <button class="settings-tab-btn" data-tab="general" onclick="switchSettingsTab('general',this)"><span class="stab-dot"></span><span data-i18n="settings.general">General</span></button>
1289
1411
  </div>
1290
1412
  <div class="settings-cards-grid">
@@ -1336,7 +1458,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1336
1458
  <div class="test-conn-row">
1337
1459
  <button class="btn btn-sm btn-ghost" onclick="testModel('embedding')" id="testEmbBtn" data-i18n="settings.test">Test Connection</button>
1338
1460
  <span class="test-result" id="testEmbResult"></span>
1339
- </div>
1461
+ </div>
1340
1462
 
1341
1463
  <div class="settings-card-divider"></div>
1342
1464
 
@@ -1381,7 +1503,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1381
1503
  <div class="test-conn-row">
1382
1504
  <button class="btn btn-sm btn-ghost" onclick="testModel('summarizer')" id="testSumBtn" data-i18n="settings.test">Test Connection</button>
1383
1505
  <span class="test-result" id="testSumResult"></span>
1384
- </div>
1506
+ </div>
1385
1507
 
1386
1508
  <div class="settings-card-divider"></div>
1387
1509
 
@@ -1408,44 +1530,44 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1408
1530
  </div>
1409
1531
  <div style="margin-top:14px">
1410
1532
  <div class="settings-card-subtitle" style="margin-bottom:4px" data-i18n="settings.skill.model">Skill Dedicated Model</div>
1411
- <div class="field-hint" style="margin-bottom:12px" data-i18n="settings.skill.model.hint">If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.</div>
1412
- <div class="settings-grid">
1413
- <div class="settings-field">
1414
- <label data-i18n="settings.provider">Provider</label>
1415
- <select id="cfgSkillProvider" onchange="onProviderChange('skill')">
1416
- <option value="">\u2014 <span data-i18n="settings.skill.usemain">Use main summarizer</span> \u2014</option>
1417
- <option value="openai_compatible">OpenAI Compatible</option>
1418
- <option value="openai">OpenAI</option>
1419
- <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
1420
- <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
1421
- <option value="deepseek">DeepSeek</option>
1422
- <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
1423
- <option value="moonshot">Moonshot (Kimi)</option>
1424
- <option value="anthropic">Anthropic</option>
1425
- <option value="gemini">Gemini</option>
1426
- <option value="azure_openai">Azure OpenAI</option>
1427
- <option value="bedrock">Bedrock</option>
1428
- <option value="openclaw">OpenClaw Host</option>
1429
- </select>
1430
- </div>
1431
- <div class="settings-field">
1432
- <label data-i18n="settings.model">Model</label>
1433
- <input type="text" id="cfgSkillModel" placeholder="e.g. claude-4.6-opus">
1434
- </div>
1435
- <div class="settings-field full-width">
1436
- <label>Endpoint</label>
1437
- <input type="text" id="cfgSkillEndpoint" placeholder="https://...">
1438
- </div>
1439
- <div class="settings-field">
1440
- <label>API Key</label>
1441
- <input type="password" id="cfgSkillApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
1442
- </div>
1533
+ <div class="field-hint" style="margin-bottom:12px" data-i18n="settings.skill.model.hint">If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.</div>
1534
+ <div class="settings-grid">
1535
+ <div class="settings-field">
1536
+ <label data-i18n="settings.provider">Provider</label>
1537
+ <select id="cfgSkillProvider" onchange="onProviderChange('skill')">
1538
+ <option value="">\u2014 <span data-i18n="settings.skill.usemain">Use main summarizer</span> \u2014</option>
1539
+ <option value="openai_compatible">OpenAI Compatible</option>
1540
+ <option value="openai">OpenAI</option>
1541
+ <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
1542
+ <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
1543
+ <option value="deepseek">DeepSeek</option>
1544
+ <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
1545
+ <option value="moonshot">Moonshot (Kimi)</option>
1546
+ <option value="anthropic">Anthropic</option>
1547
+ <option value="gemini">Gemini</option>
1548
+ <option value="azure_openai">Azure OpenAI</option>
1549
+ <option value="bedrock">Bedrock</option>
1550
+ <option value="openclaw">OpenClaw Host</option>
1551
+ </select>
1443
1552
  </div>
1444
- <div class="test-conn-row">
1445
- <button class="btn btn-sm btn-ghost" onclick="testModel('skill')" id="testSkillBtn" data-i18n="settings.test">Test Connection</button>
1446
- <span class="test-result" id="testSkillResult"></span>
1553
+ <div class="settings-field">
1554
+ <label data-i18n="settings.model">Model</label>
1555
+ <input type="text" id="cfgSkillModel" placeholder="e.g. claude-4.6-opus">
1556
+ </div>
1557
+ <div class="settings-field full-width">
1558
+ <label>Endpoint</label>
1559
+ <input type="text" id="cfgSkillEndpoint" placeholder="https://...">
1560
+ </div>
1561
+ <div class="settings-field">
1562
+ <label>API Key</label>
1563
+ <input type="password" id="cfgSkillApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
1447
1564
  </div>
1448
1565
  </div>
1566
+ <div class="test-conn-row">
1567
+ <button class="btn btn-sm btn-ghost" onclick="testModel('skill')" id="testSkillBtn" data-i18n="settings.test">Test Connection</button>
1568
+ <span class="test-result" id="testSkillResult"></span>
1569
+ </div>
1570
+ </div>
1449
1571
 
1450
1572
  <div class="settings-card-divider"></div>
1451
1573
  <div class="settings-actions">
@@ -1457,12 +1579,12 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1457
1579
  </div>
1458
1580
  </div>
1459
1581
 
1460
- <!-- ══ Card: Hub & Team ══ -->
1582
+ <!-- ══ Card: Team Sharing ══ -->
1461
1583
  <div class="settings-card card-hub" id="settingsSharingConfig" data-stab="hub">
1462
1584
  <div class="settings-card-header">
1463
1585
  <div class="settings-card-icon" style="background:rgba(6,182,212,.1);border-color:rgba(6,182,212,.15)">\u{1F310}</div>
1464
1586
  <div class="settings-card-title-wrap">
1465
- <div class="settings-card-title" data-i18n="settings.hub">Hub & Team</div>
1587
+ <div class="settings-card-title" data-i18n="settings.hub">Team Sharing</div>
1466
1588
  <div class="settings-card-desc" data-i18n="settings.hub.desc">Share memories, tasks and skills with your team</div>
1467
1589
  </div>
1468
1590
  </div>
@@ -1478,11 +1600,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1478
1600
  <div class="team-guide-opt-icon" style="background:rgba(6,182,212,.1);border:1px solid rgba(6,182,212,.15)">\u{1F310}</div>
1479
1601
  <div class="team-guide-opt-title" data-i18n="guide.join.title">Join a Remote Team</div>
1480
1602
  </div>
1481
- <div class="team-guide-opt-desc" data-i18n="guide.join.desc">Your team already has a Hub running? Join it to share memories, tasks and skills with team members.</div>
1603
+ <div class="team-guide-opt-desc" data-i18n="guide.join.desc">Your team already has a server running? Join it to share memories, tasks and skills with team members.</div>
1482
1604
  <ol class="team-guide-steps">
1483
- <li><span data-i18n="guide.join.s1">Ask your Hub admin for the Hub Address and Team Token</span></li>
1605
+ <li><span data-i18n="guide.join.s1">Ask your team admin for the Server Address and Team Token</span></li>
1484
1606
  <li><span data-i18n="guide.join.s2">Enable sharing above, select "Client" mode</span></li>
1485
- <li><span data-i18n="guide.join.s3">Fill in Hub Address and Team Token, click "Test Connection"</span></li>
1607
+ <li><span data-i18n="guide.join.s3">Fill in Server Address and Team Token, click "Test Connection"</span></li>
1486
1608
  <li><span data-i18n="guide.join.s4">Save settings and restart the OpenClaw gateway (page refreshes automatically)</span></li>
1487
1609
  </ol>
1488
1610
  <button class="btn-guide" onclick="guideGoToHub('client')" data-i18n="guide.join.btn">\u2192 Configure Client Mode</button>
@@ -1490,16 +1612,16 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1490
1612
  <div class="team-guide-opt">
1491
1613
  <div class="team-guide-opt-header">
1492
1614
  <div class="team-guide-opt-icon" style="background:rgba(139,92,246,.1);border:1px solid rgba(139,92,246,.15)">\u{1F5A5}\uFE0F</div>
1493
- <div class="team-guide-opt-title" data-i18n="guide.hub.title">Start Your Own Hub</div>
1615
+ <div class="team-guide-opt-title" data-i18n="guide.hub.title">Start Your Own Team Server</div>
1494
1616
  </div>
1495
- <div class="team-guide-opt-desc" data-i18n="guide.hub.desc">Be the team server. Run a Hub on this device so others can connect and share memories with you.</div>
1617
+ <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>
1496
1618
  <ol class="team-guide-steps">
1497
- <li><span data-i18n="guide.hub.s1">Enable sharing above, select "Hub" mode</span></li>
1619
+ <li><span data-i18n="guide.hub.s1">Enable sharing above, select "Server" mode</span></li>
1498
1620
  <li><span data-i18n="guide.hub.s2">Set a team name, save settings, and restart the gateway (page refreshes automatically)</span></li>
1499
- <li><span data-i18n="guide.hub.s3">Share the Hub Address and Team Token with your team members</span></li>
1621
+ <li><span data-i18n="guide.hub.s3">Share the Server Address and Team Token with your team members</span></li>
1500
1622
  <li><span data-i18n="guide.hub.s4">Approve join requests in the Admin Panel</span></li>
1501
1623
  </ol>
1502
- <button class="btn-guide" onclick="guideGoToHub('hub')" data-i18n="guide.hub.btn">\u2192 Configure Hub Mode</button>
1624
+ <button class="btn-guide" onclick="guideGoToHub('hub')" data-i18n="guide.hub.btn">\u2192 Configure Server Mode</button>
1503
1625
  </div>
1504
1626
  </div>
1505
1627
  </div>
@@ -1510,26 +1632,26 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1510
1632
  <input type="checkbox" id="cfgSharingEnabled" onchange="onSharingToggle()">
1511
1633
  <span class="toggle-slider"></span>
1512
1634
  </label>
1513
- <label data-i18n="settings.hub.enable.label">Enable Hub Sharing</label>
1635
+ <label data-i18n="settings.hub.enable.label">Enable Team Sharing</label>
1514
1636
  </div>
1515
1637
 
1516
1638
  <div id="sharingConfigPanel" style="display:none">
1517
1639
  <div style="margin-bottom:14px">
1518
1640
  <label style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;display:block;margin-bottom:6px" data-i18n="settings.hub.role">Role</label>
1519
1641
  <div style="display:flex;gap:8px">
1520
- <button class="btn btn-sm" id="btnRoleHub" onclick="selectSharingRole('hub')" data-i18n="settings.hub.role.hub">Hub (Server)</button>
1521
- <button class="btn btn-sm" id="btnRoleClient" onclick="selectSharingRole('client')" data-i18n="settings.hub.role.client">Client (Connect to Hub)</button>
1642
+ <button class="btn btn-sm" id="btnRoleHub" onclick="selectSharingRole('hub')" data-i18n="settings.hub.role.hub">Server (Host a team)</button>
1643
+ <button class="btn btn-sm" id="btnRoleClient" onclick="selectSharingRole('client')" data-i18n="settings.hub.role.client">Client (Join a team)</button>
1522
1644
  </div>
1523
- <div class="field-hint" style="margin-top:6px" data-i18n="settings.hub.role.hint">Hub: this device runs the central server. Client: connect to an existing Hub. The two modes are mutually exclusive.</div>
1645
+ <div class="field-hint" style="margin-top:6px" data-i18n="settings.hub.role.hint">Server = host the team server; Client = join an existing team. These two modes are mutually exclusive.</div>
1524
1646
  </div>
1525
1647
 
1526
1648
  <div id="hubModeConfig" style="display:none">
1527
1649
  <input type="hidden" id="cfgHubTeamToken" value="">
1528
1650
  <div class="settings-grid">
1529
1651
  <div class="settings-field">
1530
- <label data-i18n="settings.hub.port">Hub Port</label>
1652
+ <label data-i18n="settings.hub.port">Server Port</label>
1531
1653
  <input type="number" id="cfgHubPort" placeholder="18800" value="18800">
1532
- <div class="field-hint" data-i18n="settings.hub.port.hint">Port for Hub service. Default: 18800</div>
1654
+ <div class="field-hint" data-i18n="settings.hub.port.hint">Port for team server. Default: 18800</div>
1533
1655
  </div>
1534
1656
  <div class="settings-field">
1535
1657
  <label data-i18n="settings.hub.teamName">Team Name</label>
@@ -1549,20 +1671,20 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1549
1671
  <div id="clientModeConfig" style="display:none">
1550
1672
  <div style="background:rgba(99,102,241,.08);border:1px solid rgba(99,102,241,.2);border-radius:10px;padding:14px 18px;margin-bottom:14px;font-size:12px;line-height:1.7;color:var(--text-sec)">
1551
1673
  <div style="font-weight:700;color:var(--text);margin-bottom:4px" data-i18n="settings.hub.clientSteps.title">Quick Setup (3 steps)</div>
1552
- <div><span style="color:var(--accent)">1.</span> <span data-i18n="settings.hub.clientSteps.s1">Ask your Hub admin for Hub Address and Team Token</span></div>
1674
+ <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>
1553
1675
  <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>
1554
1676
  <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>
1555
1677
  </div>
1556
1678
  <div class="settings-grid">
1557
1679
  <div class="settings-field full-width">
1558
- <label data-i18n="settings.hub.hubAddress">Hub Address</label>
1680
+ <label data-i18n="settings.hub.hubAddress">Server Address</label>
1559
1681
  <input type="text" id="cfgClientHubAddress" placeholder="e.g. 192.168.1.100:18800">
1560
- <div class="field-hint" data-i18n="settings.hub.hubAddress.hint">Hub server address, e.g. 192.168.1.100:18800 or hub.example.com:18800</div>
1682
+ <div class="field-hint" data-i18n="settings.hub.hubAddress.hint">Team server address, e.g. 192.168.1.100:18800</div>
1561
1683
  </div>
1562
1684
  <div class="settings-field">
1563
1685
  <label data-i18n="settings.hub.teamTokenClient">Team Token</label>
1564
1686
  <input type="text" id="cfgClientTeamToken" placeholder="">
1565
- <div class="field-hint" data-i18n="settings.hub.teamTokenClient.hint">Get this from your Hub admin to join the team</div>
1687
+ <div class="field-hint" data-i18n="settings.hub.teamTokenClient.hint">Get this from your team admin to join</div>
1566
1688
  </div>
1567
1689
  <input type="hidden" id="cfgClientUserToken" value="">
1568
1690
  </div>
@@ -1574,13 +1696,12 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1574
1696
  </div>
1575
1697
 
1576
1698
  <div id="sharingPanelsWrap" style="display:none">
1577
- <div class="settings-card-divider"></div>
1578
- <div id="sharingStatusPanel"></div>
1579
- <div id="sharingTeamPanel"></div>
1580
- <div id="sharingAdminPanel"></div>
1699
+ <div class="settings-card-divider"></div>
1700
+ <div id="sharingStatusPanel"></div>
1701
+ <div id="sharingTeamPanel"></div>
1702
+ <div id="sharingAdminPanel"></div>
1581
1703
  </div>
1582
1704
 
1583
- <div class="settings-card-divider"></div>
1584
1705
  <div class="settings-actions">
1585
1706
  <span class="settings-saved" id="hubSaved">\u2713 <span data-i18n="settings.saved">Saved</span></span>
1586
1707
  <button class="btn btn-ghost" onclick="loadConfig()" data-i18n="settings.reset">Reset</button>
@@ -1637,24 +1758,24 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1637
1758
  <div class="admin-view" id="adminView">
1638
1759
  <div id="adminNotEnabled" style="display:none"></div>
1639
1760
  <div id="adminMainContent">
1640
- <div class="admin-header">
1641
- <div class="admin-header-top">
1642
- <h2><span class="ah-icon">\u{1F6E1}</span> <span data-i18n="admin.title">Hub Admin Panel</span></h2>
1643
- <button class="btn btn-sm btn-ghost" onclick="loadAdminData()" style="backdrop-filter:blur(8px)" data-i18n="admin.refresh">\u21BB Refresh</button>
1644
- </div>
1645
- <div class="admin-header-sub" data-i18n="admin.subtitle">Manage team members, groups, and shared resources</div>
1646
- <div class="admin-stat-row" id="adminStats"></div>
1647
- </div>
1648
- <div class="admin-tabs" id="adminTabsBar">
1649
- <button class="admin-tab active" onclick="switchAdminTab('users',this)"><span class="at-icon">\u{1F465}</span> <span data-i18n="admin.tab.users">Users</span> <span class="at-count" id="adminTabCountUsers">0</span></button>
1650
- <button class="admin-tab" onclick="switchAdminTab('sharedMemories',this)"><span class="at-icon">\u{1F4AD}</span> <span data-i18n="admin.tab.sharedMemories">Shared Memories</span> <span class="at-count" id="adminTabCountMemories">0</span></button>
1651
- <button class="admin-tab" onclick="switchAdminTab('memories',this)"><span class="at-icon">\u{1F4CB}</span> <span data-i18n="admin.tab.memories">Shared Tasks</span> <span class="at-count" id="adminTabCountTasks">0</span></button>
1652
- <button class="admin-tab" onclick="switchAdminTab('skills',this)"><span class="at-icon">\u{1F9E0}</span> <span data-i18n="admin.tab.skills">Shared Skills</span> <span class="at-count" id="adminTabCountSkills">0</span></button>
1761
+ <div class="admin-header">
1762
+ <div class="admin-header-top">
1763
+ <h2><span class="ah-icon">\u{1F6E1}</span> <span data-i18n="admin.title">Team Admin Panel</span></h2>
1764
+ <button class="btn btn-sm btn-ghost" onclick="loadAdminData()" style="backdrop-filter:blur(8px)" data-i18n="admin.refresh">\u21BB Refresh</button>
1653
1765
  </div>
1654
- <div class="admin-panel active" id="adminUsersPanel"></div>
1655
- <div class="admin-panel" id="adminSharedMemoriesPanel"></div>
1656
- <div class="admin-panel" id="adminMemoriesPanel"></div>
1657
- <div class="admin-panel" id="adminSkillsPanel"></div>
1766
+ <div class="admin-header-sub" data-i18n="admin.subtitle">Manage team members and shared resources</div>
1767
+ <div class="admin-stat-row" id="adminStats"></div>
1768
+ </div>
1769
+ <div class="admin-tabs" id="adminTabsBar">
1770
+ <button class="admin-tab active" onclick="switchAdminTab('users',this)"><span class="at-icon">\u{1F465}</span> <span data-i18n="admin.tab.users">Users</span> <span class="at-count" id="adminTabCountUsers">0</span></button>
1771
+ <button class="admin-tab" onclick="switchAdminTab('memories',this)"><span class="at-icon">\u{1F4AD}</span> <span data-i18n="admin.tab.memories">Shared Memories</span> <span class="at-count" id="adminTabCountMemories">0</span></button>
1772
+ <button class="admin-tab" onclick="switchAdminTab('tasks',this)"><span class="at-icon">\u{1F4CB}</span> <span data-i18n="admin.tab.tasks">Shared Tasks</span> <span class="at-count" id="adminTabCountTasks">0</span></button>
1773
+ <button class="admin-tab" onclick="switchAdminTab('skills',this)"><span class="at-icon">\u{1F9E0}</span> <span data-i18n="admin.tab.skills">Shared Skills</span> <span class="at-count" id="adminTabCountSkills">0</span></button>
1774
+ </div>
1775
+ <div class="admin-panel active" id="adminUsersPanel"></div>
1776
+ <div class="admin-panel" id="adminTasksPanel"></div>
1777
+ <div class="admin-panel" id="adminMemoriesPanel"></div>
1778
+ <div class="admin-panel" id="adminSkillsPanel"></div>
1658
1779
  </div>
1659
1780
  </div>
1660
1781
 
@@ -1901,14 +2022,14 @@ const I18N={
1901
2022
  'skills.search.local':'Local',
1902
2023
  'skills.search.noresult':'No matching skills found',
1903
2024
  'skills.load.error':'Failed to load skills',
1904
- 'skills.hub.title':'\u{1F310} Hub Skills',
2025
+ 'skills.hub.title':'\u{1F310} Team Skills',
1905
2026
  'scope.local':'Local',
1906
2027
  'scope.group':'Group',
1907
2028
  'scope.all':'All',
1908
- 'skills.visibility.public':'Public',
1909
- 'skills.visibility.private':'Private',
1910
- 'skills.setPublic':'Set Public',
1911
- 'skills.setPrivate':'Set Private',
2029
+ 'skills.visibility.public':'Shared Locally',
2030
+ 'skills.visibility.private':'This Agent Only',
2031
+ 'skills.setPublic':'Share Locally',
2032
+ 'skills.setPrivate':'This Agent Only',
1912
2033
  'tasks.total':'Total Tasks',
1913
2034
  'tasks.active':'Active',
1914
2035
  'tasks.completed':'Completed',
@@ -1925,6 +2046,17 @@ const I18N={
1925
2046
  'tasks.skipped.default':'This conversation was too brief to generate a summary. It will not appear in search results.',
1926
2047
  'refresh':'\\u21BB Refresh',
1927
2048
  'logout':'Logout',
2049
+ 'notif.title':'\\u{1F514} Notifications',
2050
+ 'notif.markAll':'Mark all read',
2051
+ 'notif.empty':'No notifications yet',
2052
+ 'notif.removed.memory':'Your shared memory was removed by admin',
2053
+ 'notif.removed.task':'Your shared task was removed by admin',
2054
+ 'notif.removed.skill':'Your shared skill was removed by admin',
2055
+ 'notif.clearAll':'Clear all',
2056
+ 'notif.timeAgo.just':'just now',
2057
+ 'notif.timeAgo.min':'{n}m ago',
2058
+ 'notif.timeAgo.hour':'{n}h ago',
2059
+ 'notif.timeAgo.day':'{n}d ago',
1928
2060
  'stat.memories':'Memories',
1929
2061
  'stat.sessions':'Sessions',
1930
2062
  'stat.embeddings':'Embeddings',
@@ -1979,6 +2111,8 @@ const I18N={
1979
2111
  'chart.nodata':'No data in this range',
1980
2112
  'chart.nocalls':'No viewer calls in this range',
1981
2113
  'chart.toolperf':'Tool Response Time',
2114
+ 'chart.apply':'Apply',
2115
+ 'chart.selectRange':'Please select a time range',
1982
2116
  'chart.list':'List',
1983
2117
  'chart.search':'Search',
1984
2118
  'modal.edit':'Edit Memory',
@@ -1994,11 +2128,14 @@ const I18N={
1994
2128
  'toast.deleted':'Memory deleted',
1995
2129
  'toast.opfail':'Operation failed',
1996
2130
  'toast.delfail':'Delete failed',
1997
- 'toast.setPublic':'Set to public',
1998
- 'toast.setPrivate':'Set to private',
2131
+ 'toast.setPublic':'Shared locally',
2132
+ 'toast.setPrivate':'Now visible to this agent only',
1999
2133
  'toast.cleared':'All memories cleared',
2000
2134
  'toast.clearfail':'Clear failed',
2001
2135
  'toast.notfound':'Memory not found in cache',
2136
+ 'confirm.title':'Confirm',
2137
+ 'confirm.ok':'Confirm',
2138
+ 'confirm.cancel':'Cancel',
2002
2139
  'confirm.delete':'Delete this memory?',
2003
2140
  'confirm.clearall':'Delete ALL memories? This cannot be undone.',
2004
2141
  'confirm.clearall2':'Are you absolutely sure?',
@@ -2174,16 +2311,16 @@ const I18N={
2174
2311
  'tasks.error.detail':'Failed to load task details',
2175
2312
  'tasks.untitled.related':'Untitled',
2176
2313
  'tab.admin':'\u{1F6E1} Admin',
2177
- 'settings.hub':'Hub & Team',
2178
- 'settings.hub.enable':'Enable Hub Sharing',
2314
+ 'settings.hub':'Team Sharing',
2315
+ 'settings.hub.enable':'Enable Team Sharing',
2179
2316
  'settings.hub.enable.hint':'When off, everything works locally as usual.',
2180
- 'settings.hub.enable.label':'Enable Hub Sharing',
2317
+ 'settings.hub.enable.label':'Enable Team Sharing',
2181
2318
  'settings.hub.role':'Role',
2182
- 'settings.hub.role.hub':'Hub (Server)',
2183
- 'settings.hub.role.client':'Client (Connect to Hub)',
2184
- 'settings.hub.role.hint':'Hub = run the server; Client = connect to one. These two modes are mutually exclusive.',
2185
- 'settings.hub.port':'Hub Port',
2186
- 'settings.hub.port.hint':'Port for Hub service. Default: 18800',
2319
+ 'settings.hub.role.hub':'Server (Host a team)',
2320
+ 'settings.hub.role.client':'Client (Join a team)',
2321
+ 'settings.hub.role.hint':'Server = host the team server; Client = join an existing team. These two modes are mutually exclusive.',
2322
+ 'settings.hub.port':'Server Port',
2323
+ 'settings.hub.port.hint':'Port for team server. Default: 18800',
2187
2324
  'settings.hub.teamName':'Team Name',
2188
2325
  'settings.hub.teamName.hint':'Display name for your team',
2189
2326
  'settings.hub.teamToken':'Team Token',
@@ -2192,23 +2329,23 @@ const I18N={
2192
2329
  'settings.hub.hubSteps.title':'Quick Setup (3 steps)',
2193
2330
  'settings.hub.hubSteps.s1':'Fill in Team Name below (or keep default)',
2194
2331
  'settings.hub.hubSteps.s2':'Click "Save Settings", then restart OpenClaw gateway',
2195
- 'settings.hub.hubSteps.s3':'Share the Hub Address and Team Token below with your team members',
2332
+ 'settings.hub.hubSteps.s3':'Share the Server Address and Team Token below with your team members',
2196
2333
  'settings.hub.clientSteps.title':'Quick Setup (3 steps)',
2197
- 'settings.hub.clientSteps.s1':'Ask your Hub admin for Hub Address and Team Token',
2334
+ 'settings.hub.clientSteps.s1':'Ask your team admin for the Server Address and Team Token',
2198
2335
  'settings.hub.clientSteps.s2':'Fill them in below, click "Test Connection" to verify',
2199
2336
  'settings.hub.clientSteps.s3':'Click "Save Settings", then restart OpenClaw gateway (page refreshes automatically)',
2200
2337
  'settings.hub.shareInfo.title':'Share this info with your team members:',
2201
2338
  'settings.hub.shareInfo.yourIP':'your-IP',
2202
2339
  'settings.hub.shareInfo.clickCopy':'Click to copy',
2203
- 'settings.hub.restartAlert':'Hub sharing config saved! Please restart the OpenClaw gateway for changes to take effect.\\n\\nRun: openclaw gateway stop && openclaw gateway start',
2204
- 'settings.hub.hubAddress':'Hub Address',
2205
- 'settings.hub.hubAddress.hint':'Hub server address, e.g. 192.168.1.100:18800',
2340
+ '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',
2341
+ 'settings.hub.hubAddress':'Server Address',
2342
+ 'settings.hub.hubAddress.hint':'Team server address, e.g. 192.168.1.100:18800',
2206
2343
  'settings.hub.teamTokenClient':'Team Token',
2207
- 'settings.hub.teamTokenClient.hint':'Get this from your Hub admin to join the team',
2344
+ 'settings.hub.teamTokenClient.hint':'Get this from your team admin to join',
2208
2345
  'settings.hub.userToken':'User Token',
2209
2346
  'settings.hub.userToken.hint':'Usually auto-obtained after joining. Only fill if given by admin.',
2210
2347
  'settings.hub.testConnection':'Test Connection',
2211
- 'settings.hub.test.noAddr':'Please enter Hub address first',
2348
+ 'settings.hub.test.noAddr':'Please enter server address first',
2212
2349
  'settings.hub.test.testing':'Testing...',
2213
2350
  'settings.hub.test.ok':'Connected successfully',
2214
2351
  'settings.hub.test.fail':'Connection failed',
@@ -2224,16 +2361,16 @@ const I18N={
2224
2361
  'sharing.sidebar.notConfigured':'Not configured',
2225
2362
  'sharing.sidebar.identity':'Identity:',
2226
2363
  'sharing.sidebar.admin':'Admin',
2227
- 'sharing.sidebar.targetHub':'Target Hub:',
2228
- 'sharing.pendingApproval.hint':'Your join request has been submitted. Please wait for the Hub admin to approve.',
2229
- 'sharing.rejected.hint':'Your join request was rejected by the Hub admin. Please contact the admin or retry.',
2364
+ 'sharing.sidebar.targetHub':'Team Server:',
2365
+ 'sharing.pendingApproval.hint':'Your join request has been submitted. Please wait for the team admin to approve.',
2366
+ 'sharing.rejected.hint':'Your join request was rejected by the team admin. Please contact the admin or retry.',
2230
2367
  'sharing.retryJoin':'Retry Join',
2231
2368
  'sharing.retryJoin.hint':'Clears local data and re-submits the join request',
2232
2369
  'sharing.retryJoin.confirm':'This will clear your current connection and re-submit a join request. Continue?',
2233
2370
  'sharing.retryJoin.success':'Join request re-submitted. Waiting for admin approval.',
2234
2371
  'sharing.retryJoin.fail':'Failed to retry join',
2235
- 'sharing.cannotJoinSelf':'Cannot join your own Hub. Please enter a remote Hub address.',
2236
- 'scope.hub':'Hub',
2372
+ 'sharing.cannotJoinSelf':'Cannot join your own server. Please enter a remote server address.',
2373
+ 'scope.hub':'Team',
2237
2374
  'memory.detail.title':'Memory Detail',
2238
2375
  'memory.detail.loading':'Loading...',
2239
2376
  'memory.detail.notFound':'Memory not found',
@@ -2241,12 +2378,13 @@ const I18N={
2241
2378
  'memory.detail.created':'Created ',
2242
2379
  'memory.detail.updated':'Updated ',
2243
2380
  'memory.detail.viewTarget':'View target: ',
2244
- 'admin.title':'Hub Admin Panel',
2381
+ 'admin.title':'Team Admin Panel',
2245
2382
  'admin.subtitle':'Manage team members and shared resources',
2246
2383
  'admin.refresh':'\u21BB Refresh',
2247
2384
  'admin.tab.users':'Users',
2385
+ 'admin.tab.tasks':'Shared Tasks',
2248
2386
  'admin.tab.groups':'Groups',
2249
- 'admin.tab.memories':'Shared Tasks',
2387
+ 'admin.tab.memories':'Shared Memories',
2250
2388
  'admin.tab.skills':'Shared Skills',
2251
2389
  'admin.stat.activeUsers':'Active Users',
2252
2390
  'admin.stat.pending':'Pending',
@@ -2260,6 +2398,43 @@ const I18N={
2260
2398
  'admin.approve':'Approve',
2261
2399
  'admin.reject':'Reject',
2262
2400
  'admin.device':'Device: ',
2401
+ 'admin.joined':'Joined: ',
2402
+ 'admin.approved':'Approved: ',
2403
+ 'admin.lastActive':'Last Active: ',
2404
+ 'admin.ip':'IP: ',
2405
+ 'admin.contrib.memories':'Memories',
2406
+ 'admin.contrib.tasks':'Tasks',
2407
+ 'admin.contrib.skills':'Skills',
2408
+ 'admin.groups':'Groups: ',
2409
+ 'admin.neverActive':'Never',
2410
+ 'admin.promoteAdmin':'Promote to Admin',
2411
+ 'admin.demoteMember':'Demote to Member',
2412
+ 'admin.editName':'Edit Name',
2413
+ 'admin.lastAdminHint':'Last admin — cannot remove or demote',
2414
+ 'admin.editNamePrompt':'Enter new username:',
2415
+ 'confirm.promoteAdmin':'Promote this user to admin? They will be able to manage all team members and resources.',
2416
+ 'confirm.demoteMember':'Demote this admin to member?',
2417
+ 'toast.roleChanged':'Role updated',
2418
+ 'toast.roleChangeFail':'Role change failed',
2419
+ 'toast.usernameChanged':'Username updated',
2420
+ 'toast.renameFail':'Rename failed',
2421
+ 'toast.invalidUsername':'Username must be 2-32 characters',
2422
+ 'admin.filterAll':'All Users',
2423
+ 'admin.filterAllValues':'All',
2424
+ 'admin.filter.owner':'All Users',
2425
+ 'admin.filter.visibility':'Visibility',
2426
+ 'admin.filter.group':'Group',
2427
+ 'admin.search.placeholder':'Search...',
2428
+ 'admin.sort.newest':'Newest first',
2429
+ 'admin.sort.oldest':'Oldest first',
2430
+ 'admin.filter.role':'Role',
2431
+ 'admin.filter.status':'Status',
2432
+ 'admin.expand':'Expand',
2433
+ 'admin.collapse':'Collapse',
2434
+ 'admin.noContent':'No content',
2435
+ 'admin.summary':'Summary',
2436
+ 'admin.description':'Description',
2437
+ 'admin.contentLabel':'Content',
2263
2438
  'admin.groups':'Groups',
2264
2439
  'admin.newGroup':'+ New Group',
2265
2440
  'admin.groupName':'Group name',
@@ -2273,16 +2448,20 @@ const I18N={
2273
2448
  'admin.add':'Add',
2274
2449
  'admin.remove':'Remove',
2275
2450
  'admin.sharedTasks':'Shared Tasks',
2276
- 'admin.noSharedTasks':'No shared tasks on Hub.',
2451
+ 'admin.noSharedTasks':'No shared tasks in team.',
2277
2452
  'admin.owner':'Owner: ',
2278
2453
  'admin.group':'Group: ',
2454
+ 'admin.kind':'Kind: ',
2455
+ 'admin.role':'Role: ',
2456
+ 'admin.visibility':'Visibility: ',
2457
+ 'admin.session':'Session',
2458
+ 'admin.content':'Content',
2279
2459
  'admin.chunks':'Chunks: ',
2280
2460
  'admin.updated':'Updated: ',
2281
2461
  'admin.sharedSkills':'Shared Skills',
2282
- 'admin.noSharedSkills':'No shared skills on Hub.',
2462
+ 'admin.noSharedSkills':'No shared skills in team.',
2283
2463
  'admin.sharedMemories':'Shared Memories',
2284
- 'admin.noSharedMemories':'No shared memories on Hub.',
2285
- 'admin.tab.sharedMemories':'Shared Memories',
2464
+ 'admin.noSharedMemories':'No shared memories in team.',
2286
2465
  'admin.version':'v',
2287
2466
  'admin.quality':'Quality: ',
2288
2467
  'admin.membersCount':'Members ({n}):',
@@ -2291,6 +2470,7 @@ const I18N={
2291
2470
  'admin.noPermission':'You do not have admin permissions to access this panel.',
2292
2471
  'admin.groupsFailed':'Failed to load groups: ',
2293
2472
  'toast.userApproved':'User approved',
2473
+ 'sharing.approved.toast':'Your join request has been approved!',
2294
2474
  'toast.userRejected':'User rejected',
2295
2475
  'toast.approveFail':'Approve failed',
2296
2476
  'toast.rejectFail':'Reject failed',
@@ -2307,27 +2487,32 @@ const I18N={
2307
2487
  'toast.removeFail':'Remove failed',
2308
2488
  'toast.groupNameRequired':'Group name is required',
2309
2489
  'confirm.rejectUser':'Reject this user?',
2490
+ 'confirm.removeUser':'Remove this user from the team? This will revoke their access.',
2491
+ 'confirm.cleanResources':'Also delete all shared resources (tasks, skills, memories) from this user?',
2492
+ 'toast.userRemoved':'User removed',
2493
+ 'toast.removeFail':'Remove failed',
2494
+ 'admin.remove':'Remove',
2310
2495
  'confirm.removeGroupMember':'Remove this member from the group?',
2311
2496
  'confirm.removeMember':'Remove this member?',
2312
2497
  'confirm.deleteGroup':'Delete group "{name}"? Members will be removed.',
2313
2498
  'confirm.deleteGroupShort':'Delete group "{name}"?',
2314
- 'confirm.removeTask':'Remove shared task "{name}" from Hub? This cannot be undone.',
2315
- 'confirm.removeSkill':'Remove shared skill "{name}" from Hub? This cannot be undone.',
2316
- 'confirm.removeMemory':'Remove shared memory "{name}" from Hub? This cannot be undone.',
2499
+ 'confirm.removeTask':'Remove shared task "{name}" from team? This cannot be undone.',
2500
+ 'confirm.removeSkill':'Remove shared skill "{name}" from team? This cannot be undone.',
2501
+ 'confirm.removeMemory':'Remove shared memory "{name}" from team? This cannot be undone.',
2317
2502
  'sharing.disabled':'Sharing disabled',
2318
- 'sharing.disabled.hint':'Enable sharing in plugin config to connect a Hub.',
2319
- 'sharing.hubAdmin':'Hub Admin',
2503
+ 'sharing.disabled.hint':'Enable sharing in settings to connect a team.',
2504
+ 'sharing.hubAdmin':'Team Admin',
2320
2505
  'sharing.client':'Client',
2321
- 'sharing.hubMode':'Hub mode',
2506
+ 'sharing.hubMode':'Server mode',
2322
2507
  'sharing.hubMode.status':'Status: not connected to self',
2323
- 'sharing.hubMode.hint':'Configure sharing.client with hubAddress and userToken pointing to this Hub to enable admin UI.',
2508
+ 'sharing.hubMode.hint':'Configure sharing.client with hubAddress and userToken pointing to this server to enable admin UI.',
2324
2509
  'sharing.clientConfigured':'Client configured',
2325
2510
  'sharing.clientDisconnected':'Status: disconnected',
2326
- 'sharing.clientDisconnected.hint':'Viewer will keep showing local data; Hub actions may fail until the connection is restored.',
2511
+ 'sharing.clientDisconnected.hint':'Viewer will keep showing local data; team actions may fail until the connection is restored.',
2327
2512
  'sharing.clientNotConfigured':'Client not configured',
2328
2513
  'sharing.clientNotConfigured.hint':'Set hubAddress and userToken in sharing.client to enable team features.',
2329
2514
  'sharing.settingsDisabled':'Sharing is disabled.',
2330
- 'sharing.settingsDisabled.hint':'Enable sharing in config to use Hub memory and skill collaboration.',
2515
+ 'sharing.settingsDisabled.hint':'Enable sharing in settings to use team memory and skill collaboration.',
2331
2516
  'sharing.noTeam':'No team connection.',
2332
2517
  'sharing.adminUnavailable':'Admin tools unavailable.',
2333
2518
  'sharing.adminEnabled':'Admin controls enabled',
@@ -2342,15 +2527,15 @@ const I18N={
2342
2527
  'sharing.username.taken':'Username already taken',
2343
2528
  'sharing.username.updated':'Username updated',
2344
2529
  'sharing.username.error':'Failed to update username',
2345
- 'sharing.hubNotConfigured':'Hub is running but client connection is not configured.',
2346
- 'sharing.hubNotConfigured.hint':'Add sharing.client.hubAddress and sharing.client.userToken pointing to this Hub to enable the admin interface.',
2347
- 'sharing.notConnected':'Not connected to Hub.',
2530
+ 'sharing.hubNotConfigured':'Server is running but client connection is not configured.',
2531
+ 'sharing.hubNotConfigured.hint':'Add sharing.client.hubAddress and sharing.client.userToken pointing to this server to enable the admin interface.',
2532
+ 'sharing.notConnected':'Not connected to team server.',
2348
2533
  'sharing.role':'Role:',
2349
2534
  'sharing.mode':'Mode:',
2350
- 'sharing.role.hub':'Server (hosting the Hub)',
2351
- 'sharing.role.client':'Member (connected to Hub)',
2535
+ 'sharing.role.hub':'Server (hosting the team)',
2536
+ 'sharing.role.client':'Member (connected to team)',
2352
2537
  'sharing.clientConfiguredLabel':'Client configured:',
2353
- 'sharing.configuredHub':'Configured Hub:',
2538
+ 'sharing.configuredHub':'Team Server:',
2354
2539
  'sharing.connected':'Connected:',
2355
2540
  'sharing.yes':'yes',
2356
2541
  'sharing.no':'no',
@@ -2361,9 +2546,9 @@ const I18N={
2361
2546
  'sharing.loadingGroups':'Loading groups...',
2362
2547
  'sharing.noGroupsYet':'No groups yet.',
2363
2548
  'search.localResults':'Local Results',
2364
- 'search.hubResults':'Hub Results',
2549
+ 'search.hubResults':'Team Results',
2365
2550
  'search.noLocal':'No local results.',
2366
- 'search.noHub':'No Hub results.',
2551
+ 'search.noHub':'No team results.',
2367
2552
  'search.viewDetail':'View Detail',
2368
2553
  'search.sharedMemory':'Shared Memory',
2369
2554
  'search.loadFailed':'Failed to load shared memory',
@@ -2371,20 +2556,48 @@ const I18N={
2371
2556
  'share.shareBtn':'Share',
2372
2557
  'share.updateBtn':'Update',
2373
2558
  'share.unshareBtn':'Unshare',
2374
- 'toast.taskShared':'Task shared',
2559
+ 'share.hub.sharedBadge':'Shared to Team',
2560
+ 'share.hub.shareBtn':'Share to Team',
2561
+ 'share.hub.unshareBtn':'Remove from Team',
2562
+ 'share.status.thisAgent':'This Agent',
2563
+ 'share.status.agents':'Local',
2564
+ 'share.status.hub':'Team',
2565
+ 'share.scope.title':'Sharing Scope',
2566
+ 'share.scope.private':'This Agent Only',
2567
+ 'share.scope.local':'All Local Agents',
2568
+ 'share.scope.team':'Team',
2569
+ 'share.scope.current':'Current',
2570
+ 'share.scope.teamDisabled':'Not connected to team server',
2571
+ 'share.scope.teamIncludes':'Includes visibility to all local agents',
2572
+ 'share.scope.confirm':'Confirm',
2573
+ 'share.scope.cancel':'Cancel',
2574
+ 'share.scope.shrinkToLocal':'This will remove the content from team. Team members will no longer be able to find it. Continue?',
2575
+ 'share.scope.shrinkToPrivate':'This will remove the content from team and local sharing. Continue?',
2576
+ 'share.scope.shrinkLocalToPrivate':'This will stop sharing with other local agents. Continue?',
2577
+ 'share.scope.changed':'Sharing scope updated',
2578
+ 'share.scope.changeFail':'Failed to update sharing scope',
2579
+ 'share.scope.inactiveDisabled':'Merged/duplicate memories cannot be shared (not included in search results)',
2580
+ 'share.scope.taskNotCompleted':'Only completed tasks can be shared (active/skipped tasks are excluded)',
2581
+ 'share.scope.skillNotActive':'Only active skills can be shared (draft/archived skills are excluded)',
2582
+ 'skill.pullToLocal':'Pull to Local',
2583
+ 'toast.taskShared':'Task shared to team',
2375
2584
  'toast.taskShareFail':'Task share failed',
2376
- 'toast.taskUnshared':'Task unshared',
2585
+ 'toast.taskUnshared':'Task removed from team',
2377
2586
  'toast.taskUnshareFail':'Task unshare failed',
2378
- 'toast.memoryShared':'Memory shared',
2587
+ 'toast.memoryShared':'Memory shared to team',
2379
2588
  'toast.memoryShareFail':'Memory share failed',
2380
- 'toast.memoryUnshared':'Memory unshared',
2589
+ 'toast.memoryUnshared':'Memory removed from team',
2381
2590
  'toast.memoryUnshareFail':'Memory unshare failed',
2382
- 'toast.skillShared':'Skill shared',
2591
+ 'toast.skillShared':'Skill shared to team',
2383
2592
  'toast.skillShareFail':'Skill share failed',
2384
- 'toast.skillUnshared':'Skill unshared',
2593
+ 'toast.skillUnshared':'Skill removed from team',
2385
2594
  'toast.skillUnshareFail':'Skill unshare failed',
2386
2595
  'share.memoryVisibilityPrompt':'Share visibility (public or group):',
2387
- 'share.memoryUnshareConfirm':'Unshare this memory?',
2596
+ 'share.memoryUnshareConfirm':'Remove this memory from team?',
2597
+ 'share.memoryLocalUnshareConfirm':'Stop sharing this memory locally?',
2598
+ 'share.localUnshareConfirm':'Stop sharing locally?',
2599
+ 'share.teamUnshareConfirm':'Remove from team sharing?',
2600
+ 'share.local.originalOwnerMissing':'This shared memory has no recorded original owner, so it cannot be restored automatically.',
2388
2601
  'share.group':'Group',
2389
2602
  'share.public':'Public',
2390
2603
  'toast.skillPulled':'Skill pulled to local storage',
@@ -2415,35 +2628,35 @@ const I18N={
2415
2628
  'update.failed':'Update failed',
2416
2629
  'update.restarting':'Restarting service, page will refresh automatically...',
2417
2630
  'update.dismiss':'Dismiss',
2418
- 'sharing.disable.confirm.hub':'You are about to shut down the Hub 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?',
2419
- 'sharing.disable.confirm.client':'You are about to disconnect from the team Hub.\\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?',
2631
+ '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?',
2632
+ '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?',
2420
2633
  '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',
2421
2634
  'admin.notEnabled.title':'Team sharing is not enabled',
2422
- '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 Hub sharing first.',
2423
- 'admin.notEnabled.setupHub':'Set Up as Hub Server',
2635
+ '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.',
2636
+ 'admin.notEnabled.setupHub':'Set Up as Team Server',
2424
2637
  'admin.notEnabled.joinTeam':'Join an Existing Team',
2425
2638
  'admin.notEnabled.hint':'If you have previously configured sharing, your data is still preserved. Re-enabling sharing will restore access to all shared content.',
2426
- 'sharing.disconnected.hint':'Unable to reach the Hub server. The Hub may be offline or the network is unavailable.',
2639
+ 'sharing.disconnected.hint':'Unable to reach the team server. The server may be offline or the network is unavailable.',
2427
2640
  'sharing.retryConnection':'Retry Connection',
2428
2641
  'sharing.retryConnection.loading':'Connecting...',
2429
2642
  'sharing.retryConnection.success':'Connected successfully!',
2430
- 'sharing.retryConnection.fail':'Still unable to connect. Check if the Hub is online.',
2643
+ 'sharing.retryConnection.fail':'Still unable to connect. Check if the team server is online.',
2431
2644
  'guide.title':'Get Started with Team Collaboration',
2432
2645
  'guide.subtitle':'MemOS supports team memory sharing. Choose one of the following options to enable collaboration, or continue using local-only mode.',
2433
2646
  'guide.join.title':'Join a Remote Team',
2434
- 'guide.join.desc':'Your team already has a Hub running? Join it to share memories, tasks and skills with team members.',
2435
- 'guide.join.s1':'Ask your Hub admin for the Hub Address and Team Token',
2436
- 'guide.join.s2':'Go to Settings \u2192 Hub & Team, enable sharing, select "Client" mode',
2437
- 'guide.join.s3':'Fill in Hub Address and Team Token, click "Test Connection"',
2647
+ 'guide.join.desc':'Your team already has a server running? Join it to share memories, tasks and skills with team members.',
2648
+ 'guide.join.s1':'Ask your team admin for the Server Address and Team Token',
2649
+ 'guide.join.s2':'Go to Settings \u2192 Team Sharing, enable sharing, select "Client" mode',
2650
+ 'guide.join.s3':'Fill in Server Address and Team Token, click "Test Connection"',
2438
2651
  'guide.join.s4':'Save settings and restart the OpenClaw gateway (page refreshes automatically)',
2439
2652
  'guide.join.btn':'\u2192 Configure Client Mode',
2440
- 'guide.hub.title':'Start Your Own Hub',
2441
- 'guide.hub.desc':'Be the team server. Run a Hub on this device so others can connect and share memories with you.',
2442
- 'guide.hub.s1':'Go to Settings \u2192 Hub & Team, enable sharing, select "Hub" mode',
2653
+ 'guide.hub.title':'Start Your Own Team Server',
2654
+ 'guide.hub.desc':'Be the team server. Run it on this device so others can connect and share memories with you.',
2655
+ 'guide.hub.s1':'Go to Settings \u2192 Team Sharing, enable sharing, select "Server" mode',
2443
2656
  'guide.hub.s2':'Set a team name, save settings, and restart the gateway (page refreshes automatically)',
2444
- 'guide.hub.s3':'Share the Hub Address and Team Token with your team members',
2657
+ 'guide.hub.s3':'Share the Server Address and Team Token with your team members',
2445
2658
  'guide.hub.s4':'Approve join requests in the Admin Panel',
2446
- 'guide.hub.btn':'\u2192 Configure Hub Mode'
2659
+ 'guide.hub.btn':'\u2192 Configure Server Mode'
2447
2660
  },
2448
2661
  zh:{
2449
2662
  'title':'MemOS 记忆',
@@ -2490,14 +2703,14 @@ const I18N={
2490
2703
  'skills.search.local':'本地',
2491
2704
  'skills.search.noresult':'未找到匹配的技能',
2492
2705
  'skills.load.error':'加载技能失败',
2493
- 'skills.hub.title':'\u{1F310} Hub 共享技能',
2706
+ 'skills.hub.title':'\u{1F310} 团队共享技能',
2494
2707
  'scope.local':'本地',
2495
2708
  'scope.group':'团队',
2496
2709
  'scope.all':'全部',
2497
- 'skills.visibility.public':'公开',
2498
- 'skills.visibility.private':'私有',
2499
- 'skills.setPublic':'设为公开',
2500
- 'skills.setPrivate':'设为私有',
2710
+ 'skills.visibility.public':'本机共享',
2711
+ 'skills.visibility.private':'仅本智能体',
2712
+ 'skills.setPublic':'共享给本机智能体',
2713
+ 'skills.setPrivate':'仅本智能体',
2501
2714
  'tasks.total':'任务总数',
2502
2715
  'tasks.active':'进行中',
2503
2716
  'tasks.completed':'已完成',
@@ -2514,6 +2727,17 @@ const I18N={
2514
2727
  'tasks.skipped.default':'对话内容过少,未生成摘要。该任务不会出现在检索结果中。',
2515
2728
  'refresh':'\\u21BB 刷新',
2516
2729
  'logout':'退出',
2730
+ 'notif.title':'\\u{1F514} 通知',
2731
+ 'notif.markAll':'全部已读',
2732
+ 'notif.empty':'暂无通知',
2733
+ 'notif.removed.memory':'你共享的记忆被管理员移除',
2734
+ 'notif.removed.task':'你共享的任务被管理员移除',
2735
+ 'notif.removed.skill':'你共享的技能被管理员移除',
2736
+ 'notif.clearAll':'清除全部',
2737
+ 'notif.timeAgo.just':'刚刚',
2738
+ 'notif.timeAgo.min':'{n}分钟前',
2739
+ 'notif.timeAgo.hour':'{n}小时前',
2740
+ 'notif.timeAgo.day':'{n}天前',
2517
2741
  'stat.memories':'记忆',
2518
2742
  'stat.sessions':'会话',
2519
2743
  'stat.embeddings':'嵌入',
@@ -2568,6 +2792,8 @@ const I18N={
2568
2792
  'chart.nodata':'此范围内暂无数据',
2569
2793
  'chart.nocalls':'此范围内暂无查看器调用',
2570
2794
  'chart.toolperf':'工具响应耗时',
2795
+ 'chart.apply':'应用',
2796
+ 'chart.selectRange':'请选择时间范围',
2571
2797
  'chart.list':'列表',
2572
2798
  'chart.search':'搜索',
2573
2799
  'modal.edit':'编辑记忆',
@@ -2583,11 +2809,14 @@ const I18N={
2583
2809
  'toast.deleted':'记忆已删除',
2584
2810
  'toast.opfail':'操作失败',
2585
2811
  'toast.delfail':'删除失败',
2586
- 'toast.setPublic':'已设为公开',
2587
- 'toast.setPrivate':'已设为私有',
2812
+ 'toast.setPublic':'已共享给本机智能体',
2813
+ 'toast.setPrivate':'现在仅本智能体可见',
2588
2814
  'toast.cleared':'所有记忆已清除',
2589
2815
  'toast.clearfail':'清除失败',
2590
2816
  'toast.notfound':'缓存中未找到此记忆',
2817
+ 'confirm.title':'确认',
2818
+ 'confirm.ok':'确定',
2819
+ 'confirm.cancel':'取消',
2591
2820
  'confirm.delete':'确定要删除这条记忆吗?',
2592
2821
  'confirm.clearall':'确定要删除所有记忆?此操作不可撤销。',
2593
2822
  'confirm.clearall2':'你真的确定吗?',
@@ -2763,16 +2992,16 @@ const I18N={
2763
2992
  'tasks.error.detail':'加载任务详情失败',
2764
2993
  'tasks.untitled.related':'未命名',
2765
2994
  'tab.admin':'\u{1F6E1} 管理',
2766
- 'settings.hub':'Hub 与团队',
2767
- 'settings.hub.enable':'启用 Hub 共享',
2995
+ 'settings.hub':'团队共享',
2996
+ 'settings.hub.enable':'启用团队共享',
2768
2997
  'settings.hub.enable.hint':'关闭时仅本地使用,不影响其他功能。',
2769
- 'settings.hub.enable.label':'启用 Hub 共享',
2998
+ 'settings.hub.enable.label':'启用团队共享',
2770
2999
  'settings.hub.role':'角色',
2771
- 'settings.hub.role.hub':'Hub(服务端)',
2772
- 'settings.hub.role.client':'Client(连接到 Hub)',
2773
- 'settings.hub.role.hint':'Hub = 本机做服务端;Client = 连接别人的服务端。两种模式互斥,不能同时使用。',
2774
- 'settings.hub.port':'Hub 端口',
2775
- 'settings.hub.port.hint':'Hub 服务端口,默认 18800',
3000
+ 'settings.hub.role.hub':'服务端(托管团队)',
3001
+ 'settings.hub.role.client':'客户端(加入团队)',
3002
+ 'settings.hub.role.hint':'服务端 = 本机托管团队;客户端 = 加入别人的团队。两种模式互斥,不能同时使用。',
3003
+ 'settings.hub.port':'服务端口',
3004
+ 'settings.hub.port.hint':'团队服务端口,默认 18800',
2776
3005
  'settings.hub.teamName':'团队名称',
2777
3006
  'settings.hub.teamName.hint':'你的团队显示名称',
2778
3007
  'settings.hub.teamToken':'团队令牌',
@@ -2781,23 +3010,23 @@ const I18N={
2781
3010
  'settings.hub.hubSteps.title':'快速配置(3 步)',
2782
3011
  'settings.hub.hubSteps.s1':'填写下方团队名称(或保持默认)',
2783
3012
  'settings.hub.hubSteps.s2':'点击"保存设置",然后重启 OpenClaw 网关',
2784
- 'settings.hub.hubSteps.s3':'将下方的 Hub 地址和团队令牌分享给团队成员',
3013
+ 'settings.hub.hubSteps.s3':'将下方的服务器地址和团队令牌分享给团队成员',
2785
3014
  'settings.hub.clientSteps.title':'快速配置(3 步)',
2786
- 'settings.hub.clientSteps.s1':'向 Hub 管理员获取 Hub 地址和团队令牌',
3015
+ 'settings.hub.clientSteps.s1':'向团队管理员获取服务器地址和团队令牌',
2787
3016
  'settings.hub.clientSteps.s2':'填入下方,点击"测试连接"验证连通性',
2788
3017
  'settings.hub.clientSteps.s3':'点击「保存设置」,然后重启 OpenClaw 网关(页面会自动刷新)',
2789
3018
  'settings.hub.shareInfo.title':'请将以下信息分享给团队成员:',
2790
3019
  'settings.hub.shareInfo.yourIP':'你的IP',
2791
3020
  'settings.hub.shareInfo.clickCopy':'点击复制',
2792
- 'settings.hub.restartAlert':'Hub 共享配置已保存!请重启 OpenClaw 网关使配置生效。\\n\\n执行命令:openclaw gateway stop && openclaw gateway start',
2793
- 'settings.hub.hubAddress':'Hub 地址',
2794
- 'settings.hub.hubAddress.hint':'Hub 服务器地址,如 192.168.1.100:18800',
3021
+ 'settings.hub.restartAlert':'团队共享配置已保存!请重启 OpenClaw 网关使配置生效。\\n\\n执行命令:openclaw gateway stop && openclaw gateway start',
3022
+ 'settings.hub.hubAddress':'服务器地址',
3023
+ 'settings.hub.hubAddress.hint':'团队服务器地址,如 192.168.1.100:18800',
2795
3024
  'settings.hub.teamTokenClient':'团队令牌',
2796
- 'settings.hub.teamTokenClient.hint':'向 Hub 管理员获取此令牌以加入团队',
3025
+ 'settings.hub.teamTokenClient.hint':'向团队管理员获取此令牌以加入团队',
2797
3026
  'settings.hub.userToken':'用户令牌',
2798
3027
  'settings.hub.userToken.hint':'通常在加入团队后自动获取,无需手动填写。',
2799
3028
  'settings.hub.testConnection':'测试连接',
2800
- 'settings.hub.test.noAddr':'请先输入 Hub 地址',
3029
+ 'settings.hub.test.noAddr':'请先输入服务器地址',
2801
3030
  'settings.hub.test.testing':'测试中...',
2802
3031
  'settings.hub.test.ok':'连接成功',
2803
3032
  'settings.hub.test.fail':'连接失败',
@@ -2813,16 +3042,16 @@ const I18N={
2813
3042
  'sharing.sidebar.notConfigured':'未配置',
2814
3043
  'sharing.sidebar.identity':'身份:',
2815
3044
  'sharing.sidebar.admin':'管理员',
2816
- 'sharing.sidebar.targetHub':'目标 Hub:',
2817
- 'sharing.pendingApproval.hint':'加入申请已提交,请等待 Hub 管理员审核通过。',
2818
- 'sharing.rejected.hint':'您的加入申请已被 Hub 管理员拒绝,请联系管理员或重新申请。',
3045
+ 'sharing.sidebar.targetHub':'团队服务器:',
3046
+ 'sharing.pendingApproval.hint':'加入申请已提交,请等待团队管理员审核通过。',
3047
+ 'sharing.rejected.hint':'您的加入申请已被团队管理员拒绝,请联系管理员或重新申请。',
2819
3048
  'sharing.retryJoin':'重新申请',
2820
3049
  'sharing.retryJoin.hint':'清除本地连接数据并重新提交加入申请',
2821
3050
  'sharing.retryJoin.confirm':'这将清除当前连接数据并重新提交加入申请,是否继续?',
2822
3051
  'sharing.retryJoin.success':'加入申请已重新提交,请等待管理员审核。',
2823
3052
  'sharing.retryJoin.fail':'重新申请失败',
2824
- 'sharing.cannotJoinSelf':'不能加入自己的 Hub,请输入远程 Hub 地址。',
2825
- 'scope.hub':'Hub',
3053
+ 'sharing.cannotJoinSelf':'不能加入自己的服务端,请输入远程服务器地址。',
3054
+ 'scope.hub':'团队',
2826
3055
  'memory.detail.title':'记忆详情',
2827
3056
  'memory.detail.loading':'加载中...',
2828
3057
  'memory.detail.notFound':'未找到该记忆',
@@ -2830,12 +3059,13 @@ const I18N={
2830
3059
  'memory.detail.created':'创建于 ',
2831
3060
  'memory.detail.updated':'更新于 ',
2832
3061
  'memory.detail.viewTarget':'查看目标: ',
2833
- 'admin.title':'Hub 管理面板',
3062
+ 'admin.title':'团队管理面板',
2834
3063
  'admin.subtitle':'管理团队成员和共享资源',
2835
3064
  'admin.refresh':'\u21BB 刷新',
2836
3065
  'admin.tab.users':'用户',
3066
+ 'admin.tab.tasks':'共享任务',
2837
3067
  'admin.tab.groups':'分组',
2838
- 'admin.tab.memories':'共享任务',
3068
+ 'admin.tab.memories':'共享记忆',
2839
3069
  'admin.tab.skills':'共享技能',
2840
3070
  'admin.stat.activeUsers':'活跃用户',
2841
3071
  'admin.stat.pending':'待审核',
@@ -2849,6 +3079,43 @@ const I18N={
2849
3079
  'admin.approve':'批准',
2850
3080
  'admin.reject':'拒绝',
2851
3081
  'admin.device':'设备:',
3082
+ 'admin.joined':'加入:',
3083
+ 'admin.approved':'审批:',
3084
+ 'admin.lastActive':'最后活跃:',
3085
+ 'admin.ip':'IP:',
3086
+ 'admin.contrib.memories':'记忆',
3087
+ 'admin.contrib.tasks':'任务',
3088
+ 'admin.contrib.skills':'技能',
3089
+ 'admin.groups':'分组:',
3090
+ 'admin.neverActive':'从未',
3091
+ 'admin.promoteAdmin':'提升为管理员',
3092
+ 'admin.demoteMember':'降为成员',
3093
+ 'admin.editName':'编辑名称',
3094
+ 'admin.lastAdminHint':'唯一管理员 — 无法删除或降级',
3095
+ 'admin.editNamePrompt':'请输入新用户名:',
3096
+ 'confirm.promoteAdmin':'确定要将此用户提升为管理员吗?管理员可以管理所有团队成员和资源。',
3097
+ 'confirm.demoteMember':'确定要将此管理员降为普通成员吗?',
3098
+ 'toast.roleChanged':'角色已更新',
3099
+ 'toast.roleChangeFail':'角色更新失败',
3100
+ 'toast.usernameChanged':'用户名已更新',
3101
+ 'toast.renameFail':'重命名失败',
3102
+ 'toast.invalidUsername':'用户名长度需为 2-32 个字符',
3103
+ 'admin.filterAll':'全部用户',
3104
+ 'admin.filterAllValues':'全部',
3105
+ 'admin.filter.owner':'全部用户',
3106
+ 'admin.filter.visibility':'可见性',
3107
+ 'admin.filter.group':'团队',
3108
+ 'admin.search.placeholder':'搜索...',
3109
+ 'admin.sort.newest':'最新优先',
3110
+ 'admin.sort.oldest':'最早优先',
3111
+ 'admin.filter.role':'角色',
3112
+ 'admin.filter.status':'状态',
3113
+ 'admin.expand':'展开',
3114
+ 'admin.collapse':'收起',
3115
+ 'admin.noContent':'暂无内容',
3116
+ 'admin.summary':'摘要',
3117
+ 'admin.description':'描述',
3118
+ 'admin.contentLabel':'内容',
2852
3119
  'admin.groups':'分组',
2853
3120
  'admin.newGroup':'+ 新建分组',
2854
3121
  'admin.groupName':'分组名称',
@@ -2862,16 +3129,20 @@ const I18N={
2862
3129
  'admin.add':'添加',
2863
3130
  'admin.remove':'移除',
2864
3131
  'admin.sharedTasks':'共享任务',
2865
- 'admin.noSharedTasks':'Hub 上暂无共享任务。',
3132
+ 'admin.noSharedTasks':'团队暂无共享任务。',
2866
3133
  'admin.owner':'归属:',
2867
3134
  'admin.group':'分组:',
3135
+ 'admin.kind':'类型:',
3136
+ 'admin.role':'角色:',
3137
+ 'admin.visibility':'可见性:',
3138
+ 'admin.session':'会话',
3139
+ 'admin.content':'内容',
2868
3140
  'admin.chunks':'记忆片段:',
2869
3141
  'admin.updated':'更新于:',
2870
3142
  'admin.sharedSkills':'共享技能',
2871
- 'admin.noSharedSkills':'Hub 上暂无共享技能。',
3143
+ 'admin.noSharedSkills':'团队暂无共享技能。',
2872
3144
  'admin.sharedMemories':'共享记忆',
2873
- 'admin.noSharedMemories':'Hub 上暂无共享记忆。',
2874
- 'admin.tab.sharedMemories':'共享记忆',
3145
+ 'admin.noSharedMemories':'团队暂无共享记忆。',
2875
3146
  'admin.version':'v',
2876
3147
  'admin.quality':'质量:',
2877
3148
  'admin.membersCount':'成员({n}):',
@@ -2880,6 +3151,7 @@ const I18N={
2880
3151
  'admin.noPermission':'您没有管理员权限,无法访问此面板。',
2881
3152
  'admin.groupsFailed':'加载分组失败:',
2882
3153
  'toast.userApproved':'用户已批准',
3154
+ 'sharing.approved.toast':'您的加入申请已通过审核!',
2883
3155
  'toast.userRejected':'用户已拒绝',
2884
3156
  'toast.approveFail':'批准失败',
2885
3157
  'toast.rejectFail':'拒绝失败',
@@ -2896,27 +3168,32 @@ const I18N={
2896
3168
  'toast.removeFail':'移除失败',
2897
3169
  'toast.groupNameRequired':'请输入分组名称',
2898
3170
  'confirm.rejectUser':'确定要拒绝此用户吗?',
3171
+ 'confirm.removeUser':'确定要移除此用户吗?移除后该用户将无法访问团队资源。',
3172
+ 'confirm.cleanResources':'是否同时删除该用户共享的所有资源(任务、技能、记忆)?',
3173
+ 'toast.userRemoved':'用户已移除',
3174
+ 'toast.removeFail':'移除失败',
3175
+ 'admin.remove':'移除',
2899
3176
  'confirm.removeGroupMember':'确定要将此成员移出分组吗?',
2900
3177
  'confirm.removeMember':'确定要移除此成员吗?',
2901
3178
  'confirm.deleteGroup':'确定要删除分组「{name}」吗?成员将被移除。',
2902
3179
  'confirm.deleteGroupShort':'确定要删除分组「{name}」吗?',
2903
- 'confirm.removeTask':'确定要从 Hub 移除共享任务「{name}」吗?此操作不可撤销。',
2904
- 'confirm.removeSkill':'确定要从 Hub 移除共享技能「{name}」吗?此操作不可撤销。',
2905
- 'confirm.removeMemory':'确定要从 Hub 移除共享记忆「{name}」吗?此操作不可撤销。',
3180
+ 'confirm.removeTask':'确定要从团队移除共享任务「{name}」吗?此操作不可撤销。',
3181
+ 'confirm.removeSkill':'确定要从团队移除共享技能「{name}」吗?此操作不可撤销。',
3182
+ 'confirm.removeMemory':'确定要从团队移除共享记忆「{name}」吗?此操作不可撤销。',
2906
3183
  'sharing.disabled':'共享已禁用',
2907
- 'sharing.disabled.hint':'在插件配置中启用共享以连接 Hub。',
2908
- 'sharing.hubAdmin':'Hub 管理员',
3184
+ 'sharing.disabled.hint':'在设置中启用共享以连接团队。',
3185
+ 'sharing.hubAdmin':'团队管理员',
2909
3186
  'sharing.client':'客户端',
2910
- 'sharing.hubMode':'Hub 模式',
3187
+ 'sharing.hubMode':'服务端模式',
2911
3188
  'sharing.hubMode.status':'状态:未连接到自身',
2912
- 'sharing.hubMode.hint':'配置 sharing.client 的 hubAddress 和 userToken 指向此 Hub 以启用管理界面。',
3189
+ 'sharing.hubMode.hint':'配置 sharing.client 的 hubAddress 和 userToken 指向此服务端以启用管理界面。',
2913
3190
  'sharing.clientConfigured':'客户端已配置',
2914
3191
  'sharing.clientDisconnected':'状态:已断开',
2915
- 'sharing.clientDisconnected.hint':'查看器将继续显示本地数据;Hub 操作可能在连接恢复前失败。',
3192
+ 'sharing.clientDisconnected.hint':'查看器将继续显示本地数据;团队操作可能在连接恢复前失败。',
2916
3193
  'sharing.clientNotConfigured':'客户端未配置',
2917
3194
  'sharing.clientNotConfigured.hint':'设置 sharing.client 中的 hubAddress 和 userToken 以启用团队功能。',
2918
3195
  'sharing.settingsDisabled':'共享已禁用。',
2919
- 'sharing.settingsDisabled.hint':'在配置中启用共享以使用 Hub 记忆和技能协作。',
3196
+ 'sharing.settingsDisabled.hint':'在设置中启用共享以使用团队记忆和技能协作。',
2920
3197
  'sharing.noTeam':'无团队连接。',
2921
3198
  'sharing.adminUnavailable':'管理工具不可用。',
2922
3199
  'sharing.adminEnabled':'管理控制已启用',
@@ -2931,15 +3208,15 @@ const I18N={
2931
3208
  'sharing.username.taken':'用户名已被占用',
2932
3209
  'sharing.username.updated':'用户名已更新',
2933
3210
  'sharing.username.error':'更新用户名失败',
2934
- 'sharing.hubNotConfigured':'Hub 正在运行,但客户端连接未配置。',
2935
- 'sharing.hubNotConfigured.hint':'添加 sharing.client.hubAddress 和 sharing.client.userToken 指向此 Hub 以启用管理界面。',
2936
- 'sharing.notConnected':'未连接到 Hub。',
3211
+ 'sharing.hubNotConfigured':'服务端正在运行,但客户端连接未配置。',
3212
+ 'sharing.hubNotConfigured.hint':'添加 sharing.client.hubAddress 和 sharing.client.userToken 指向此服务端以启用管理界面。',
3213
+ 'sharing.notConnected':'未连接到团队服务器。',
2937
3214
  'sharing.role':'角色:',
2938
3215
  'sharing.mode':'身份:',
2939
- 'sharing.role.hub':'服务端(Hub 主机)',
2940
- 'sharing.role.client':'成员(连接到 Hub)',
3216
+ 'sharing.role.hub':'服务端(托管团队)',
3217
+ 'sharing.role.client':'成员(连接到团队)',
2941
3218
  'sharing.clientConfiguredLabel':'客户端已配置:',
2942
- 'sharing.configuredHub':'配置的 Hub:',
3219
+ 'sharing.configuredHub':'团队服务器:',
2943
3220
  'sharing.connected':'已连接:',
2944
3221
  'sharing.yes':'是',
2945
3222
  'sharing.no':'否',
@@ -2950,9 +3227,9 @@ const I18N={
2950
3227
  'sharing.loadingGroups':'正在加载分组...',
2951
3228
  'sharing.noGroupsYet':'暂无分组。',
2952
3229
  'search.localResults':'本地结果',
2953
- 'search.hubResults':'Hub 结果',
3230
+ 'search.hubResults':'团队结果',
2954
3231
  'search.noLocal':'无本地结果。',
2955
- 'search.noHub':'无 Hub 结果。',
3232
+ 'search.noHub':'无团队结果。',
2956
3233
  'search.viewDetail':'查看详情',
2957
3234
  'search.sharedMemory':'共享记忆',
2958
3235
  'search.loadFailed':'加载共享记忆失败',
@@ -2960,20 +3237,48 @@ const I18N={
2960
3237
  'share.shareBtn':'共享',
2961
3238
  'share.updateBtn':'更新共享',
2962
3239
  'share.unshareBtn':'取消共享',
2963
- 'toast.taskShared':'任务已共享',
3240
+ 'share.hub.sharedBadge':'已共享到团队',
3241
+ 'share.hub.shareBtn':'共享到团队',
3242
+ 'share.hub.unshareBtn':'从团队移除',
3243
+ 'share.status.thisAgent':'当前智能体',
3244
+ 'share.status.agents':'本机',
3245
+ 'share.status.hub':'团队',
3246
+ 'share.scope.title':'共享范围',
3247
+ 'share.scope.private':'仅本智能体',
3248
+ 'share.scope.local':'本机所有智能体',
3249
+ 'share.scope.team':'团队',
3250
+ 'share.scope.current':'当前',
3251
+ 'share.scope.teamDisabled':'未连接团队服务器',
3252
+ 'share.scope.teamIncludes':'包含本机所有智能体的可见性',
3253
+ 'share.scope.confirm':'确认',
3254
+ 'share.scope.cancel':'取消',
3255
+ 'share.scope.shrinkToLocal':'将从团队中移除此内容,团队成员将无法搜索到。确认?',
3256
+ 'share.scope.shrinkToPrivate':'将从团队和本机共享中移除此内容。确认?',
3257
+ 'share.scope.shrinkLocalToPrivate':'将停止共享给其他智能体。确认?',
3258
+ 'share.scope.changed':'共享范围已更新',
3259
+ 'share.scope.changeFail':'更新共享范围失败',
3260
+ 'share.scope.inactiveDisabled':'已合并/重复的记忆无法共享(不会出现在检索结果中)',
3261
+ 'share.scope.taskNotCompleted':'仅已完成的任务可以共享(进行中/已跳过的任务无法共享)',
3262
+ 'share.scope.skillNotActive':'仅活跃的技能可以共享(草稿/已归档的技能无法共享)',
3263
+ 'skill.pullToLocal':'拉取到本地',
3264
+ 'toast.taskShared':'任务已共享到团队',
2964
3265
  'toast.taskShareFail':'任务共享失败',
2965
- 'toast.taskUnshared':'任务已取消共享',
3266
+ 'toast.taskUnshared':'任务已从团队移除',
2966
3267
  'toast.taskUnshareFail':'取消共享失败',
2967
- 'toast.memoryShared':'记忆已共享',
3268
+ 'toast.memoryShared':'记忆已共享到团队',
2968
3269
  'toast.memoryShareFail':'记忆共享失败',
2969
- 'toast.memoryUnshared':'记忆已取消共享',
3270
+ 'toast.memoryUnshared':'记忆已从团队移除',
2970
3271
  'toast.memoryUnshareFail':'记忆取消共享失败',
2971
- 'toast.skillShared':'技能已共享',
3272
+ 'toast.skillShared':'技能已共享到团队',
2972
3273
  'toast.skillShareFail':'技能共享失败',
2973
- 'toast.skillUnshared':'技能已取消共享',
3274
+ 'toast.skillUnshared':'技能已从团队移除',
2974
3275
  'toast.skillUnshareFail':'技能取消共享失败',
2975
- 'share.memoryVisibilityPrompt':'共享可见性(public 或 group):',
2976
- 'share.memoryUnshareConfirm':'取消共享此记忆?',
3276
+ 'share.memoryVisibilityPrompt':'共享范围(public 或 group):',
3277
+ 'share.memoryUnshareConfirm':'从团队移除该记忆?',
3278
+ 'share.memoryLocalUnshareConfirm':'停止本机共享该记忆?',
3279
+ 'share.localUnshareConfirm':'停止本机共享?',
3280
+ 'share.teamUnshareConfirm':'从团队共享中移除?',
3281
+ 'share.local.originalOwnerMissing':'该共享记忆没有记录原始所有者,无法自动恢复。',
2977
3282
  'share.group':'团队',
2978
3283
  'share.public':'公开',
2979
3284
  'toast.skillPulled':'技能已拉取到本地',
@@ -3004,35 +3309,35 @@ const I18N={
3004
3309
  'update.failed':'更新失败',
3005
3310
  'update.restarting':'正在重启服务,页面将自动刷新...',
3006
3311
  'update.dismiss':'关闭',
3007
- 'sharing.disable.confirm.hub':'你即将关闭 Hub 服务。\\n\\n关闭后将会:\\n\\u2022 所有已连接的团队成员将断开连接\\n\\u2022 他们将无法继续同步记忆、任务和技能\\n\\u2022 已共享的数据会保留,重新开启后仍可使用\\n\\n确定要关闭吗?',
3008
- 'sharing.disable.confirm.client':'你即将断开与团队 Hub 的连接。\\n\\n断开后将会:\\n\\u2022 你将无法再接收团队共享的记忆、任务和技能\\n\\u2022 你的本地数据不受影响,会完整保留\\n\\u2022 之后可以随时重新开启共享来恢复连接\\n\\n确定要断开吗?',
3312
+ 'sharing.disable.confirm.hub':'你即将关闭团队服务。\\n\\n关闭后将会:\\n\\u2022 所有已连接的团队成员将断开连接\\n\\u2022 他们将无法继续同步记忆、任务和技能\\n\\u2022 已共享的数据会保留,重新开启后仍可使用\\n\\n确定要关闭吗?',
3313
+ 'sharing.disable.confirm.client':'你即将断开与团队的连接。\\n\\n断开后将会:\\n\\u2022 你将无法再接收团队共享的记忆、任务和技能\\n\\u2022 你的本地数据不受影响,会完整保留\\n\\u2022 之后可以随时重新开启共享来恢复连接\\n\\n确定要断开吗?',
3009
3314
  'sharing.disable.restartAlert':'共享已关闭。请重启 OpenClaw 网关使更改生效。\\n\\n执行命令:openclaw gateway stop && openclaw gateway start',
3010
3315
  'admin.notEnabled.title':'团队共享尚未开启',
3011
- 'admin.notEnabled.desc':'管理面板用于管理团队成员、共享的记忆、任务和技能。使用此功能前,需要先开启 Hub 共享。',
3012
- 'admin.notEnabled.setupHub':'配置为 Hub 服务端',
3316
+ 'admin.notEnabled.desc':'管理面板用于管理团队成员、共享的记忆、任务和技能。使用此功能前,需要先开启团队共享。',
3317
+ 'admin.notEnabled.setupHub':'配置为团队服务端',
3013
3318
  'admin.notEnabled.joinTeam':'加入已有团队',
3014
3319
  'admin.notEnabled.hint':'如果之前配置过共享,你的数据仍然保留。重新开启共享即可恢复访问所有共享内容。',
3015
- 'sharing.disconnected.hint':'无法连接到 Hub 服务器,Hub 可能已下线或网络不可用。',
3320
+ 'sharing.disconnected.hint':'无法连接到团队服务器,服务器可能已下线或网络不可用。',
3016
3321
  'sharing.retryConnection':'重试连接',
3017
3322
  'sharing.retryConnection.loading':'连接中...',
3018
3323
  'sharing.retryConnection.success':'连接成功!',
3019
- 'sharing.retryConnection.fail':'仍然无法连接,请检查 Hub 是否在线。',
3324
+ 'sharing.retryConnection.fail':'仍然无法连接,请检查团队服务器是否在线。',
3020
3325
  'guide.title':'开始团队协作',
3021
3326
  'guide.subtitle':'MemOS 支持团队记忆共享。选择以下方式之一开启协作,或继续使用纯本地模式。',
3022
3327
  'guide.join.title':'加入远程团队',
3023
- 'guide.join.desc':'你的团队已有 Hub 在运行?加入即可与团队成员共享记忆、任务和技能。',
3024
- 'guide.join.s1':'向 Hub 管理员索取 Hub 地址和 Team Token',
3025
- 'guide.join.s2':'前往「设置 → Hub & Team」,开启共享,选择「Client」模式',
3026
- 'guide.join.s3':'填写 Hub 地址和 Team Token,点击「测试连接」',
3328
+ 'guide.join.desc':'你的团队已有服务器在运行?加入即可与团队成员共享记忆、任务和技能。',
3329
+ 'guide.join.s1':'向团队管理员索取服务器地址和团队令牌',
3330
+ 'guide.join.s2':'前往「设置 → 团队共享」,开启共享,选择「客户端」模式',
3331
+ 'guide.join.s3':'填写服务器地址和团队令牌,点击「测试连接」',
3027
3332
  'guide.join.s4':'保存设置并重启 OpenClaw 网关(页面会自动刷新)',
3028
- 'guide.join.btn':'\u2192 配置 Client 模式',
3029
- 'guide.hub.title':'自建 Hub 服务',
3333
+ 'guide.join.btn':'\u2192 配置客户端模式',
3334
+ 'guide.hub.title':'自建团队服务',
3030
3335
  'guide.hub.desc':'将本机作为团队服务端,让其他成员连接过来共享记忆。',
3031
- 'guide.hub.s1':'前往「设置 → Hub & Team」,开启共享,选择「Hub」模式',
3336
+ 'guide.hub.s1':'前往「设置 → 团队共享」,开启共享,选择「服务端」模式',
3032
3337
  'guide.hub.s2':'设置团队名称,保存设置后重启网关(页面会自动刷新)',
3033
- 'guide.hub.s3':'将 Hub 地址和 Team Token 分享给团队成员',
3338
+ 'guide.hub.s3':'将服务器地址和团队令牌分享给团队成员',
3034
3339
  'guide.hub.s4':'在管理面板中审批加入请求',
3035
- 'guide.hub.btn':'\u2192 配置 Hub 模式'
3340
+ 'guide.hub.btn':'\u2192 配置服务端模式'
3036
3341
  }
3037
3342
  };
3038
3343
  const LANG_KEY='memos-viewer-lang';
@@ -3142,6 +3447,10 @@ function _genToken(len){
3142
3447
  function onSharingToggle(){
3143
3448
  var on=document.getElementById('cfgSharingEnabled').checked;
3144
3449
  document.getElementById('sharingConfigPanel').style.display=on?'block':'none';
3450
+ var pw=document.getElementById('sharingPanelsWrap');
3451
+ if(pw) pw.style.display=on?'':'none';
3452
+ var tg=document.getElementById('teamSetupGuide');
3453
+ if(tg) tg.style.display=on?'none':'';
3145
3454
  if(on) selectSharingRole(_sharingRole);
3146
3455
  }
3147
3456
  function selectSharingRole(role){
@@ -3184,10 +3493,10 @@ function updateHubShareInfo(){
3184
3493
  var addr=ip?(ip+':'+esc(port)):('&lt;'+t('settings.hub.shareInfo.yourIP')+'&gt;:'+esc(port));
3185
3494
  var tip=t('settings.hub.shareInfo.clickCopy');
3186
3495
  content.innerHTML=
3187
- '<span style="font-size:11px;color:var(--text-muted);font-weight:500">Hub '+t('settings.hub.hubAddress')+'</span>'+
3188
- '<span style="'+cpStyle+'" onclick="navigator.clipboard.writeText(this.textContent);toast(&#39;Copied!&#39;,&#39;success&#39;)" title="'+tip+'">'+addr+'</span>'+
3496
+ '<span style="font-size:11px;color:var(--text-muted);font-weight:500">'+t('settings.hub.hubAddress')+'</span>'+
3497
+ '<span style="'+cpStyle+'" onclick="navigator.clipboard.writeText(this.textContent);toast(t(&#39;copy.done&#39;),&#39;success&#39;)" title="'+tip+'">'+addr+'</span>'+
3189
3498
  '<span style="font-size:11px;color:var(--text-muted);font-weight:500">Team Token</span>'+
3190
- '<span style="'+cpStyle+'" onclick="navigator.clipboard.writeText(this.textContent);toast(&#39;Copied!&#39;,&#39;success&#39;)" title="'+tip+'">'+esc(token)+'</span>';
3499
+ '<span style="'+cpStyle+'" onclick="navigator.clipboard.writeText(this.textContent);toast(t(&#39;copy.done&#39;),&#39;success&#39;)" title="'+tip+'">'+esc(token)+'</span>';
3191
3500
  };
3192
3501
  if(_cachedLocalIP){renderShare(_cachedLocalIP);return;}
3193
3502
  renderShare('');
@@ -3306,6 +3615,7 @@ function onTaskScopeChange(){
3306
3615
  loadTasks();
3307
3616
  }
3308
3617
 
3618
+ var _clientPendingPollTimer=null;
3309
3619
  async function loadSharingStatus(forcePending){
3310
3620
  try{
3311
3621
  const r=await fetch('/api/sharing/status');
@@ -3315,6 +3625,14 @@ async function loadSharingStatus(forcePending){
3315
3625
  renderSharingSettings(d);
3316
3626
  updateTeamGuide(d);
3317
3627
  if(forcePending && d && d.admin && d.admin.canManageUsers) loadSharingPendingUsers();
3628
+ var conn=d&&d.connection||{};
3629
+ if(conn.pendingApproval&&!_clientPendingPollTimer){
3630
+ _clientPendingPollTimer=setInterval(function(){loadSharingStatus(false);},10000);
3631
+ }else if(!conn.pendingApproval&&_clientPendingPollTimer){
3632
+ clearInterval(_clientPendingPollTimer);
3633
+ _clientPendingPollTimer=null;
3634
+ if(conn.connected) toast(t('sharing.approved.toast'),'success');
3635
+ }
3318
3636
  }catch(e){
3319
3637
  renderSharingSidebar(null);
3320
3638
  renderSharingSettings(null);
@@ -3397,6 +3715,7 @@ function renderSharingSettings(data){
3397
3715
  if(data.role) _sharingRole=data.role;
3398
3716
  var isAdmin=(data.admin&&data.admin.canManageUsers)||(conn.connected&&user.role==='admin')||(actualRole==='hub');
3399
3717
  window._isHubAdmin=isAdmin;
3718
+ if(isAdmin) startAdminPoll();
3400
3719
  var hubAdminBtn=document.getElementById('hubAdminEntryBtn');
3401
3720
 
3402
3721
  if(actualRole==='hub'){
@@ -3470,7 +3789,7 @@ async function retryConnection(){
3470
3789
  }
3471
3790
 
3472
3791
  async function retryHubJoin(){
3473
- if(!confirm(t('sharing.retryJoin.confirm'))) return;
3792
+ if(!(await confirmModal(t('sharing.retryJoin.confirm')))) return;
3474
3793
  try{
3475
3794
  var r=await fetch('/api/sharing/retry-join',{method:'POST',headers:{'Content-Type':'application/json'}});
3476
3795
  var d=await r.json();
@@ -3590,152 +3909,48 @@ function guideGoToHub(role){
3590
3909
  if(card) card.scrollIntoView({behavior:'smooth',block:'start'});
3591
3910
  }
3592
3911
 
3593
- /* ─── Group Manager ─── */
3594
- var groupManagerUsers=[];
3595
- async function loadGroupManager(){
3596
- var panel=document.getElementById('groupManagerPanel');
3597
- if(!panel) return;
3598
- panel.style.display='block';
3599
- panel.innerHTML=t('sharing.loadingGroups');
3600
- try{
3601
- var [gr,ur]=await Promise.all([
3602
- fetch('/api/sharing/groups').then(function(r){return r.json();}),
3603
- fetch('/api/sharing/users').then(function(r){return r.json();})
3604
- ]);
3605
- var groups=Array.isArray(gr.groups)?gr.groups:[];
3606
- groupManagerUsers=Array.isArray(ur.users)?ur.users:[];
3607
- renderGroupManager(panel,groups);
3608
- }catch(e){panel.innerHTML=t('admin.groupsFailed')+esc(String(e));}
3609
- }
3610
- function renderGroupManager(panel,groups){
3611
- var html='<div style="margin-bottom:8px;display:flex;gap:8px;align-items:center;font-size:12px">'+
3612
- '<strong>'+t('admin.groups')+' ('+groups.length+')</strong>'+
3613
- '<button class="btn btn-sm" onclick="showCreateGroupForm()" style="font-size:11px">'+t('admin.newGroup')+'</button>'+
3614
- '</div>';
3615
- html+='<div id="createGroupForm" style="display:none;margin-bottom:10px;padding:10px;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:12px">'+
3616
- '<input id="newGroupName" type="text" placeholder="'+t('admin.groupName')+'" style="width:60%;padding:4px 8px;border:1px solid var(--border);border-radius:6px;font-size:12px;margin-right:6px;background:var(--bg);color:var(--text)">'+
3617
- '<input id="newGroupDesc" type="text" placeholder="'+t('admin.groupDesc')+'" style="width:60%;padding:4px 8px;border:1px solid var(--border);border-radius:6px;font-size:12px;margin-right:6px;margin-top:6px;background:var(--bg);color:var(--text)">'+
3618
- '<div style="margin-top:6px"><button class="btn btn-sm btn-primary" onclick="createGroup()" style="font-size:11px">'+t('admin.create')+'</button> '+
3619
- '<button class="btn btn-sm btn-ghost" onclick="hideCreateGroupForm()" style="font-size:11px">'+t('admin.cancel')+'</button></div>'+
3620
- '</div>';
3621
- if(groups.length===0){
3622
- html+='<div style="font-size:12px;color:var(--text-muted);padding:6px 0">'+t('sharing.noGroupsYet')+'</div>';
3623
- }else{
3624
- html+='<div style="display:flex;flex-direction:column;gap:6px">';
3625
- for(var i=0;i<groups.length;i++){
3626
- var g=groups[i];
3627
- html+='<div style="padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:12px">'+
3628
- '<div style="display:flex;justify-content:space-between;align-items:center">'+
3629
- '<div><strong style="font-size:12px">'+esc(g.name)+'</strong>'+(g.description?' — <span style="color:var(--text-sec);font-size:11px">'+esc(g.description)+'</span>':'')+'</div>'+
3630
- '<div style="display:flex;gap:6px">'+
3631
- '<button class="btn btn-sm" onclick="toggleGroupMembers(&quot;'+escAttr(g.id)+'&quot;)" style="font-size:11px;padding:2px 8px">'+t('admin.members')+'</button>'+
3632
- '<button class="btn btn-sm btn-ghost" onclick="deleteGroup(&quot;'+escAttr(g.id)+'&quot;,&quot;'+escAttr(g.name)+'&quot;)" style="color:#ef4444;font-size:11px;padding:2px 8px">'+t('admin.delete')+'</button>'+
3633
- '</div>'+
3634
- '</div>'+
3635
- '<div id="groupMembers_'+escAttr(g.id)+'" style="display:none;margin-top:6px;font-size:12px"></div>'+
3636
- '</div>';
3637
- }
3638
- html+='</div>';
3912
+
3913
+ /* ─── Hub Admin Panel ─── */
3914
+ var adminDataCache={users:[],groups:[],tasks:[],skills:[],memories:[]};
3915
+ var ADMIN_PAGE_SIZE=20;
3916
+ var adminPage={users:0,tasks:0,skills:0,memories:0};
3917
+
3918
+ function adminPaginateHtml(total,page,refilterFn){
3919
+ var pages=Math.ceil(total/ADMIN_PAGE_SIZE);
3920
+ if(pages<=1) return '';
3921
+ var html='<div class="pagination" style="padding:16px 0">';
3922
+ html+='<button class="pg-btn'+(page===0?' disabled':'')+'" onclick="'+refilterFn+'Page(-1)">\\u2190</button>';
3923
+ var start=Math.max(0,page-2),end=Math.min(pages,page+3);
3924
+ if(start>0) html+='<button class="pg-btn" onclick="'+refilterFn+'Page(0)">1</button>'+(start>1?'<span class="pg-info">...</span>':'');
3925
+ for(var i=start;i<end;i++){
3926
+ html+='<button class="pg-btn'+(i===page?' active':'')+'" onclick="'+refilterFn+'Page('+i+')">'+(i+1)+'</button>';
3639
3927
  }
3640
- panel.innerHTML=html;
3641
- }
3642
- function showCreateGroupForm(){var f=document.getElementById('createGroupForm');if(f)f.style.display='block';}
3643
- function hideCreateGroupForm(){var f=document.getElementById('createGroupForm');if(f)f.style.display='none';}
3644
- async function createGroup(){
3645
- var name=(document.getElementById('newGroupName')).value.trim();
3646
- var desc=(document.getElementById('newGroupDesc')).value.trim();
3647
- if(!name){toast(t('toast.groupNameRequired'),'error');return;}
3648
- try{
3649
- var r=await fetch('/api/sharing/groups',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,description:desc})});
3650
- var d=await r.json();
3651
- if(d.ok){toast(t('toast.groupCreated'),'success');hideCreateGroupForm();loadGroupManager();}else{toast(d.error||t('toast.createFail'),'error');}
3652
- }catch(e){toast(t('toast.createFail')+': '+e.message,'error');}
3928
+ if(end<pages) html+=(end<pages-1?'<span class="pg-info">...</span>':'')+'<button class="pg-btn" onclick="'+refilterFn+'Page('+(pages-1)+')">'+pages+'</button>';
3929
+ html+='<button class="pg-btn'+(page>=pages-1?' disabled':'')+'" onclick="'+refilterFn+'Page('+(page+1)+')">\\u2192</button>';
3930
+ html+='<span class="pg-info">'+total+' '+t('pagination.total')+'</span>';
3931
+ html+='</div>';
3932
+ return html;
3653
3933
  }
3654
- async function deleteGroup(groupId,groupName){
3655
- if(!confirm(t('confirm.deleteGroup').replace('{name}',groupName))) return;
3656
- try{
3657
- var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId),{method:'DELETE'});
3658
- var d=await r.json();
3659
- if(d.ok){toast(t('toast.groupDeleted'),'success');loadGroupManager();}else{toast(d.error||t('toast.deleteFail'),'error');}
3660
- }catch(e){toast(t('toast.deleteFail')+': '+e.message,'error');}
3934
+
3935
+ var _adminPollTimer=null;
3936
+ function startAdminPoll(){
3937
+ if(_adminPollTimer) return;
3938
+ _adminPollTimer=setInterval(function(){pollAdminPending();},30000);
3939
+ pollAdminPending();
3661
3940
  }
3662
- async function toggleGroupMembers(groupId){
3663
- var el=document.getElementById('groupMembers_'+groupId);
3664
- if(!el) return;
3665
- if(el.style.display!=='none'){el.style.display='none';return;}
3666
- el.style.display='block';
3667
- el.innerHTML=t('sharing.loading');
3941
+ async function pollAdminPending(){
3668
3942
  try{
3669
- var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members');
3943
+ var r=await fetch('/api/sharing/pending-users');
3670
3944
  var d=await r.json();
3671
- var members=Array.isArray(d.members)?d.members:[];
3672
- renderGroupMembers(el,groupId,members);
3673
- }catch(e){el.innerHTML=t('admin.groupsFailed')+esc(String(e));}
3674
- }
3675
- function renderGroupMembers(el,groupId,members){
3676
- var html='<div style="font-size:12px;margin-bottom:6px;color:var(--text-sec)">'+t('admin.membersCount').replace('{n}',members.length)+'</div>';
3677
- if(members.length>0){
3678
- html+='<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">';
3679
- for(var i=0;i<members.length;i++){
3680
- var m=members[i];
3681
- html+='<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 10px;background:var(--bg-card);border:1px solid var(--border);border-radius:12px;font-size:12px">'+
3682
- esc(m.username||m.userId)+
3683
- ' <button style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:11px;padding:0 2px" onclick="removeGroupMember(&quot;'+escAttr(groupId)+'&quot;,&quot;'+escAttr(m.userId)+'&quot;)">&times;</button>'+
3684
- '</span>';
3685
- }
3686
- html+='</div>';
3687
- }else{
3688
- html+='<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">'+t('admin.noMembersYet')+'</div>';
3689
- }
3690
- var memberIds=new Set(members.map(function(m){return m.userId;}));
3691
- var available=groupManagerUsers.filter(function(u){return !memberIds.has(u.id);});
3692
- if(available.length>0){
3693
- html+='<div style="display:flex;gap:6px;align-items:center">'+
3694
- '<select id="addMemberSelect_'+escAttr(groupId)+'" style="padding:4px 8px;border:1px solid var(--border);border-radius:6px;font-size:12px">';
3695
- for(var j=0;j<available.length;j++){
3696
- html+='<option value="'+escAttr(available[j].id)+'">'+esc(available[j].username)+'</option>';
3945
+ var count=Array.isArray(d.users)?d.users.length:0;
3946
+ var badge=document.getElementById('adminPendingBadge');
3947
+ if(badge){
3948
+ if(count>0){badge.textContent=count;badge.style.display='inline';}
3949
+ else{badge.style.display='none';}
3697
3950
  }
3698
- html+='</select>'+
3699
- '<button class="btn btn-sm" onclick="addGroupMember(&quot;'+escAttr(groupId)+'&quot;)">'+t('admin.add')+'</button>'+
3700
- '</div>';
3701
- }
3702
- el.innerHTML=html;
3703
- }
3704
- async function addGroupMember(groupId){
3705
- var sel=document.getElementById('addMemberSelect_'+groupId);
3706
- if(!sel) return;
3707
- var userId=sel.value;
3708
- if(!userId) return;
3709
- try{
3710
- var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
3711
- var d=await r.json();
3712
- if(d.ok){toast(t('toast.memberAdded'),'success');reloadGroupMembers(groupId);}else{toast(d.error||t('toast.addFail'),'error');}
3713
- }catch(e){toast(t('toast.addFail')+': '+e.message,'error');}
3714
- }
3715
- async function removeGroupMember(groupId,userId){
3716
- if(!confirm(t('confirm.removeGroupMember'))) return;
3717
- try{
3718
- var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
3719
- var d=await r.json();
3720
- if(d.ok){toast(t('toast.memberRemoved'),'success');reloadGroupMembers(groupId);}else{toast(d.error||t('toast.removeFail'),'error');}
3721
- }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
3722
- }
3723
- async function reloadGroupMembers(groupId){
3724
- var el=document.getElementById('groupMembers_'+groupId);
3725
- if(!el) return;
3726
- el.style.display='block';
3727
- el.innerHTML=t('sharing.loading');
3728
- try{
3729
- var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members');
3730
- var d=await r.json();
3731
- var members=Array.isArray(d.members)?d.members:[];
3732
- renderGroupMembers(el,groupId,members);
3733
- }catch(e){el.innerHTML=t('admin.groupsFailed')+esc(String(e));}
3951
+ }catch(e){}
3734
3952
  }
3735
3953
 
3736
- /* ─── Hub Admin Panel ─── */
3737
- var adminDataCache={users:[],groups:[],tasks:[],skills:[],memories:[]};
3738
-
3739
3954
  function switchAdminTab(tab,btn){
3740
3955
  document.querySelectorAll('.admin-tabs .admin-tab').forEach(function(t){t.classList.remove('active');});
3741
3956
  btn.classList.add('active');
@@ -3787,16 +4002,18 @@ async function loadAdminData(){
3787
4002
  fetch('/api/admin/shared-memories').then(function(r){return r.json();})
3788
4003
  ]);
3789
4004
  adminDataCache.users=Array.isArray(usersR.users)?usersR.users:[];
3790
- adminDataCache.groups=[];
3791
4005
  adminDataCache.tasks=Array.isArray(tasksR.tasks)?tasksR.tasks:[];
3792
4006
  adminDataCache.skills=Array.isArray(skillsR.skills)?skillsR.skills:[];
3793
4007
  adminDataCache.memories=Array.isArray(memoriesR.memories)?memoriesR.memories:[];
3794
4008
  var pending=Array.isArray(pendingR.users)?pendingR.users:[];
4009
+ adminDataCache._pending=pending;
4010
+ var badge=document.getElementById('adminPendingBadge');
4011
+ if(badge){if(pending.length>0){badge.textContent=pending.length;badge.style.display='inline';}else{badge.style.display='none';}}
3795
4012
  renderAdminStats(pending.length);
3796
4013
  renderAdminUsers(adminDataCache.users, pending);
3797
- renderAdminMemories(adminDataCache.tasks);
4014
+ renderAdminTasks(adminDataCache.tasks);
3798
4015
  renderAdminSkills(adminDataCache.skills);
3799
- renderAdminSharedMemories(adminDataCache.memories);
4016
+ renderAdminMemories(adminDataCache.memories);
3800
4017
  }catch(e){
3801
4018
  var statsEl=document.getElementById('adminStats');
3802
4019
  if(statsEl) statsEl.innerHTML='<div class="admin-empty">'+t('admin.loadFailed')+esc(String(e))+'</div>';
@@ -3809,9 +4026,9 @@ function renderAdminStats(pendingCount){
3809
4026
  el.innerHTML=
3810
4027
  '<div class="admin-stat-box"><span class="as-icon">\u{1F465}</span><div class="val">'+adminDataCache.users.length+'</div><div class="lbl">'+t('admin.stat.activeUsers')+'</div></div>'+
3811
4028
  '<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>'+
4029
+ '<div class="admin-stat-box"><span class="as-icon">\u{1F4AD}</span><div class="val">'+(adminDataCache.memories||[]).length+'</div><div class="lbl">'+t('admin.stat.sharedMemories')+'</div></div>'+
3812
4030
  '<div class="admin-stat-box"><span class="as-icon">\u{1F4CB}</span><div class="val">'+adminDataCache.tasks.length+'</div><div class="lbl">'+t('admin.stat.sharedTasks')+'</div></div>'+
3813
- '<div class="admin-stat-box"><span class="as-icon">\u{1F9E0}</span><div class="val">'+adminDataCache.skills.length+'</div><div class="lbl">'+t('admin.stat.sharedSkills')+'</div></div>'+
3814
- '<div class="admin-stat-box"><span class="as-icon">\u{1F4AD}</span><div class="val">'+(adminDataCache.memories||[]).length+'</div><div class="lbl">'+t('admin.stat.sharedMemories')+'</div></div>';
4031
+ '<div class="admin-stat-box"><span class="as-icon">\u{1F9E0}</span><div class="val">'+adminDataCache.skills.length+'</div><div class="lbl">'+t('admin.stat.sharedSkills')+'</div></div>';
3815
4032
  var tc=document.getElementById('adminTabCountUsers');if(tc)tc.textContent=adminDataCache.users.length+pendingCount;
3816
4033
  tc=document.getElementById('adminTabCountMemories');if(tc)tc.textContent=(adminDataCache.memories||[]).length;
3817
4034
  tc=document.getElementById('adminTabCountTasks');if(tc)tc.textContent=adminDataCache.tasks.length;
@@ -3839,15 +4056,61 @@ function renderAdminUsers(users,pending){
3839
4056
  if(users.length===0){
3840
4057
  html+='<div class="admin-empty"><span class="ae-icon">\u{1F465}</span>'+t('admin.noActiveUsers')+'</div>';
3841
4058
  }else{
3842
- for(var i=0;i<users.length;i++){
4059
+ var totalUsers=users.length;
4060
+ var usersPages=Math.ceil(totalUsers/ADMIN_PAGE_SIZE);
4061
+ if(adminPage.users>=usersPages) adminPage.users=Math.max(0,usersPages-1);
4062
+ var usersStart=adminPage.users*ADMIN_PAGE_SIZE;
4063
+ var usersEnd=Math.min(usersStart+ADMIN_PAGE_SIZE,totalUsers);
4064
+ var adminCount=users.filter(function(x){return x.role==='admin';}).length;
4065
+ for(var i=usersStart;i<usersEnd;i++){
3843
4066
  var u=users[i];
3844
- html+='<div class="admin-card"><div class="admin-card-header"><div class="admin-card-title">'+esc(u.username||u.id)+'</div>'+
4067
+ var uid=escAttr(u.id);
4068
+ var uname=escAttr(u.username||'');
4069
+
4070
+ var titleDisplay='<span class="admin-card-title" id="au_name_'+uid+'">'+esc(u.username||u.id)+
4071
+ ' <button onclick="adminStartEditName(this,&quot;'+uid+'&quot;,&quot;'+uname+'&quot;)" style="background:none;border:none;cursor:pointer;color:var(--text-muted);padding:2px;font-size:13px;vertical-align:middle;opacity:.5;transition:opacity .15s" onmouseenter="this.style.opacity=1" onmouseleave="this.style.opacity=.5" title="'+t('admin.editName')+'">\u270E</button></span>';
4072
+
4073
+ var editRow='<div id="au_edit_'+uid+'" style="display:none;align-items:center;gap:6px">'+
4074
+ '<input id="au_input_'+uid+'" type="text" value="'+uname+'" style="flex:1;padding:5px 10px;border:1px solid var(--pri);border-radius:8px;font-size:13px;font-weight:600;background:var(--bg);color:var(--text);outline:none;min-width:0" onkeydown="if(event.key===&quot;Enter&quot;)adminSaveEditName(&quot;'+uid+'&quot;);if(event.key===&quot;Escape&quot;)adminCancelEditName(&quot;'+uid+'&quot;)">'+
4075
+ '<button onclick="adminSaveEditName(&quot;'+uid+'&quot;)" class="btn btn-sm btn-primary" style="padding:4px 12px;font-size:11px;white-space:nowrap">\u2713</button>'+
4076
+ '<button onclick="adminCancelEditName(&quot;'+uid+'&quot;)" class="btn btn-sm btn-ghost" style="padding:4px 8px;font-size:11px;color:var(--text-muted)">\u2717</button></div>';
4077
+
4078
+ var mc=u.memoryCount||0, tc=u.taskCount||0, sc=u.skillCount||0;
4079
+ var contribHtml='<div class="au-contrib">'+
4080
+ '<span class="au-contrib-item"><span class="au-contrib-num" style="color:var(--pri)">'+mc+'</span> '+t('admin.contrib.memories')+'</span>'+
4081
+ '<span class="au-contrib-item"><span class="au-contrib-num" style="color:var(--green)">'+tc+'</span> '+t('admin.contrib.tasks')+'</span>'+
4082
+ '<span class="au-contrib-item"><span class="au-contrib-num" style="color:var(--amber)">'+sc+'</span> '+t('admin.contrib.skills')+'</span>'+
4083
+ '</div>';
4084
+
4085
+ var infoRows=[];
4086
+ if(u.deviceName) infoRows.push('<span class="au-info-item">\u{1F4BB} '+t('admin.device')+esc(u.deviceName)+'</span>');
4087
+ if(u.lastIp) infoRows.push('<span class="au-info-item">\u{1F310} '+t('admin.ip')+esc(u.lastIp)+'</span>');
4088
+ if(u.createdAt) infoRows.push('<span class="au-info-item">\u{1F4C5} '+t('admin.joined')+formatDateTimeSeconds(u.createdAt)+'</span>');
4089
+ if(u.approvedAt) infoRows.push('<span class="au-info-item">\u2705 '+t('admin.approved')+formatDateTimeSeconds(u.approvedAt)+'</span>');
4090
+ infoRows.push('<span class="au-info-item">\u{1F552} '+t('admin.lastActive')+(u.lastActiveAt?formatDateTimeSeconds(u.lastActiveAt):t('admin.neverActive'))+'</span>');
4091
+ var infoHtml='<div class="au-info">'+infoRows.join('')+'</div>';
4092
+
4093
+ var actions='';
4094
+ if(u.role!=='admin'){
4095
+ actions+='<button class="btn btn-sm btn-ghost" onclick="adminToggleRole(&quot;'+uid+'&quot;,&quot;admin&quot;)" style="color:var(--accent)">'+t('admin.promoteAdmin')+'</button>';
4096
+ actions+='<button class="btn btn-sm btn-ghost" onclick="adminRemoveUser(&quot;'+uid+'&quot;,&quot;'+uname+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>';
4097
+ }else if(adminCount>1){
4098
+ actions+='<button class="btn btn-sm btn-ghost" onclick="adminToggleRole(&quot;'+uid+'&quot;,&quot;member&quot;)">'+t('admin.demoteMember')+'</button>';
4099
+ actions+='<button class="btn btn-sm btn-ghost" onclick="adminRemoveUser(&quot;'+uid+'&quot;,&quot;'+uname+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>';
4100
+ }else{
4101
+ actions+='<span style="font-size:11px;color:var(--text-muted);padding:4px 0">'+t('admin.lastAdminHint')+'</span>';
4102
+ }
4103
+ html+='<div class="admin-card au-card"><div class="admin-card-header"><div style="flex:1;min-width:0">'+titleDisplay+editRow+'</div>'+
3845
4104
  '<span class="admin-badge '+(u.role==='admin'?'admin':'member')+'">'+esc(u.role||'member')+'</span></div>'+
3846
- '<div class="admin-card-meta">ID: '+esc(u.id)+(u.status?' \u00B7 Status: '+esc(u.status):'')+'</div></div>';
4105
+ contribHtml+infoHtml+
4106
+ (actions?'<div class="admin-card-actions" style="border-top:1px dashed var(--border);padding-top:10px;margin-top:4px">'+actions+'</div>':'')+
4107
+ '</div>';
3847
4108
  }
4109
+ html+=adminPaginateHtml(totalUsers,adminPage.users,'adminUsers');
3848
4110
  }
3849
4111
  el.innerHTML=html;
3850
4112
  }
4113
+ function adminUsersPage(p){var maxP=Math.max(0,Math.ceil(adminDataCache.users.length/ADMIN_PAGE_SIZE)-1);if(p<0){adminPage.users=Math.max(0,adminPage.users-1);}else{adminPage.users=Math.min(p,maxP);}renderAdminUsers(adminDataCache.users,adminDataCache._pending||[]);}
3851
4114
 
3852
4115
  async function adminApproveUser(userId,username){
3853
4116
  try{
@@ -3857,140 +4120,151 @@ async function adminApproveUser(userId,username){
3857
4120
  }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
3858
4121
  }
3859
4122
  async function adminRejectUser(userId){
3860
- if(!confirm(t('confirm.rejectUser'))) return;
4123
+ if(!(await confirmModal(t('confirm.rejectUser'),{danger:true}))) return;
3861
4124
  try{
3862
4125
  var r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
3863
4126
  var d=await r.json();
3864
4127
  if(d.ok){toast(t('toast.userRejected'),'success');loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
3865
4128
  }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
3866
4129
  }
3867
-
3868
- function renderAdminGroups(groups){
3869
- var el=document.getElementById('adminGroupsPanel');
3870
- if(!el) return;
3871
- var html='<div style="margin-bottom:12px;display:flex;justify-content:space-between;align-items:center">'+
3872
- '<h3 style="font-size:14px;font-weight:600;color:var(--text)">'+t('admin.groups')+' ('+groups.length+')</h3>'+
3873
- '<button class="btn btn-sm btn-primary" onclick="showAdminCreateGroup()">'+t('admin.newGroup')+'</button></div>';
3874
- html+='<div id="adminCreateGroupForm" style="display:none;margin-bottom:14px;padding:14px;background:var(--bg);border:1px solid var(--border);border-radius:10px">'+
3875
- '<div style="display:flex;flex-direction:column;gap:8px">'+
3876
- '<input id="adminNewGroupName" type="text" placeholder="'+t('admin.groupName')+'" style="padding:8px 12px;border:1px solid var(--border);border-radius:8px;font-size:13px;background:var(--bg-card);color:var(--text)">'+
3877
- '<input id="adminNewGroupDesc" type="text" placeholder="'+t('admin.groupDesc')+'" style="padding:8px 12px;border:1px solid var(--border);border-radius:8px;font-size:13px;background:var(--bg-card);color:var(--text)">'+
3878
- '<div style="display:flex;gap:8px"><button class="btn btn-sm btn-primary" onclick="adminCreateGroup()">'+t('admin.create')+'</button>'+
3879
- '<button class="btn btn-sm btn-ghost" onclick="hideAdminCreateGroup()">'+t('admin.cancel')+'</button></div></div></div>';
3880
- if(groups.length===0){
3881
- html+='<div class="admin-empty"><span class="ae-icon">\u{1F4C2}</span>'+t('admin.noGroups')+'</div>';
3882
- }else{
3883
- for(var i=0;i<groups.length;i++){
3884
- var g=groups[i];
3885
- html+='<div class="admin-card"><div class="admin-card-header"><div class="admin-card-title">'+esc(g.name)+'</div>'+
3886
- '<button class="btn btn-sm btn-ghost" onclick="adminDeleteGroup(&quot;'+escAttr(g.id)+'&quot;,&quot;'+escAttr(g.name)+'&quot;)" style="color:var(--rose);font-size:11px">'+t('admin.delete')+'</button></div>'+
3887
- (g.description?'<div class="admin-card-meta">'+esc(g.description)+'</div>':'')+
3888
- '<div id="adminGroupMembers_'+escAttr(g.id)+'" style="margin-top:10px"></div>'+
3889
- '</div>';
3890
- }
3891
- }
3892
- el.innerHTML=html;
3893
- for(var i=0;i<groups.length;i++){adminLoadGroupMembers(groups[i].id);}
3894
- }
3895
- function showAdminCreateGroup(){var f=document.getElementById('adminCreateGroupForm');if(f)f.style.display='block';}
3896
- function hideAdminCreateGroup(){var f=document.getElementById('adminCreateGroupForm');if(f)f.style.display='none';}
3897
- async function adminCreateGroup(){
3898
- var name=(document.getElementById('adminNewGroupName')).value.trim();
3899
- var desc=(document.getElementById('adminNewGroupDesc')).value.trim();
3900
- if(!name){toast(t('toast.groupNameRequired'),'error');return;}
4130
+ async function adminToggleRole(userId,newRole){
4131
+ var msg=newRole==='admin'?t('confirm.promoteAdmin'):t('confirm.demoteMember');
4132
+ if(!(await confirmModal(msg))) return;
3901
4133
  try{
3902
- var r=await fetch('/api/sharing/groups',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,description:desc})});
4134
+ var r=await fetch('/api/sharing/change-role',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,role:newRole})});
3903
4135
  var d=await r.json();
3904
- if(d.ok||d.id){toast(t('toast.groupCreated'),'success');hideAdminCreateGroup();loadAdminData();}else{toast(d.error||t('toast.createFail'),'error');}
3905
- }catch(e){toast(t('toast.createFail')+': '+e.message,'error');}
3906
- }
3907
- async function adminDeleteGroup(groupId,groupName){
3908
- if(!confirm(t('confirm.deleteGroupShort').replace('{name}',groupName))) return;
4136
+ if(d.ok){toast(t('toast.roleChanged'),'success');loadAdminData();}else{toast(d.error||t('toast.roleChangeFail'),'error');}
4137
+ }catch(e){toast(t('toast.roleChangeFail')+': '+e.message,'error');}
4138
+ }
4139
+ function adminStartEditName(btn,userId,currentName){
4140
+ var nameEl=document.getElementById('au_name_'+userId);
4141
+ var editEl=document.getElementById('au_edit_'+userId);
4142
+ var inputEl=document.getElementById('au_input_'+userId);
4143
+ if(!nameEl||!editEl||!inputEl) return;
4144
+ nameEl.style.display='none';
4145
+ editEl.style.display='flex';
4146
+ inputEl.value=currentName;
4147
+ inputEl.focus();
4148
+ inputEl.select();
4149
+ }
4150
+ function adminCancelEditName(userId){
4151
+ var nameEl=document.getElementById('au_name_'+userId);
4152
+ var editEl=document.getElementById('au_edit_'+userId);
4153
+ if(nameEl) nameEl.style.display='';
4154
+ if(editEl) editEl.style.display='none';
4155
+ }
4156
+ async function adminSaveEditName(userId){
4157
+ var inputEl=document.getElementById('au_input_'+userId);
4158
+ if(!inputEl) return;
4159
+ var newName=inputEl.value.trim();
4160
+ if(!newName||newName.length<2||newName.length>32){toast(t('toast.invalidUsername'),'warn');return;}
4161
+ inputEl.disabled=true;
3909
4162
  try{
3910
- var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId),{method:'DELETE'});
4163
+ var r=await fetch('/api/sharing/rename-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:newName})});
3911
4164
  var d=await r.json();
3912
- if(d.ok){toast(t('toast.groupDeleted'),'success');loadAdminData();}else{toast(d.error||t('toast.deleteFail'),'error');}
3913
- }catch(e){toast(t('toast.deleteFail')+': '+e.message,'error');}
4165
+ if(d.ok){toast(t('toast.usernameChanged'),'success');loadAdminData();}else{inputEl.disabled=false;toast(d.error||t('toast.renameFail'),'error');}
4166
+ }catch(e){inputEl.disabled=false;toast(t('toast.renameFail')+': '+e.message,'error');}
3914
4167
  }
3915
- async function adminLoadGroupMembers(groupId){
3916
- var el=document.getElementById('adminGroupMembers_'+groupId);
3917
- if(!el) return;
3918
- el.innerHTML=t('sharing.loading');
3919
- try{
3920
- var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members');
3921
- var d=await r.json();
3922
- var members=Array.isArray(d.members)?d.members:[];
3923
- var html='<div style="font-size:12px;margin-bottom:6px;color:var(--text-sec)">'+t('admin.membersCount').replace('{n}',members.length)+'</div>';
3924
- if(members.length>0){
3925
- html+='<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">';
3926
- for(var i=0;i<members.length;i++){
3927
- var m=members[i];
3928
- html+='<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 10px;background:var(--bg-card);border:1px solid var(--border);border-radius:12px;font-size:12px">'+
3929
- esc(m.username||m.userId)+
3930
- ' <button style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:11px;padding:0 2px" onclick="adminRemoveGroupMember(&quot;'+escAttr(groupId)+'&quot;,&quot;'+escAttr(m.userId)+'&quot;)">&times;</button></span>';
3931
- }
3932
- html+='</div>';
3933
- }else{
3934
- html+='<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">'+t('admin.noMembers')+'</div>';
3935
- }
3936
- var memberIds=new Set(members.map(function(m){return m.userId;}));
3937
- var available=adminDataCache.users.filter(function(u){return !memberIds.has(u.id);});
3938
- if(available.length>0){
3939
- html+='<div style="display:flex;gap:6px;align-items:center">'+
3940
- '<select id="adminAddMember_'+escAttr(groupId)+'" style="padding:4px 8px;border:1px solid var(--border);border-radius:6px;font-size:12px;background:var(--bg-card);color:var(--text)">';
3941
- for(var j=0;j<available.length;j++){
3942
- html+='<option value="'+escAttr(available[j].id)+'">'+esc(available[j].username)+'</option>';
3943
- }
3944
- html+='</select><button class="btn btn-sm" onclick="adminAddGroupMember(&quot;'+escAttr(groupId)+'&quot;)">'+t('admin.add')+'</button></div>';
3945
- }
3946
- el.innerHTML=html;
3947
- }catch(e){el.innerHTML=t('admin.groupsFailed')+esc(String(e));}
3948
- }
3949
- async function adminAddGroupMember(groupId){
3950
- var sel=document.getElementById('adminAddMember_'+groupId);
3951
- if(!sel) return;
3952
- try{
3953
- var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:sel.value})});
3954
- var d=await r.json();
3955
- if(d.ok){toast(t('toast.memberAdded'),'success');adminLoadGroupMembers(groupId);}else{toast(d.error||t('toast.addFail'),'error');}
3956
- }catch(e){toast(t('toast.addFail')+': '+e.message,'error');}
3957
- }
3958
- async function adminRemoveGroupMember(groupId,userId){
3959
- if(!confirm(t('confirm.removeMember'))) return;
4168
+
4169
+ async function adminRemoveUser(userId,username){
4170
+ if(!(await confirmModal(t('confirm.removeUser'),{danger:true}))) return;
4171
+ var clean=await confirmModal(t('confirm.cleanResources'));
3960
4172
  try{
3961
- var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
4173
+ var r=await fetch('/api/sharing/remove-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,cleanResources:clean})});
3962
4174
  var d=await r.json();
3963
- if(d.ok){toast(t('toast.memberRemoved'),'success');adminLoadGroupMembers(groupId);}else{toast(d.error||t('toast.removeFail'),'error');}
4175
+ if(d.ok){toast(t('toast.userRemoved'),'success');loadAdminData();}else{toast(d.error||t('toast.removeFail'),'error');}
3964
4176
  }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
3965
4177
  }
3966
4178
 
3967
- function renderAdminMemories(tasks){
3968
- var el=document.getElementById('adminMemoriesPanel');
4179
+
4180
+ function adminToolbarHtml(items,keyFn,filterId,searchId,onchangeFn,opts){
4181
+ opts=opts||{};
4182
+ var owners={};
4183
+ for(var i=0;i<items.length;i++){var n=keyFn(items[i]);if(n)owners[n]=1;}
4184
+ var names=Object.keys(owners).sort();
4185
+ var html='';
4186
+ if(names.length>=1){
4187
+ html+='<select id="'+filterId+'" onchange="'+onchangeFn+'()" class="admin-toolbar select"><option value="">'+t('admin.filter.owner')+'</option>';
4188
+ for(var j=0;j<names.length;j++) html+='<option value="'+escAttr(names[j])+'">'+esc(names[j])+'</option>';
4189
+ html+='</select>';
4190
+ }
4191
+ if(opts.sortId){
4192
+ html+='<select id="'+opts.sortId+'" onchange="'+onchangeFn+'()" class="admin-toolbar select">'+
4193
+ '<option value="newest">'+t('admin.sort.newest')+'</option>'+
4194
+ '<option value="oldest">'+t('admin.sort.oldest')+'</option></select>';
4195
+ }
4196
+ if(opts.extraFilters){
4197
+ for(var ef=0;ef<opts.extraFilters.length;ef++){
4198
+ var f=opts.extraFilters[ef];
4199
+ var vals={};
4200
+ for(var fi=0;fi<items.length;fi++){var fv=f.keyFn(items[fi]);if(fv)vals[fv]=1;}
4201
+ var fkeys=Object.keys(vals).sort();
4202
+ if(fkeys.length>=1){
4203
+ html+='<select id="'+f.id+'" onchange="'+onchangeFn+'()" class="admin-toolbar select"><option value="">'+esc(f.label)+'</option>';
4204
+ for(var fk=0;fk<fkeys.length;fk++) html+='<option value="'+escAttr(fkeys[fk])+'">'+esc(fkeys[fk])+'</option>';
4205
+ html+='</select>';
4206
+ }
4207
+ }
4208
+ }
4209
+ return html;
4210
+ }
4211
+ function renderAdminTasks(tasks){
4212
+ var el=document.getElementById('adminTasksPanel');
3969
4213
  if(!el) return;
3970
4214
  adminTasksCache=tasks;
3971
- var html='<h3 style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:10px">'+t('admin.sharedTasks')+' ('+tasks.length+')</h3>';
3972
- if(tasks.length===0){
4215
+ var filterVal=document.getElementById('adminTaskFilter')?document.getElementById('adminTaskFilter').value:'';
4216
+ var sortVal=document.getElementById('adminTaskSort')?document.getElementById('adminTaskSort').value:'newest';
4217
+ var statusVal=document.getElementById('adminTaskStatusFilter')?document.getElementById('adminTaskStatusFilter').value:'';
4218
+ var filtered=tasks;
4219
+ if(filterVal) filtered=filtered.filter(function(tk){return (tk.ownerName||tk.sourceUserId||'')===filterVal;});
4220
+ if(statusVal) filtered=filtered.filter(function(tk){return (tk.status||'')===statusVal;});
4221
+ filtered=filtered.slice().sort(function(a,b){var ta=a.updatedAt||a.createdAt||0,tb=b.updatedAt||b.createdAt||0;return sortVal==='oldest'?ta-tb:tb-ta;});
4222
+ var html='<div class="admin-toolbar"><h3>'+t('admin.sharedTasks')+' ('+filtered.length+'/'+tasks.length+')</h3>'+
4223
+ adminToolbarHtml(tasks,function(tk){return tk.ownerName||tk.sourceUserId||'';},'adminTaskFilter','adminTaskSearch','refilterAdminTasks',{sortId:'adminTaskSort',extraFilters:[
4224
+ {id:'adminTaskStatusFilter',label:t('admin.filter.status'),keyFn:function(tk){return tk.status||''}}
4225
+ ]})+'</div>';
4226
+ if(filtered.length===0){
3973
4227
  html+='<div class="admin-empty"><span class="ae-icon">\u{1F4CB}</span>'+t('admin.noSharedTasks')+'</div>';
3974
4228
  }else{
3975
- for(var i=0;i<tasks.length;i++){
3976
- var tk=tasks[i];
3977
- html+='<div class="admin-card" onclick="openHubTaskDetailFromCache(\\\'admin\\\','+i+')" style="cursor:pointer"><div class="admin-card-header"><div class="admin-card-title">'+esc(tk.title||tk.id)+'</div>'+
4229
+ var tPages=Math.ceil(filtered.length/ADMIN_PAGE_SIZE);
4230
+ if(adminPage.tasks>=tPages) adminPage.tasks=Math.max(0,tPages-1);
4231
+ var tStart=adminPage.tasks*ADMIN_PAGE_SIZE,tEnd=Math.min(tStart+ADMIN_PAGE_SIZE,filtered.length);
4232
+ for(var i=tStart;i<tEnd;i++){
4233
+ var tk=filtered[i];
4234
+ var cardId='adminTaskCard_'+i;
4235
+ var timeRange='';
4236
+ if(tk.startedAt) timeRange+=formatTime(tk.startedAt);
4237
+ if(tk.endedAt) timeRange+=' \u2192 '+formatTime(tk.endedAt);
4238
+ html+='<div class="admin-card admin-card-clickable" id="'+cardId+'" onclick="toggleAdminTaskCard(&quot;'+cardId+'&quot;,'+i+')">'+
4239
+ '<div class="admin-card-header"><div class="admin-card-title">'+esc(tk.title||tk.id)+'</div></div>'+
4240
+ '<div class="admin-card-tags">'+
4241
+ '<div class="admin-card-tags-left">'+
4242
+ '<span class="admin-card-tag tag-owner">\u{1F464} '+esc(tk.ownerName||tk.sourceUserId||'unknown')+'</span>'+
4243
+ (tk.status?'<span class="admin-card-tag tag-status">'+esc(tk.status)+'</span>':'')+
4244
+ (tk.chunkCount!=null?'<span class="admin-card-tag tag-kind">\u{1F4DD} '+tk.chunkCount+' '+t('admin.chunks')+'</span>':'')+
3978
4245
  '</div>'+
3979
- '<div class="admin-card-meta">'+
3980
- t('admin.owner')+esc(tk.ownerName||tk.sourceUserId||'unknown')+
3981
- (tk.chunkCount!=null?' \u00B7 '+t('admin.chunks')+tk.chunkCount:'')+
3982
- ' \u00B7 '+t('admin.updated')+new Date(tk.updatedAt||tk.createdAt).toLocaleDateString()+
4246
+ '<span class="admin-card-actions" onclick="event.stopPropagation()">'+
4247
+ '<button class="btn btn-sm btn-ghost" onclick="adminDeleteTask(&quot;'+escAttr(tk.id)+'&quot;,&quot;'+escAttr(tk.title||tk.id)+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>'+
4248
+ '<button class="btn btn-sm btn-ghost admin-card-expand-btn" onclick="toggleAdminTaskCard(&quot;'+cardId+'&quot;,'+i+')">'+t('admin.expand')+'</button>'+
4249
+ '<span class="admin-card-time">'+(timeRange||formatDateTimeSeconds(tk.updatedAt||tk.createdAt))+'</span>'+
4250
+ '</span>'+
3983
4251
  '</div>'+
3984
- '<div class="admin-card-actions">'+
3985
- '<button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();adminDeleteTask(&quot;'+escAttr(tk.id)+'&quot;,&quot;'+escAttr(tk.title||tk.id)+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>'+
3986
- '</div></div>';
4252
+ (tk.summary?'<div class="admin-card-preview">'+esc(tk.summary.slice(0,120))+'</div>':'')+
4253
+ '<div class="admin-card-detail" id="'+cardId+'_detail" onclick="event.stopPropagation()"></div>'+
4254
+ '</div>';
3987
4255
  }
4256
+ html+=adminPaginateHtml(filtered.length,adminPage.tasks,'adminTasks');
3988
4257
  }
3989
4258
  el.innerHTML=html;
4259
+ if(filterVal){var sel=document.getElementById('adminTaskFilter');if(sel)sel.value=filterVal;}
4260
+ if(sortVal!=='newest'){var so=document.getElementById('adminTaskSort');if(so)so.value=sortVal;}
4261
+ if(statusVal){var sf=document.getElementById('adminTaskStatusFilter');if(sf)sf.value=statusVal;}
3990
4262
  }
4263
+ function refilterAdminTasks(){adminPage.tasks=0;renderAdminTasks(adminTasksCache||[]);}
4264
+ function adminTasksPage(p){if(p<0){adminPage.tasks=Math.max(0,adminPage.tasks-1);}else{adminPage.tasks=p;}renderAdminTasks(adminTasksCache||[]);}
3991
4265
 
3992
4266
  async function adminDeleteTask(taskId,taskTitle){
3993
- if(!confirm(t('confirm.removeTask').replace('{name}',taskTitle))) return;
4267
+ if(!(await confirmModal(t('confirm.removeTask').replace('{name}',taskTitle),{danger:true}))) return;
3994
4268
  try{
3995
4269
  var r=await fetch('/api/admin/shared-tasks/'+encodeURIComponent(taskId),{method:'DELETE'});
3996
4270
  var d=await r.json();
@@ -4002,30 +4276,59 @@ function renderAdminSkills(skills){
4002
4276
  var el=document.getElementById('adminSkillsPanel');
4003
4277
  if(!el) return;
4004
4278
  adminSkillsCache=skills;
4005
- var html='<h3 style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:10px">'+t('admin.sharedSkills')+' ('+skills.length+')</h3>';
4006
- if(skills.length===0){
4279
+ var filterVal=document.getElementById('adminSkillFilter')?document.getElementById('adminSkillFilter').value:'';
4280
+ var sortVal=document.getElementById('adminSkillSort')?document.getElementById('adminSkillSort').value:'newest';
4281
+ var statusVal=document.getElementById('adminSkillStatusFilter')?document.getElementById('adminSkillStatusFilter').value:'';
4282
+ var filtered=skills;
4283
+ if(filterVal) filtered=filtered.filter(function(s){return (s.ownerName||s.sourceUserId||'')===filterVal;});
4284
+ if(statusVal) filtered=filtered.filter(function(s){return (s.status||'')===statusVal;});
4285
+ filtered=filtered.slice().sort(function(a,b){var ta=a.updatedAt||a.createdAt||0,tb=b.updatedAt||b.createdAt||0;return sortVal==='oldest'?ta-tb:tb-ta;});
4286
+ var html='<div class="admin-toolbar"><h3>'+t('admin.sharedSkills')+' ('+filtered.length+'/'+skills.length+')</h3>'+
4287
+ adminToolbarHtml(skills,function(s){return s.ownerName||s.sourceUserId||'';},'adminSkillFilter','adminSkillSearch','refilterAdminSkills',{sortId:'adminSkillSort',extraFilters:[
4288
+ {id:'adminSkillStatusFilter',label:t('admin.filter.status'),keyFn:function(s){return s.status||''}}
4289
+ ]})+'</div>';
4290
+ if(filtered.length===0){
4007
4291
  html+='<div class="admin-empty"><span class="ae-icon">\u{1F9E0}</span>'+t('admin.noSharedSkills')+'</div>';
4008
4292
  }else{
4009
- for(var i=0;i<skills.length;i++){
4010
- var s=skills[i];
4011
- html+='<div class="admin-card" onclick="openHubSkillDetailFromCache(\\\'admin\\\','+i+')" style="cursor:pointer"><div class="admin-card-header"><div class="admin-card-title">'+esc(s.name||s.id)+'</div>'+
4293
+ var sPages=Math.ceil(filtered.length/ADMIN_PAGE_SIZE);
4294
+ if(adminPage.skills>=sPages) adminPage.skills=Math.max(0,sPages-1);
4295
+ var sStart=adminPage.skills*ADMIN_PAGE_SIZE,sEnd=Math.min(sStart+ADMIN_PAGE_SIZE,filtered.length);
4296
+ for(var i=sStart;i<sEnd;i++){
4297
+ var s=filtered[i];
4298
+ var cardId='adminSkillCard_'+i;
4299
+ var qs=s.qualityScore;
4300
+ html+='<div class="admin-card admin-card-clickable" id="'+cardId+'" onclick="toggleAdminSkillCard(&quot;'+cardId+'&quot;,'+i+')">'+
4301
+ '<div class="admin-card-header"><div class="admin-card-title">'+esc(s.name||s.id)+'</div></div>'+
4302
+ '<div class="admin-card-tags">'+
4303
+ '<div class="admin-card-tags-left">'+
4304
+ '<span class="admin-card-tag tag-owner">\u{1F464} '+esc(s.ownerName||s.sourceUserId||'unknown')+'</span>'+
4305
+ (s.status?'<span class="admin-card-tag tag-status">'+esc(s.status)+'</span>':'')+
4306
+ (s.version!=null?'<span class="admin-card-tag tag-version">v'+s.version+'</span>':'')+
4307
+ (qs!=null?'<span class="admin-card-tag tag-kind">\u2605 '+Number(qs).toFixed(1)+'</span>':'')+
4012
4308
  '</div>'+
4013
- '<div class="admin-card-meta">'+
4014
- (s.description?esc(s.description)+'<br>':'')+
4015
- t('admin.owner')+esc(s.ownerName||s.sourceUserId||'unknown')+
4016
- (s.version!=null?' \u00B7 '+t('admin.version')+s.version:'')+
4017
- (s.qualityScore!=null?' \u00B7 '+t('admin.quality')+s.qualityScore:'')+
4309
+ '<span class="admin-card-actions" onclick="event.stopPropagation()">'+
4310
+ '<button class="btn btn-sm btn-ghost" onclick="adminDeleteSkill(&quot;'+escAttr(s.id)+'&quot;,&quot;'+escAttr(s.name||s.id)+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>'+
4311
+ '<button class="btn btn-sm btn-ghost admin-card-expand-btn" onclick="toggleAdminSkillCard(&quot;'+cardId+'&quot;,'+i+')">'+t('admin.expand')+'</button>'+
4312
+ (s.sourceSkillId?'<button class="btn btn-sm btn-ghost" onclick="window.open(&quot;/api/skill/'+encodeURIComponent(s.sourceSkillId)+'/download&quot;,&quot;_blank&quot;)" style="color:var(--pri)">\u2B07</button>':'')+
4313
+ '<span class="admin-card-time">'+formatDateTimeSeconds(s.updatedAt||s.createdAt)+'</span>'+
4314
+ '</span>'+
4018
4315
  '</div>'+
4019
- '<div class="admin-card-actions">'+
4020
- '<button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();adminDeleteSkill(&quot;'+escAttr(s.id)+'&quot;,&quot;'+escAttr(s.name||s.id)+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>'+
4021
- '</div></div>';
4316
+ (s.description?'<div class="admin-card-preview">'+esc(s.description.slice(0,150))+'</div>':'')+
4317
+ '<div class="admin-card-detail" id="'+cardId+'_detail" onclick="event.stopPropagation()"></div>'+
4318
+ '</div>';
4022
4319
  }
4320
+ html+=adminPaginateHtml(filtered.length,adminPage.skills,'adminSkills');
4023
4321
  }
4024
4322
  el.innerHTML=html;
4323
+ if(filterVal){var sel=document.getElementById('adminSkillFilter');if(sel)sel.value=filterVal;}
4324
+ if(sortVal!=='newest'){var so=document.getElementById('adminSkillSort');if(so)so.value=sortVal;}
4325
+ if(statusVal){var sf=document.getElementById('adminSkillStatusFilter');if(sf)sf.value=statusVal;}
4025
4326
  }
4327
+ function refilterAdminSkills(){adminPage.skills=0;renderAdminSkills(adminSkillsCache||[]);}
4328
+ function adminSkillsPage(p){if(p<0){adminPage.skills=Math.max(0,adminPage.skills-1);}else{adminPage.skills=p;}renderAdminSkills(adminSkillsCache||[]);}
4026
4329
 
4027
4330
  async function adminDeleteSkill(skillId,skillName){
4028
- if(!confirm(t('confirm.removeSkill').replace('{name}',skillName))) return;
4331
+ if(!(await confirmModal(t('confirm.removeSkill').replace('{name}',skillName),{danger:true}))) return;
4029
4332
  try{
4030
4333
  var r=await fetch('/api/admin/shared-skills/'+encodeURIComponent(skillId),{method:'DELETE'});
4031
4334
  var d=await r.json();
@@ -4033,34 +4336,242 @@ async function adminDeleteSkill(skillId,skillName){
4033
4336
  }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
4034
4337
  }
4035
4338
 
4036
- function renderAdminSharedMemories(memories){
4037
- var el=document.getElementById('adminSharedMemoriesPanel');
4339
+ function renderAdminMemories(memories){
4340
+ var el=document.getElementById('adminMemoriesPanel');
4038
4341
  if(!el) return;
4039
4342
  adminMemoriesCache=memories||[];
4040
- var html='<h3 style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:10px">'+t('admin.sharedMemories')+' ('+(memories||[]).length+')</h3>';
4041
- if(!memories||memories.length===0){
4343
+ var all=memories||[];
4344
+ var filterVal=document.getElementById('adminMemoryFilter')?document.getElementById('adminMemoryFilter').value:'';
4345
+ var sortVal=document.getElementById('adminMemorySort')?document.getElementById('adminMemorySort').value:'newest';
4346
+ var roleVal=document.getElementById('adminMemoryRoleFilter')?document.getElementById('adminMemoryRoleFilter').value:'';
4347
+ var filtered=all;
4348
+ if(filterVal) filtered=filtered.filter(function(m){return (m.ownerName||m.sourceUserId||'')===filterVal;});
4349
+ if(roleVal) filtered=filtered.filter(function(m){return (m.role||'')===roleVal;});
4350
+ filtered=filtered.slice().sort(function(a,b){var ta=a.updatedAt||a.createdAt||0,tb=b.updatedAt||b.createdAt||0;return sortVal==='oldest'?ta-tb:tb-ta;});
4351
+ var html='<div class="admin-toolbar"><h3>'+t('admin.sharedMemories')+' ('+filtered.length+'/'+all.length+')</h3>'+
4352
+ adminToolbarHtml(all,function(m){return m.ownerName||m.sourceUserId||'';},'adminMemoryFilter','adminMemorySearch','refilterAdminMemories',{sortId:'adminMemorySort',extraFilters:[
4353
+ {id:'adminMemoryRoleFilter',label:t('admin.filter.role'),keyFn:function(m){return m.role||''}}
4354
+ ]})+'</div>';
4355
+ if(filtered.length===0){
4042
4356
  html+='<div class="admin-empty"><span class="ae-icon">\u{1F4AD}</span>'+t('admin.noSharedMemories')+'</div>';
4043
4357
  }else{
4044
- for(var i=0;i<memories.length;i++){
4045
- var m=memories[i];
4046
- html+='<div class="admin-card" onclick="openHubMemoryDetail(\\\'admin\\\','+i+')" style="cursor:pointer"><div class="admin-card-header"><div class="admin-card-title">'+esc(m.summary||m.content?.slice(0,80)||m.id)+'</div>'+
4047
- '</div>'+
4048
- '<div class="admin-card-meta">'+
4049
- t('admin.owner')+esc(m.ownerName||m.sourceUserId||'unknown')+
4050
- (m.kind?' \u00B7 Kind: '+esc(m.kind):'')+
4051
- (m.role?' \u00B7 Role: '+esc(m.role):'')+
4052
- ' \u00B7 '+t('admin.updated')+new Date(m.updatedAt||m.createdAt).toLocaleDateString()+
4358
+ var mPages=Math.ceil(filtered.length/ADMIN_PAGE_SIZE);
4359
+ if(adminPage.memories>=mPages) adminPage.memories=Math.max(0,mPages-1);
4360
+ var mStart=adminPage.memories*ADMIN_PAGE_SIZE,mEnd=Math.min(mStart+ADMIN_PAGE_SIZE,filtered.length);
4361
+ for(var i=mStart;i<mEnd;i++){
4362
+ var m=filtered[i];
4363
+ var cardId='adminMemCard_'+i;
4364
+ var preview=m.content?esc(m.content.slice(0,120)):'';
4365
+ html+='<div class="admin-card" id="'+cardId+'">'+
4366
+ '<div class="admin-card-header"><div class="admin-card-title">'+esc(m.summary||m.content?.slice(0,80)||m.id)+'</div></div>'+
4367
+ '<div class="admin-card-tags">'+
4368
+ '<div class="admin-card-tags-left">'+
4369
+ '<span class="admin-card-tag tag-owner">\u{1F464} '+esc(m.ownerName||m.sourceUserId||'unknown')+'</span>'+
4370
+ (m.role?'<span class="admin-card-tag tag-role">'+esc(m.role)+'</span>':'')+
4371
+ (m.kind?'<span class="admin-card-tag tag-kind">'+esc(m.kind)+'</span>':'')+
4053
4372
  '</div>'+
4054
- '<div class="admin-card-actions">'+
4373
+ '<span class="admin-card-actions">'+
4055
4374
  '<button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();adminDeleteMemory(&quot;'+escAttr(m.id)+'&quot;,&quot;'+escAttr(m.summary||m.id)+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>'+
4056
- '</div></div>';
4375
+ '<button class="btn btn-sm btn-ghost admin-card-expand-btn" onclick="event.stopPropagation();toggleAdminMemoryCard(&quot;'+cardId+'&quot;,'+i+')">'+t('admin.expand')+'</button>'+
4376
+ '<span class="admin-card-time">'+formatDateTimeSeconds(m.updatedAt||m.createdAt)+'</span>'+
4377
+ '</span>'+
4378
+ '</div>'+
4379
+ (preview?'<div class="admin-card-preview">'+preview+'</div>':'')+
4380
+ '<div class="admin-card-detail" id="'+cardId+'_detail"></div>'+
4381
+ '</div>';
4057
4382
  }
4383
+ html+=adminPaginateHtml(filtered.length,adminPage.memories,'adminMemories');
4058
4384
  }
4059
4385
  el.innerHTML=html;
4386
+ if(filterVal){var sel=document.getElementById('adminMemoryFilter');if(sel)sel.value=filterVal;}
4387
+ if(sortVal!=='newest'){var so=document.getElementById('adminMemorySort');if(so)so.value=sortVal;}
4388
+ if(roleVal){var rf=document.getElementById('adminMemoryRoleFilter');if(rf)rf.value=roleVal;}
4389
+ }
4390
+ function refilterAdminMemories(){adminPage.memories=0;renderAdminMemories(adminMemoriesCache||[]);}
4391
+ function adminMemoriesPage(p){if(p<0){adminPage.memories=Math.max(0,adminPage.memories-1);}else{adminPage.memories=p;}renderAdminMemories(adminMemoriesCache||[]);}
4392
+
4393
+ function toggleAdminMemoryCard(cardId,idx){
4394
+ var card=document.getElementById(cardId);
4395
+ if(!card) return;
4396
+ var detail=document.getElementById(cardId+'_detail');
4397
+ var btn=card.querySelector('.admin-card-expand-btn');
4398
+ if(card.classList.contains('expanded')){
4399
+ card.classList.remove('expanded');
4400
+ if(btn) btn.textContent=t('admin.expand');
4401
+ return;
4402
+ }
4403
+ card.classList.add('expanded');
4404
+ if(btn) btn.textContent=t('admin.collapse');
4405
+ if(detail.getAttribute('data-loaded')) return;
4406
+ detail.setAttribute('data-loaded','1');
4407
+ var m=(adminMemoriesCache||[])[idx];
4408
+ if(!m){detail.innerHTML='<div style="color:var(--text-muted);font-size:13px">'+t('admin.noContent')+'</div>';return;}
4409
+ var metaHtml='<div class="admin-card-detail-meta">'+
4410
+ (m.kind?'<span class="meta-item">'+t('admin.kind')+esc(m.kind)+'</span>':'')+
4411
+ (m.role?'<span class="meta-item">'+t('admin.role')+esc(m.role)+'</span>':'')+
4412
+ (m.visibility?'<span class="meta-item">'+t('admin.visibility')+esc(m.visibility)+'</span>':'')+
4413
+ '<span class="meta-item">'+t('admin.owner')+esc(m.ownerName||m.sourceUserId||'unknown')+'</span>'+
4414
+ (m.groupName?'<span class="meta-item">'+t('admin.group')+esc(m.groupName)+'</span>':'')+
4415
+ '<span class="meta-item">'+new Date(m.updatedAt||m.createdAt||0).toLocaleString(dateLoc())+'</span>'+
4416
+ '</div>';
4417
+ var summaryHtml=m.summary?'<div class="admin-card-detail-section"><div class="detail-label">'+t('admin.summary')+'</div><div style="font-size:13px;color:var(--text);line-height:1.6">'+esc(m.summary)+'</div></div>':'';
4418
+ var contentHtml='<div class="admin-card-detail-section"><div class="detail-label">'+t('admin.contentLabel')+'</div><div class="admin-card-detail-content" id="'+cardId+'_content">'+
4419
+ (m.content?esc(m.content):t('sharing.loading'))+'</div></div>';
4420
+ detail.innerHTML=metaHtml+summaryHtml+contentHtml;
4421
+ if(!m.content&&(m.remoteHitId||m.id)){
4422
+ fetch('/api/sharing/memory-detail',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({remoteHitId:m.remoteHitId||m.id})})
4423
+ .then(function(r){return r.json();}).then(function(d){
4424
+ var ce=document.getElementById(cardId+'_content');
4425
+ if(!ce) return;
4426
+ if(!d.error&&d.content){ce.textContent=d.content;}
4427
+ else{ce.textContent=t('admin.noContent');}
4428
+ }).catch(function(){
4429
+ var ce=document.getElementById(cardId+'_content');
4430
+ if(ce) ce.textContent=t('admin.noContent');
4431
+ });
4432
+ }
4433
+ }
4434
+
4435
+ async function toggleAdminTaskCard(cardId,idx){
4436
+ var card=document.getElementById(cardId);
4437
+ if(!card) return;
4438
+ var detail=document.getElementById(cardId+'_detail');
4439
+ var btn=card.querySelector('.admin-card-expand-btn');
4440
+ if(card.classList.contains('expanded')){
4441
+ card.classList.remove('expanded');
4442
+ if(btn) btn.textContent=t('admin.expand');
4443
+ return;
4444
+ }
4445
+ card.classList.add('expanded');
4446
+ if(btn) btn.textContent=t('admin.collapse');
4447
+ if(detail.getAttribute('data-loaded')) return;
4448
+ detail.setAttribute('data-loaded','1');
4449
+ var tk=(adminTasksCache||[])[idx];
4450
+ if(!tk){detail.innerHTML='<div style="color:var(--text-muted);font-size:14px;padding:12px">'+t('admin.noContent')+'</div>';return;}
4451
+ var localTaskId=tk.sourceTaskId||tk.id;
4452
+ detail.innerHTML='<div class="spinner"></div>';
4453
+ var task=null;
4454
+ try{
4455
+ var r=await fetch('/api/task/'+encodeURIComponent(localTaskId));
4456
+ if(r.ok) task=await r.json();
4457
+ }catch(e){}
4458
+ if(!task){detail.innerHTML='<div style="color:var(--text-muted);font-size:14px;padding:12px">'+t('admin.noContent')+'</div>';return;}
4459
+ var metaHtml='<div class="admin-card-detail-meta admin-task-meta">'+
4460
+ (tk.status?'<span class="meta-item"><span class="task-status-badge '+tk.status+'">'+esc(tk.status)+'</span></span>':'')+
4461
+ (tk.visibility?'<span class="meta-item">'+t('admin.visibility')+esc(tk.visibility)+'</span>':'')+
4462
+ '<span class="meta-item">'+t('admin.owner')+esc(tk.ownerName||'unknown')+'</span>'+
4463
+ (tk.groupName?'<span class="meta-item">'+t('admin.group')+esc(tk.groupName)+'</span>':'')+
4464
+ (task.chunks&&task.chunks.length?'<span class="meta-item">\u{1F4AC} '+task.chunks.length+' '+t('tasks.chunks.label')+'</span>':'')+
4465
+ (task.startedAt?'<span class="meta-item">\u{1F4C5} '+formatDateTimeSeconds(task.startedAt)+'</span>':'')+
4466
+ (task.endedAt?'<span class="meta-item">\u2192 '+formatDateTimeSeconds(task.endedAt)+'</span>':'')+
4467
+ '<span class="meta-item">'+t('admin.updated')+new Date(tk.updatedAt||tk.createdAt||0).toLocaleString(dateLoc())+'</span>'+
4468
+ '</div>';
4469
+ var summaryHtml=(task.summary||tk.summary)?
4470
+ '<div class="admin-card-detail-section admin-task-summary"><div class="detail-label">'+t('admin.summary')+'</div><div class="admin-task-summary-body">'+renderSummaryHtml(task.summary||tk.summary)+'</div></div>':
4471
+ '<div class="admin-card-detail-section"><div style="color:var(--text-muted);font-size:14px">'+t('admin.noContent')+'</div></div>';
4472
+ var chunksHtml='';
4473
+ if(task.chunks&&task.chunks.length>0){
4474
+ var safeId=cardId.replace(/[^a-zA-Z0-9_-]/g,'_');
4475
+ chunksHtml='<div class="admin-card-detail-section"><div class="detail-label">'+t('tasks.chunks')+' ('+task.chunks.length+')</div><div class="admin-task-chunks">'+
4476
+ task.chunks.map(function(c,i){
4477
+ var role=c.role||'assistant';
4478
+ var roleLabel=role==='user'?t('tasks.role.user'):role==='assistant'?t('tasks.role.assistant'):role.toUpperCase();
4479
+ var bid='admchunk_b_'+safeId+'_'+i;
4480
+ var eid='admchunk_e_'+safeId+'_'+i;
4481
+ return '<div class="adm-msg">'+
4482
+ '<div class="adm-msg-side '+role+'"><div class="adm-msg-role">'+roleLabel+'</div><div class="adm-msg-time">'+formatDateTimeSeconds(c.createdAt)+'</div></div>'+
4483
+ '<div style="flex:1;min-width:0"><div class="adm-msg-body collapsed" id="'+bid+'">'+esc(c.content||'')+'</div>'+
4484
+ '<div class="adm-msg-toggle" id="'+eid+'" onclick="event.stopPropagation();toggleAdminChunkExpand(&quot;'+cardId.replace(/"/g,'&amp;quot;')+'&quot;,'+i+')">'+t('tasks.expand')+'</div></div></div>';
4485
+ }).join('')+'</div></div>';
4486
+ }else{
4487
+ chunksHtml='<div class="admin-card-detail-section"><div class="detail-label">'+t('tasks.chunks')+'</div><div style="color:var(--text-muted);font-size:13px;padding:8px 0">'+t('tasks.nochunks')+'</div></div>';
4488
+ }
4489
+ detail.innerHTML=metaHtml+summaryHtml+chunksHtml;
4490
+ if(task.chunks&&task.chunks.length>0) setTimeout(function(){initAdminChunkExpanders(cardId,task.chunks.length)},50);
4491
+ }
4492
+ function toggleAdminChunkExpand(cardId,i){
4493
+ var safeId=cardId.replace(/[^a-zA-Z0-9_-]/g,'_');
4494
+ var b=document.getElementById('admchunk_b_'+safeId+'_'+i);
4495
+ var e=document.getElementById('admchunk_e_'+safeId+'_'+i);
4496
+ if(!b||!e)return;
4497
+ if(b.classList.contains('collapsed')){b.classList.remove('collapsed');e.textContent=t('tasks.collapse');}
4498
+ else{b.classList.add('collapsed');e.textContent=t('tasks.expand');}
4499
+ }
4500
+ function initAdminChunkExpanders(cardId,count){
4501
+ var safeId=cardId.replace(/[^a-zA-Z0-9_-]/g,'_');
4502
+ for(var i=0;i<count;i++){
4503
+ var b=document.getElementById('admchunk_b_'+safeId+'_'+i);
4504
+ var e=document.getElementById('admchunk_e_'+safeId+'_'+i);
4505
+ if(b&&b.scrollHeight>b.clientHeight+4&&e)e.style.display='block';
4506
+ else if(b)b.classList.remove('collapsed');
4507
+ }
4508
+ }
4509
+
4510
+ async function toggleAdminSkillCard(cardId,idx){
4511
+ var card=document.getElementById(cardId);
4512
+ if(!card) return;
4513
+ var detail=document.getElementById(cardId+'_detail');
4514
+ var btn=card.querySelector('.admin-card-expand-btn');
4515
+ if(card.classList.contains('expanded')){
4516
+ card.classList.remove('expanded');
4517
+ if(btn) btn.textContent=t('admin.expand');
4518
+ return;
4519
+ }
4520
+ card.classList.add('expanded');
4521
+ if(btn) btn.textContent=t('admin.collapse');
4522
+ if(detail.getAttribute('data-loaded')) return;
4523
+ detail.setAttribute('data-loaded','1');
4524
+ var sk=(adminSkillsCache||[])[idx];
4525
+ if(!sk){detail.innerHTML='<div style="color:var(--text-muted);font-size:13px">'+t('admin.noContent')+'</div>';return;}
4526
+ detail.innerHTML='<div class="spinner"></div>';
4527
+ var localSkillId=sk.sourceSkillId||sk.id;
4528
+ var localData=null;
4529
+ try{
4530
+ var lr=await fetch('/api/skill/'+encodeURIComponent(localSkillId));
4531
+ if(lr.ok) localData=await lr.json();
4532
+ }catch(e){}
4533
+ var localSkill=localData&&localData.skill?localData.skill:sk;
4534
+ var files=localData&&localData.files?localData.files:[];
4535
+ var versions=localData&&localData.versions?localData.versions:[];
4536
+ var qs=localSkill.qualityScore!=null?localSkill.qualityScore:sk.qualityScore;
4537
+ var metaHtml='<div class="admin-card-detail-meta">'+
4538
+ (localSkill.version!=null?'<span class="meta-item"><span class="skill-badge version">v'+localSkill.version+'</span></span>':'')+
4539
+ (localSkill.status?'<span class="meta-item"><span class="skill-badge status-'+localSkill.status+'">'+esc(localSkill.status)+'</span></span>':'')+
4540
+ (sk.visibility?'<span class="meta-item">'+t('admin.visibility')+esc(sk.visibility||'hub')+'</span>':'')+
4541
+ (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>':'')+
4542
+ '<span class="meta-item">'+t('admin.owner')+esc(sk.ownerName||'unknown')+'</span>'+
4543
+ (sk.groupName?'<span class="meta-item">'+t('admin.group')+esc(sk.groupName)+'</span>':'')+
4544
+ '<span class="meta-item">'+t('admin.updated')+new Date(sk.updatedAt||sk.createdAt||0).toLocaleString(dateLoc())+'</span>'+
4545
+ '</div>';
4546
+ var descHtml=(localSkill.description||sk.description)?'<div class="admin-card-detail-section"><div class="detail-label">'+t('admin.description')+'</div><div style="font-size:13px;color:var(--text);line-height:1.6">'+esc(localSkill.description||sk.description)+'</div></div>':'';
4547
+ var filesHtml='';
4548
+ if(files.length>0){
4549
+ var fileIcons={'skill':'\u{1F4D6}','script':'\u{2699}','reference':'\u{1F4CE}','file':'\u{1F4C4}'};
4550
+ filesHtml='<div class="admin-card-detail-section"><div class="detail-label" style="display:flex;align-items:center;justify-content:space-between">'+t('skills.files')+
4551
+ '<button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();window.open(\\x27/api/skill/'+encodeURIComponent(localSkillId)+'/download\\x27,\\x27_blank\\x27)" style="font-size:11px">\u2B07 '+t('skills.download')+'</button>'+
4552
+ '</div><div class="skill-files-list">'+
4553
+ files.map(function(f){return '<div class="skill-file-item">'+
4554
+ '<span class="skill-file-icon">'+(fileIcons[f.type]||'\u{1F4C4}')+'</span>'+
4555
+ '<span class="skill-file-name">'+esc(f.path)+'</span>'+
4556
+ '<span class="skill-file-type">'+f.type+'</span>'+
4557
+ '<span class="skill-file-size">'+(f.size>1024?(f.size/1024).toFixed(1)+'KB':f.size+'B')+'</span>'+
4558
+ '</div>';}).join('')+
4559
+ '</div></div>';
4560
+ }
4561
+ var contentHtml='';
4562
+ if(versions.length>0&&versions[0].content){
4563
+ contentHtml='<div class="admin-card-detail-section"><div class="detail-label">SKILL.md (v'+versions[0].version+')</div><div class="admin-card-detail-content">'+renderSkillMarkdown(versions[0].content)+'</div></div>';
4564
+ }else if(localSkill.content||sk.content){
4565
+ contentHtml='<div class="admin-card-detail-section"><div class="detail-label">'+t('admin.contentLabel')+'</div><div class="admin-card-detail-content"><pre style="margin:0;white-space:pre-wrap">'+esc(localSkill.content||sk.content)+'</pre></div></div>';
4566
+ }
4567
+ detail.innerHTML=metaHtml+descHtml+filesHtml+contentHtml;
4568
+ if(!descHtml&&!filesHtml&&!contentHtml){
4569
+ detail.innerHTML+=('<div style="color:var(--text-muted);font-size:13px;padding:8px 0">'+t('admin.noContent')+'</div>');
4570
+ }
4060
4571
  }
4061
4572
 
4062
4573
  async function adminDeleteMemory(memoryId,memoryTitle){
4063
- if(!confirm(t('confirm.removeMemory').replace('{name}',memoryTitle))) return;
4574
+ if(!(await confirmModal(t('confirm.removeMemory').replace('{name}',memoryTitle),{danger:true}))) return;
4064
4575
  try{
4065
4576
  var r=await fetch('/api/admin/shared-memories/'+encodeURIComponent(memoryId),{method:'DELETE'});
4066
4577
  var d=await r.json();
@@ -4073,7 +4584,7 @@ function renderSharingMemorySearchResults(data,query){
4073
4584
  const localHits=(data&&data.local&&Array.isArray(data.local.hits))?data.local.hits:[];
4074
4585
  const hubHits=(data&&data.hub&&Array.isArray(data.hub.hits))?data.hub.hits:[];
4075
4586
  document.getElementById('searchMeta').textContent='Search results for "'+query+'"';
4076
- document.getElementById('sharingSearchMeta').textContent='Local '+localHits.length+' · Hub '+hubHits.length;
4587
+ document.getElementById('sharingSearchMeta').textContent=t('scope.local')+' '+localHits.length+' · '+t('scope.hub')+' '+hubHits.length;
4077
4588
  document.getElementById('pagination').innerHTML='';
4078
4589
  list.innerHTML=''+
4079
4590
  '<div class="result-section">'+
@@ -4113,7 +4624,7 @@ async function openSharedMemoryDetail(remoteHitId,title,owner,groupName){
4113
4624
  currentSharedMemoryHitId=remoteHitId;
4114
4625
  document.getElementById('sharedMemoryOverlay').classList.add('show');
4115
4626
  document.getElementById('sharedMemoryTitle').textContent=title||t('search.sharedMemory');
4116
- document.getElementById('sharedMemoryMeta').innerHTML='<span class="meta-item">Hub</span>'+(owner?'<span class="meta-item">'+t('admin.owner')+esc(owner)+'</span>':'')+(groupName?'<span class="meta-item">'+t('admin.group')+esc(groupName)+'</span>':'');
4627
+ document.getElementById('sharedMemoryMeta').innerHTML='<span class="meta-item">'+t('scope.hub')+'</span>'+(owner?'<span class="meta-item">'+t('admin.owner')+esc(owner)+'</span>':'')+(groupName?'<span class="meta-item">'+t('admin.group')+esc(groupName)+'</span>':'');
4117
4628
  document.getElementById('sharedMemorySummary').textContent=t('sharing.loading');
4118
4629
  document.getElementById('sharedMemoryContent').textContent='';
4119
4630
  try{
@@ -4139,18 +4650,19 @@ async function openHubMemoryDetail(cacheKey,idx){
4139
4650
  if(!m) return;
4140
4651
  var overlay=document.getElementById('sharedMemoryOverlay');
4141
4652
  overlay.classList.add('show');
4142
- document.getElementById('sharedMemoryTitle').textContent=m.summary||m.content?.slice(0,80)||'(no summary)';
4143
- var metaHtml='<span class="meta-item">\\u{1F310} Hub</span>'+
4653
+ var titleText=m.summary||m.content?.slice(0,80)||'(no summary)';
4654
+ document.getElementById('sharedMemoryTitle').textContent=titleText;
4655
+ var metaHtml='<span class="meta-item">\\u{1F310} '+t('scope.hub')+'</span>'+
4144
4656
  (m.ownerName?'<span class="meta-item">'+t('admin.owner')+esc(m.ownerName)+'</span>':'')+
4145
4657
  (m.groupName?'<span class="meta-item">'+t('admin.group')+esc(m.groupName)+'</span>':'')+
4146
- (m.kind?'<span class="meta-item">Kind: '+esc(m.kind)+'</span>':'')+
4147
- (m.role?'<span class="meta-item">Role: '+esc(m.role)+'</span>':'')+
4148
- '<span class="meta-item">visibility: '+esc(m.visibility||'hub')+'</span>'+
4658
+ (m.kind?'<span class="meta-item">'+t('admin.kind')+esc(m.kind)+'</span>':'')+
4659
+ (m.role?'<span class="meta-item">'+t('admin.role')+esc(m.role)+'</span>':'')+
4660
+ (m.visibility?'<span class="meta-item">'+t('admin.visibility')+esc(m.visibility)+'</span>':'')+
4149
4661
  '<span class="meta-item">'+new Date(m.updatedAt||m.createdAt||0).toLocaleString(dateLoc())+'</span>';
4150
4662
  document.getElementById('sharedMemoryMeta').innerHTML=metaHtml;
4151
4663
  document.getElementById('sharedMemorySummary').textContent=m.summary||'';
4152
- document.getElementById('sharedMemoryContent').textContent=m.content||t('sharing.loading');
4153
- // try to fetch full content from Hub API
4664
+ var hasContent=m.content&&m.content.length>0;
4665
+ document.getElementById('sharedMemoryContent').textContent=hasContent?m.content:t('sharing.loading');
4154
4666
  var remoteId=m.remoteHitId||m.id;
4155
4667
  if(remoteId){
4156
4668
  try{
@@ -4159,8 +4671,14 @@ async function openHubMemoryDetail(cacheKey,idx){
4159
4671
  if(!d.error&&(d.content||d.summary)){
4160
4672
  if(d.summary) document.getElementById('sharedMemorySummary').textContent=d.summary;
4161
4673
  document.getElementById('sharedMemoryContent').textContent=d.content||m.content||'';
4674
+ }else if(!hasContent){
4675
+ document.getElementById('sharedMemoryContent').textContent=m.content||t('memory.detail.notFound');
4162
4676
  }
4163
- }catch(e){}
4677
+ }catch(e){
4678
+ if(!hasContent) document.getElementById('sharedMemoryContent').textContent=m.content||t('memory.detail.notFound');
4679
+ }
4680
+ }else if(!hasContent){
4681
+ document.getElementById('sharedMemoryContent').textContent=t('memory.detail.notFound');
4164
4682
  }
4165
4683
  }
4166
4684
 
@@ -4173,11 +4691,11 @@ function openHubTaskDetailFromCache(cacheKey,idx){
4173
4691
  document.getElementById('taskDetailTitle').textContent=task.title||'(no title)';
4174
4692
  document.getElementById('taskShareActions').innerHTML='';
4175
4693
  var meta=[
4176
- '<span class="meta-item">\\u{1F310} Hub</span>',
4694
+ '<span class="meta-item">\\u{1F310} '+t('scope.hub')+'</span>',
4177
4695
  task.status?'<span class="meta-item"><span class="task-status-badge '+task.status+'">'+esc(task.status)+'</span></span>':'',
4178
4696
  '<span class="meta-item">'+t('admin.owner')+esc(task.ownerName||'unknown')+'</span>',
4179
4697
  task.groupName?'<span class="meta-item">'+t('admin.group')+esc(task.groupName)+'</span>':'',
4180
- '<span class="meta-item">visibility: '+esc(task.visibility||'hub')+'</span>',
4698
+ task.visibility?'<span class="meta-item">'+t('admin.visibility')+esc(task.visibility)+'</span>':'',
4181
4699
  task.chunkCount!=null?'<span class="meta-item">\\u{1F4DD} '+esc(String(task.chunkCount))+' '+t('tasks.chunks.label')+'</span>':'',
4182
4700
  task.startedAt?'<span class="meta-item">\\u{1F4C5} '+formatTime(task.startedAt)+'</span>':'',
4183
4701
  task.endedAt?'<span class="meta-item">\\u2192 '+formatTime(task.endedAt)+'</span>':'',
@@ -4201,7 +4719,7 @@ function openHubSkillDetailFromCache(cacheKey,idx){
4201
4719
  var qs=skill.qualityScore;
4202
4720
  var qsBadge=(qs!==null&&qs!==undefined)?'<span class="meta-item"><span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+(+qs).toFixed(1)+'/10</span></span>':'';
4203
4721
  var meta=[
4204
- '<span class="meta-item">\\u{1F310} Hub</span>',
4722
+ '<span class="meta-item">\\u{1F310} '+t('scope.hub')+'</span>',
4205
4723
  skill.version!=null?'<span class="meta-item"><span class="skill-badge version">v'+skill.version+'</span></span>':'',
4206
4724
  skill.status?'<span class="meta-item"><span class="skill-badge status-'+skill.status+'">'+esc(skill.status)+'</span></span>':'',
4207
4725
  '<span class="meta-item">visibility: '+esc(skill.visibility||'hub')+'</span>',
@@ -4220,26 +4738,163 @@ function openHubSkillDetailFromCache(cacheKey,idx){
4220
4738
  if(visBtn) visBtn.style.display='none';
4221
4739
  var dlBtn=document.getElementById('skillDownloadBtn');
4222
4740
  if(dlBtn) dlBtn.style.display='none';
4223
- var shareBtn=document.getElementById('skillShareActions');
4224
- if(shareBtn) shareBtn.innerHTML='';
4741
+ var scopeBadge=document.getElementById('skillScopeBadge');
4742
+ if(scopeBadge) scopeBadge.innerHTML='';
4225
4743
  }
4226
4744
 
4227
4745
  function escAttr(s){return String(s||'').replace(/&/g,'&amp;').replace(/'/g,'&#39;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
4228
4746
 
4747
+ /* ─── Unified Sharing Scope Selector ─── */
4748
+
4749
+ function getScopeLabel(scope){
4750
+ if(scope==='team') return t('share.scope.team');
4751
+ if(scope==='local') return t('share.scope.local');
4752
+ return t('share.scope.private');
4753
+ }
4754
+ function getScopeIcon(scope){
4755
+ if(scope==='team') return '\\u{1F310}';
4756
+ if(scope==='local') return '\\u{1F465}';
4757
+ return '\\u{1F512}';
4758
+ }
4759
+ function getScopeColor(scope){
4760
+ if(scope==='team') return '#22c55e';
4761
+ if(scope==='local') return '#3b82f6';
4762
+ return '#f59e0b';
4763
+ }
4764
+
4765
+ function renderScopeBadge(scope){
4766
+ var color=getScopeColor(scope);
4767
+ var icon=getScopeIcon(scope);
4768
+ var label=getScopeLabel(scope);
4769
+ if(scope==='private') return '<span style="display:inline-flex;align-items:center;gap:3px;padding:2px 8px;background:'+color+'14;border:1px solid '+color+'33;border-radius:10px;font-size:11px;color:'+color+'">'+icon+' '+label+'</span>';
4770
+ return '<span style="display:inline-flex;align-items:center;gap:3px;padding:2px 8px;background:'+color+'18;border:1px solid '+color+'44;border-radius:10px;font-size:11px;color:'+color+'">'+icon+' '+label+'</span>';
4771
+ }
4772
+
4773
+ function openScopeSelectorModal(resourceType, resourceId, currentScope, onConfirm){
4774
+ var existing=document.getElementById('scopeSelectorOverlay');
4775
+ if(existing) existing.remove();
4776
+ var teamEnabled=sharingStatusCache&&sharingStatusCache.enabled;
4777
+ var overlay=document.createElement('div');
4778
+ overlay.id='scopeSelectorOverlay';
4779
+ 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';
4780
+ var scopeDescs={private:'',local:'',team:t('share.scope.teamIncludes')};
4781
+ var scopes=['private','local','team'];
4782
+ var h='<div style="background:var(--bg-card);border:1px solid var(--border-glow);border-radius:12px;padding:0;width:340px;box-shadow:0 16px 48px rgba(0,0,0,0.35);overflow:hidden">';
4783
+ h+='<div style="padding:14px 18px 10px;border-bottom:1px solid var(--border)">';
4784
+ h+='<div style="font-size:14px;font-weight:600;color:var(--text)">'+t('share.scope.title')+'</div>';
4785
+ h+='</div>';
4786
+ h+='<div style="padding:6px 8px">';
4787
+ for(var i=0;i<scopes.length;i++){
4788
+ var sc=scopes[i];
4789
+ var isCurrent=sc===currentScope;
4790
+ var isDisabled=sc==='team'&&!teamEnabled;
4791
+ var color=getScopeColor(sc);
4792
+ var cursor=isDisabled?'not-allowed':'pointer';
4793
+ var opacity=isDisabled?'0.4':'1';
4794
+ var selBg=isCurrent?color+'16':'transparent';
4795
+ var selBorder=isCurrent?'2px solid '+color+'55':'2px solid transparent';
4796
+ h+='<div class="scope-option'+(isCurrent?' selected':'')+'" data-scope="'+sc+'" data-color="'+color+'" style="display:flex;align-items:center;gap:12px;padding:10px 12px;margin:3px 0;border:'+selBorder+';border-radius:10px;background:'+selBg+';cursor:'+cursor+';opacity:'+opacity+';transition:all 0.12s ease"';
4797
+ if(!isDisabled) h+=' onclick="selectScopeOption(this,\\''+sc+'\\')"';
4798
+ h+='>';
4799
+ h+='<div style="width:34px;height:34px;border-radius:9px;background:'+color+'22;display:flex;align-items:center;justify-content:center;font-size:17px;flex-shrink:0">'+getScopeIcon(sc)+'</div>';
4800
+ h+='<div style="flex:1;min-width:0">';
4801
+ h+='<div style="font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px">'+getScopeLabel(sc);
4802
+ if(isCurrent) h+='<span style="font-size:10px;font-weight:600;color:#fff;background:'+color+';padding:1px 7px;border-radius:4px">'+t('share.scope.current')+'</span>';
4803
+ h+='</div>';
4804
+ var desc=isDisabled?t('share.scope.teamDisabled'):(scopeDescs[sc]||'');
4805
+ if(desc) h+='<div style="font-size:11px;color:var(--text-sec);margin-top:2px;line-height:1.3">'+desc+'</div>';
4806
+ h+='</div></div>';
4807
+ }
4808
+ h+='</div>';
4809
+ h+='<div style="padding:8px 14px 12px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end">';
4810
+ h+='<button class="btn btn-sm btn-ghost" onclick="closeScopeSelectorModal()" style="font-size:13px">'+t('share.scope.cancel')+'</button>';
4811
+ h+='<button class="btn btn-sm" id="scopeConfirmBtn" onclick="confirmScopeSelection()" disabled style="font-size:13px;min-width:56px">'+t('share.scope.confirm')+'</button>';
4812
+ h+='</div></div>';
4813
+ overlay.innerHTML=h;
4814
+ overlay.addEventListener('click',function(e){if(e.target===overlay)closeScopeSelectorModal();});
4815
+ document.body.appendChild(overlay);
4816
+ window._scopeSelectionState={resourceType:resourceType,resourceId:resourceId,currentScope:currentScope,selectedScope:currentScope,onConfirm:onConfirm};
4817
+ }
4818
+ function selectScopeOption(el,scope){
4819
+ if(!window._scopeSelectionState)return;
4820
+ var overlay=document.getElementById('scopeSelectorOverlay');
4821
+ if(!overlay)return;
4822
+ var color=getScopeColor(scope);
4823
+ var options=overlay.querySelectorAll('.scope-option');
4824
+ options.forEach(function(opt){opt.classList.remove('selected');opt.style.border='2px solid transparent';opt.style.background='transparent';});
4825
+ el.classList.add('selected');
4826
+ el.style.border='2px solid '+color+'55';
4827
+ el.style.background=color+'16';
4828
+ window._scopeSelectionState.selectedScope=scope;
4829
+ document.getElementById('scopeConfirmBtn').disabled=(scope===window._scopeSelectionState.currentScope);
4830
+ }
4831
+ function closeScopeSelectorModal(){
4832
+ var overlay=document.getElementById('scopeSelectorOverlay');
4833
+ if(overlay) overlay.remove();
4834
+ window._scopeSelectionState=null;
4835
+ }
4836
+ async function confirmScopeSelection(){
4837
+ if(!window._scopeSelectionState)return;
4838
+ var st=Object.assign({},window._scopeSelectionState);
4839
+ var newScope=st.selectedScope;
4840
+ var oldScope=st.currentScope;
4841
+ if(newScope===oldScope){closeScopeSelectorModal();return;}
4842
+ closeScopeSelectorModal();
4843
+ var shrinking=(oldScope==='team'&&newScope!=='team')||(oldScope==='local'&&newScope==='private');
4844
+ if(shrinking){
4845
+ var msg=oldScope==='team'&&newScope==='local'?t('share.scope.shrinkToLocal'):
4846
+ oldScope==='team'&&newScope==='private'?t('share.scope.shrinkToPrivate'):
4847
+ t('share.scope.shrinkLocalToPrivate');
4848
+ if(!(await confirmModal(msg,{danger:true})))return;
4849
+ }
4850
+ try{
4851
+ var url='/api/'+st.resourceType+'/'+st.resourceId+'/scope';
4852
+ var r=await fetch(url,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({scope:newScope})});
4853
+ var d=await r.json();
4854
+ if(d.ok){
4855
+ toast(t('share.scope.changed'),'success');
4856
+ if(st.onConfirm) st.onConfirm(newScope);
4857
+ else loadAll();
4858
+ }else{
4859
+ toast(d.error||t('share.scope.changeFail'),'error');
4860
+ }
4861
+ }catch(e){toast(t('share.scope.changeFail')+': '+e.message,'error');}
4862
+ }
4863
+
4229
4864
  function renderTaskShareActions(task){
4230
4865
  currentTaskDetail=task||null;
4231
4866
  const el=document.getElementById('taskShareActions');
4232
4867
  if(!el){return;}
4233
4868
  if(!task||!task.id){el.innerHTML='';return;}
4234
- const current=(task.sharingVisibility||task.visibility||null);
4235
- const isShared=!!current;
4236
- var statusHtml='';
4237
- if(isShared){
4238
- statusHtml='<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 10px;background:#22c55e22;border:1px solid #22c55e44;border-radius:12px;font-size:12px;color:#22c55e">\u2713 '+t('share.alreadyShared')+'</span>';
4869
+ var isLocalShared=task.owner==='public';
4870
+ var isTeamShared=!!(task.sharingVisibility||task.hubTaskId);
4871
+ var currentScope=isTeamShared?'team':isLocalShared?'local':'private';
4872
+ if(task.status==='completed'){
4873
+ el.innerHTML=renderScopeBadge(currentScope)+
4874
+ '<button class="btn btn-sm btn-ghost" onclick="openTaskScopeModal()">\u270F '+t('share.shareBtn')+'</button>';
4875
+ }else{
4876
+ el.innerHTML=renderScopeBadge(currentScope)+
4877
+ '<button class="btn btn-sm btn-ghost" style="opacity:0.45;cursor:not-allowed" onclick="toast(t(\\x27share.scope.taskNotCompleted\\x27),\\x27warn\\x27)">\u270F '+t('share.shareBtn')+'</button>';
4239
4878
  }
4240
- el.innerHTML=statusHtml+
4241
- '<button class="btn btn-sm" onclick="shareCurrentTask()">'+(isShared?t('share.updateBtn'):t('share.shareBtn'))+'</button>'+
4242
- (isShared?'<button class="btn btn-sm btn-ghost" onclick="unshareCurrentTask()">'+t('share.unshareBtn')+'</button>':'');
4879
+ }
4880
+ function openTaskScopeModal(){
4881
+ if(!currentTaskDetail) return;
4882
+ var task=currentTaskDetail;
4883
+ var isLocalShared=task.owner==='public';
4884
+ var isTeamShared=!!(task.sharingVisibility||task.hubTaskId);
4885
+ var cs=isTeamShared?'team':isLocalShared?'local':'private';
4886
+ openScopeSelectorModal('task',task.id,cs,function(s){
4887
+ if(s==='team'){task.sharingVisibility='public';task.hubTaskId=task.hubTaskId||'shared';}
4888
+ else if(s==='local'){task.sharingVisibility=null;task.owner='public';}
4889
+ else{task.sharingVisibility=null;task.owner=task._origOwner||'agent:main';}
4890
+ renderTaskShareActions(task);
4891
+ updateTaskCardBadge(task.id,s);
4892
+ });
4893
+ }
4894
+ function openTaskScopeModalFromList(taskId,currentScope){
4895
+ openScopeSelectorModal('task',taskId,currentScope,function(s){
4896
+ updateTaskCardBadge(taskId,s);
4897
+ });
4243
4898
  }
4244
4899
 
4245
4900
  async function shareCurrentTask(){
@@ -4262,18 +4917,34 @@ async function unshareCurrentTask(){
4262
4917
  }
4263
4918
 
4264
4919
  function renderSkillShareActions(skill){
4265
- const el=document.getElementById('skillShareActions');
4920
+ var el=document.getElementById('skillScopeBadge');
4266
4921
  if(!el){return;}
4267
4922
  if(!skill||!skill.id){el.innerHTML='';return;}
4268
- const current=(skill.sharingVisibility||null);
4269
- const isShared=!!current;
4270
- var statusHtml='';
4271
- if(isShared){
4272
- statusHtml='<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 10px;background:#22c55e22;border:1px solid #22c55e44;border-radius:12px;font-size:12px;color:#22c55e">\u2713 '+t('share.alreadyShared')+'</span>';
4273
- }
4274
- el.innerHTML=statusHtml+
4275
- '<button class="btn btn-sm" onclick="shareCurrentSkill()">'+(isShared?t('share.updateBtn'):t('share.shareBtn'))+'</button>'+
4276
- (isShared?'<button class="btn btn-sm btn-ghost" onclick="unshareCurrentSkill()">'+t('share.unshareBtn')+'</button>':'');
4923
+ var isLocalShared=skill.visibility==='public';
4924
+ var isTeamShared=!!(skill.sharingVisibility);
4925
+ var currentScope=isTeamShared?'team':isLocalShared?'local':'private';
4926
+ el.innerHTML=renderScopeBadge(currentScope);
4927
+ }
4928
+ function openSkillScopeModal(){
4929
+ if(!currentSkillDetail) return;
4930
+ var skill=currentSkillDetail;
4931
+ var isLocalShared=skill.visibility==='public';
4932
+ var isTeamShared=!!skill.sharingVisibility;
4933
+ var cs=isTeamShared?'team':isLocalShared?'local':'private';
4934
+ openScopeSelectorModal('skill',skill.id,cs,function(s){
4935
+ if(s==='team'){skill.sharingVisibility='public';}
4936
+ else if(s==='local'){skill.sharingVisibility=null;skill.visibility='public';}
4937
+ else{skill.sharingVisibility=null;skill.visibility='private';}
4938
+ renderSkillShareActions(skill);
4939
+ var scopeBadgeEl=document.getElementById('skillScopeBadge');
4940
+ if(scopeBadgeEl) scopeBadgeEl.innerHTML=renderScopeBadge(s);
4941
+ updateSkillCardBadge(skill.id,s);
4942
+ });
4943
+ }
4944
+ function openSkillScopeModalFromList(skillId,currentScope){
4945
+ openScopeSelectorModal('skill',skillId,currentScope,function(s){
4946
+ updateSkillCardBadge(skillId,s);
4947
+ });
4277
4948
  }
4278
4949
 
4279
4950
  async function shareCurrentSkill(){
@@ -4299,18 +4970,34 @@ async function shareMemoryPrompt(chunkId){
4299
4970
  try{
4300
4971
  const r=await fetch('/api/sharing/memories/share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({chunkId:chunkId,visibility:'public'})});
4301
4972
  const d=await r.json();
4302
- if(d.ok){toast(t('toast.memoryShared'),'success');loadMemories();} else {toast(d.error||t('toast.memoryShareFail'),'error');}
4973
+ if(d.ok){
4974
+ toast(t('toast.memoryShared'),'success');
4975
+ if(memoryCache[chunkId]){memoryCache[chunkId].sharingVisibility='public';}
4976
+ updateMemoryCardBadge(chunkId,'team');
4977
+ } else {toast(d.error||t('toast.memoryShareFail'),'error');}
4303
4978
  }catch(e){toast(t('toast.memoryShareFail')+': '+e.message,'error');}
4304
4979
  }
4305
4980
 
4306
4981
  async function unshareMemory(chunkId){
4982
+ if(!(await confirmModal(t('share.memoryUnshareConfirm'),{danger:true}))) return;
4307
4983
  try{
4308
4984
  const r=await fetch('/api/sharing/memories/unshare',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({chunkId:chunkId})});
4309
4985
  const d=await r.json();
4310
- if(d.ok){toast(t('toast.memoryUnshared'),'success');loadMemories();} else {toast(d.error||t('toast.memoryUnshareFail'),'error');}
4986
+ if(d.ok){
4987
+ toast(t('toast.memoryUnshared'),'success');
4988
+ if(memoryCache[chunkId]){memoryCache[chunkId].sharingVisibility=null;}
4989
+ var m=memoryCache[chunkId];
4990
+ var newScope=(m&&m.owner==='public')?'local':'private';
4991
+ updateMemoryCardBadge(chunkId,newScope);
4992
+ } else {toast(d.error||t('toast.memoryUnshareFail'),'error');}
4311
4993
  }catch(e){toast(t('toast.memoryUnshareFail')+': '+e.message,'error');}
4312
4994
  }
4313
4995
 
4996
+ function localMemoryErrorMessage(err){
4997
+ if(err==='original_owner_missing') return t('share.local.originalOwnerMissing');
4998
+ return err||t('toast.opfail');
4999
+ }
5000
+
4314
5001
  function debounceSkillSearch(){
4315
5002
  clearTimeout(skillSearchTimer);
4316
5003
  skillSearchTimer=setTimeout(function(){loadSkills();},300);
@@ -4700,10 +5387,13 @@ async function loadTasks(){
4700
5387
  const timeStr=formatTime(task.startedAt);
4701
5388
  const endStr=task.endedAt?formatTime(task.endedAt):'';
4702
5389
  const durationStr=task.endedAt?formatDuration(task.endedAt-task.startedAt):'';
5390
+ var taskIsLocalShared=task.owner==='public';
5391
+ var taskIsTeamShared=!!task.sharingVisibility;
5392
+ var taskScope=taskIsTeamShared?'team':taskIsLocalShared?'local':'private';
4703
5393
  return '<div class="task-card status-'+task.status+'" onclick="openTaskDetail(\\''+task.id+'\\')">'+
4704
5394
  '<div class="task-card-top">'+
4705
5395
  '<div class="task-card-title">'+esc(task.title)+'</div>'+
4706
- '<span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span>'+
5396
+ '<div class="task-card-badges">'+renderScopeBadge(taskScope)+'<span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span></div>'+
4707
5397
  '</div>'+
4708
5398
  (task.summary?'<div class="task-card-summary'+(task.status==='skipped'?' skipped-reason':'')+'">'+esc(task.summary)+'</div>':'')+
4709
5399
  '<div class="task-card-bottom">'+
@@ -4715,6 +5405,9 @@ async function loadTasks(){
4715
5405
  '<div class="card-actions" onclick="event.stopPropagation()">'+
4716
5406
  '<button class="btn btn-sm btn-ghost" onclick="openTaskDetail(\\''+task.id+'\\')">'+t('card.expand')+'</button>'+
4717
5407
  (task.status==='completed'&&(!task.skillStatus||task.skillStatus==='not_generated'||task.skillStatus==='skipped')?'<button class="btn btn-sm btn-ghost" onclick="retrySkillGen(\\''+task.id+'\\')">'+t('task.retrySkill.short')+'</button>':'')+
5408
+ (task.status==='completed'
5409
+ ?'<button class="btn btn-sm btn-ghost" onclick="openTaskScopeModalFromList(\\''+task.id+'\\',\\''+taskScope+'\\')">\\u270F '+t('share.shareBtn')+'</button>'
5410
+ :'<button class="btn btn-sm btn-ghost" style="opacity:0.45;cursor:not-allowed" onclick="toast(t(\\x27share.scope.taskNotCompleted\\x27),\\x27warn\\x27)">\\u270F '+t('share.shareBtn')+'</button>')+
4718
5411
  '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteTask(\\''+task.id+'\\')">'+t('task.delete')+'</button>'+
4719
5412
  '</div>'+
4720
5413
  '</div>';
@@ -4727,6 +5420,20 @@ async function loadTasks(){
4727
5420
  }
4728
5421
  }
4729
5422
 
5423
+ function updateTaskCardBadge(taskId,newScope){
5424
+ var cards=document.querySelectorAll('.task-card');
5425
+ for(var i=0;i<cards.length;i++){
5426
+ var onclick=cards[i].getAttribute('onclick')||'';
5427
+ if(onclick.indexOf(taskId)===-1) continue;
5428
+ var badges=cards[i].querySelector('.task-card-badges');
5429
+ if(!badges) continue;
5430
+ var oldBadge=badges.querySelectorAll('span[style*="border-radius:10px"]');
5431
+ for(var j=0;j<oldBadge.length;j++) oldBadge[j].remove();
5432
+ badges.insertAdjacentHTML('afterbegin',renderScopeBadge(newScope));
5433
+ break;
5434
+ }
5435
+ }
5436
+
4730
5437
  function renderTasksPagination(total){
4731
5438
  const el=document.getElementById('tasksPagination');
4732
5439
  const pages=Math.ceil(total/TASKS_PER_PAGE);
@@ -4766,9 +5473,9 @@ async function openTaskDetail(taskId){
4766
5473
 
4767
5474
  const meta=[
4768
5475
  '<span class="meta-item"><span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span></span>',
4769
- '<span class="meta-item">\\u{1F4C5} '+formatTime(task.startedAt)+'</span>',
5476
+ '<span class="meta-item">\\u{1F4C5} '+formatDateTimeSeconds(task.startedAt)+'</span>',
4770
5477
  ];
4771
- if(task.endedAt) meta.push('<span class="meta-item">\\u2192 '+formatTime(task.endedAt)+'</span>');
5478
+ if(task.endedAt) meta.push('<span class="meta-item">\\u2192 '+formatDateTimeSeconds(task.endedAt)+'</span>');
4772
5479
  meta.push('<span class="meta-item">\\u{1F4C2} '+task.sessionKey+'</span>');
4773
5480
  meta.push('<span class="meta-item">\\u{1F4DD} '+task.chunks.length+' '+t('tasks.chunks.label')+'</span>');
4774
5481
  meta.push('<div style="width:100%;margin-top:4px"><span class="meta-item" style="width:100%">'+t('tasks.taskid')+'<span class="task-id-full">'+esc(task.id)+'</span></span></div>');
@@ -4793,11 +5500,17 @@ async function openTaskDetail(taskId){
4793
5500
  }else{
4794
5501
  document.getElementById('taskDetailChunks').innerHTML=task.chunks.map(function(c,i){
4795
5502
  var roleLabel=c.role==='user'?t('tasks.role.user'):c.role==='assistant'?t('tasks.role.assistant'):c.role.toUpperCase();
5503
+ var avatarIcon=c.role==='user'?'U':c.role==='assistant'?'A':'T';
4796
5504
  return '<div class="task-chunk-item role-'+c.role+'">'+
5505
+ '<div class="task-chunk-avatar">'+avatarIcon+'</div>'+
5506
+ '<div class="task-chunk-body">'+
5507
+ '<div class="task-chunk-header">'+
4797
5508
  '<div class="task-chunk-role '+c.role+'">'+roleLabel+'</div>'+
5509
+ '<div class="task-chunk-time">'+formatDateTimeSeconds(c.createdAt)+'</div>'+
5510
+ '</div>'+
4798
5511
  '<div class="task-chunk-bubble collapsed" id="chunk_b_'+i+'">'+esc(c.content)+'</div>'+
4799
5512
  '<div class="task-chunk-expand" id="chunk_e_'+i+'" onclick="toggleChunkExpand('+i+')"><span class="expand-arrow">▼</span> <span class="expand-label">'+t('tasks.expand')+'</span></div>'+
4800
- '<div class="task-chunk-time">'+formatTime(c.createdAt)+'</div>'+
5513
+ '</div>'+
4801
5514
  '</div>';
4802
5515
  }).join('');
4803
5516
  setTimeout(function(){initChunkExpanders(task.chunks.length)},50);
@@ -4890,14 +5603,13 @@ function toggleChunkExpand(i){
4890
5603
  e.querySelector('.expand-label').textContent=t('tasks.expand');
4891
5604
  }
4892
5605
  }
4893
-
4894
5606
  function closeTaskDetail(event){
4895
5607
  if(event && event.target!==document.getElementById('taskDetailOverlay')) return;
4896
5608
  document.getElementById('taskDetailOverlay').classList.remove('show');
4897
5609
  }
4898
5610
 
4899
5611
  async function retrySkillGen(taskId){
4900
- if(!confirm(t('task.retrySkill.confirm'))) return;
5612
+ if(!(await confirmModal(t('task.retrySkill.confirm')))) return;
4901
5613
  try{
4902
5614
  const r=await fetch('/api/task/'+taskId+'/retry-skill',{method:'POST'});
4903
5615
  const d=await r.json();
@@ -4907,7 +5619,7 @@ async function retrySkillGen(taskId){
4907
5619
  }
4908
5620
 
4909
5621
  async function deleteTask(taskId){
4910
- if(!confirm(t('task.delete.confirm'))) return;
5622
+ if(!(await confirmModal(t('task.delete.confirm'),{danger:true}))) return;
4911
5623
  try{
4912
5624
  const r=await fetch('/api/task/'+taskId,{method:'DELETE'});
4913
5625
  const d=await r.json();
@@ -4929,6 +5641,22 @@ function setSkillStatusFilter(btn,status){
4929
5641
  loadSkills();
4930
5642
  }
4931
5643
 
5644
+ function updateSkillCardBadge(skillId,newScope){
5645
+ var cards=document.querySelectorAll('.skill-card');
5646
+ for(var i=0;i<cards.length;i++){
5647
+ var onclick=cards[i].getAttribute('onclick')||'';
5648
+ if(onclick.indexOf(skillId)===-1) continue;
5649
+ var badges=cards[i].querySelector('.skill-card-badges');
5650
+ if(!badges) continue;
5651
+ var oldBadge=badges.querySelectorAll('span[style*="border-radius:10px"]');
5652
+ for(var j=0;j<oldBadge.length;j++) oldBadge[j].remove();
5653
+ var versionBadge=badges.querySelector('.version');
5654
+ if(versionBadge) versionBadge.insertAdjacentHTML('afterend',renderScopeBadge(newScope));
5655
+ else badges.insertAdjacentHTML('afterbegin',renderScopeBadge(newScope));
5656
+ break;
5657
+ }
5658
+ }
5659
+
4932
5660
  async function loadSkills(){
4933
5661
  const list=document.getElementById('skillsList');
4934
5662
  const hubList=document.getElementById('hubSkillsList');
@@ -4976,14 +5704,16 @@ async function loadSkills(){
4976
5704
  const sourceLabel=tags.includes('hub-import')?'hub-import':skill.sourceType;
4977
5705
  const qs=skill.qualityScore;
4978
5706
  const qsBadge=qs!==null&&qs!==undefined?'<span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">★ '+qs.toFixed(1)+'</span>':'';
4979
- const visBadge=skill.visibility==='public'?'<span class="skill-badge visibility-public">🌐 '+t('skills.visibility.public')+'</span>':'';
5707
+ const skillIsLocalShared=skill.visibility==='public';
5708
+ const skillIsTeamShared=!!skill.sharingVisibility;
5709
+ const skillScope=skillIsTeamShared?'team':skillIsLocalShared?'local':'private';
4980
5710
  return '<div class="skill-card '+installedClass+' '+statusClass+'" onclick="openSkillDetail(&quot;'+escAttr(skill.id)+'&quot;)">'+
4981
5711
  '<div class="skill-card-top">'+
4982
5712
  '<div class="skill-card-name">🧠 '+esc(skill.name)+'</div>'+
4983
5713
  '<div class="skill-card-badges">'+
4984
5714
  qsBadge+
4985
5715
  '<span class="skill-badge version">v'+skill.version+'</span>'+
4986
- visBadge+
5716
+ renderScopeBadge(skillScope)+
4987
5717
  (skill.installed?'<span class="skill-badge installed">'+t('skills.installed.badge')+'</span>':'')+
4988
5718
  '<span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span>'+
4989
5719
  '</div>'+
@@ -4992,7 +5722,13 @@ async function loadSkills(){
4992
5722
  '<div class="skill-card-bottom">'+
4993
5723
  '<span class="tag"><span class="icon">📅</span> '+timeStr+'</span>'+
4994
5724
  '<span class="tag"><span class="icon">📦</span> '+sourceLabel+'</span>'+
4995
- (tags.length>0?'<div class="skill-card-tags">'+tags.map(t=>'<span class="skill-tag">'+esc(t)+'</span>').join('')+'</div>':'')+
5725
+ (tags.length>0?'<div class="skill-card-tags">'+tags.map(tg=>'<span class="skill-tag">'+esc(tg)+'</span>').join('')+'</div>':'')+
5726
+ '<span class="card-actions-inline" onclick="event.stopPropagation()">'+
5727
+ '<button class="btn btn-sm btn-ghost" onclick="openSkillDetail(&quot;'+escAttr(skill.id)+'&quot;)">'+t('card.expand')+'</button>'+
5728
+ (skill.status==='active'
5729
+ ?'<button class="btn btn-sm btn-ghost" onclick="openSkillScopeModalFromList(&quot;'+escAttr(skill.id)+'&quot;,&quot;'+skillScope+'&quot;)">\\u270F '+t('share.shareBtn')+'</button>'
5730
+ :'<button class="btn btn-sm btn-ghost" style="opacity:0.45;cursor:not-allowed" onclick="toast(t(\\x27share.scope.skillNotActive\\x27),\\x27warn\\x27)">\\u270F '+t('share.shareBtn')+'</button>')+
5731
+ '</span>'+
4996
5732
  '</div>'+
4997
5733
  '</div>';
4998
5734
  }).join('');
@@ -5052,12 +5788,12 @@ async function loadSkills(){
5052
5788
  '<span class="meta-chip">visibility: '+esc(skill.visibility||'hub')+'</span>'+
5053
5789
  (skill.version!=null?'<span class="meta-chip">v'+skill.version+'</span>':'')+
5054
5790
  '</div>'+
5055
- '<div class="hub-skill-actions"><button class="btn btn-sm" onclick="event.stopPropagation();pullHubSkill(&quot;'+escAttr(skill.skillId)+'&quot;)">Pull to Local</button></div>'+
5791
+ '<div class="hub-skill-actions"><button class="btn btn-sm" onclick="event.stopPropagation();pullHubSkill(&quot;'+escAttr(skill.skillId)+'&quot;)">'+t('skill.pullToLocal')+'</button></div>'+
5056
5792
  '</div>';
5057
5793
  }).join(''):'';
5058
5794
  }
5059
5795
 
5060
- document.getElementById('skillSearchMeta').textContent=t('skills.search.local')+' '+localHits.length+(hubHits.length?' · Hub '+hubHits.length:'');
5796
+ document.getElementById('skillSearchMeta').textContent=t('skills.search.local')+' '+localHits.length+(hubHits.length?' · '+t('scope.hub')+' '+hubHits.length:'');
5061
5797
  document.getElementById('skillsTotalCount').textContent=formatNum(localHits.length+hubHits.length);
5062
5798
  document.getElementById('skillsActiveCount').textContent=formatNum(localHits.length);
5063
5799
  document.getElementById('skillsDraftCount').textContent='0';
@@ -5096,7 +5832,7 @@ async function loadHubSkills(hubList){
5096
5832
  '<span class="meta-chip">visibility: '+esc(skill.visibility||'hub')+'</span>'+
5097
5833
  (skill.version!=null?'<span class="meta-chip">v'+skill.version+'</span>':'')+
5098
5834
  '</div>'+
5099
- '<div class="hub-skill-actions"><button class="btn btn-sm" onclick="event.stopPropagation();pullHubSkill(\\''+escAttr(skill.sourceSkillId)+'\\')">Pull to Local</button></div>'+
5835
+ '<div class="hub-skill-actions"><button class="btn btn-sm" onclick="event.stopPropagation();pullHubSkill(\\''+escAttr(skill.sourceSkillId)+'\\')">'+t('skill.pullToLocal')+'</button></div>'+
5100
5836
  '</div>';
5101
5837
  }).join('');
5102
5838
  }catch(e){
@@ -5125,6 +5861,7 @@ async function openSkillDetail(skillId){
5125
5861
  document.getElementById('skillRelatedTasks').innerHTML='';
5126
5862
  var vb=document.getElementById('skillVisibilityBtn');if(vb)vb.style.display='';
5127
5863
  var db=document.getElementById('skillDownloadBtn');if(db)db.style.display='';
5864
+ var sb=document.getElementById('skillScopeBadge');if(sb)sb.innerHTML='';
5128
5865
  document.getElementById('skillDetailActions').innerHTML='';
5129
5866
 
5130
5867
  try{
@@ -5146,32 +5883,29 @@ async function openSkillDetail(skillId){
5146
5883
 
5147
5884
  const qs=skill.qualityScore;
5148
5885
  const qsBadge=qs!==null&&qs!==undefined?'<span class="meta-item"><span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+qs.toFixed(1)+'/10</span></span>':'';
5149
- const visMeta=skill.visibility==='public'?'<span class="meta-item"><span class="skill-badge visibility-public">\\u{1F310} '+t('skills.visibility.public')+'</span></span>':'<span class="meta-item"><span class="skill-badge">\\u{1F512} '+t('skills.visibility.private')+'</span></span>';
5886
+ const detailSkillIsLocalShared=skill.visibility==='public';
5887
+ const detailSkillIsTeamShared=!!skill.sharingVisibility;
5888
+ const detailSkillScope=detailSkillIsTeamShared?'team':detailSkillIsLocalShared?'local':'private';
5150
5889
  document.getElementById('skillDetailMeta').innerHTML=[
5151
5890
  '<span class="meta-item"><span class="skill-badge version">v'+skill.version+'</span></span>',
5152
5891
  '<span class="meta-item"><span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span></span>',
5153
- visMeta,
5154
5892
  qsBadge,
5155
5893
  skill.installed?'<span class="meta-item"><span class="skill-badge installed">'+t('skills.installed.badge')+'</span></span>':'',
5156
5894
  '<span class="meta-item">\\u{1F4C5} '+formatTime(skill.createdAt)+'</span>',
5157
5895
  '<span class="meta-item">\\u270F '+t('skills.updated')+formatTime(skill.updatedAt)+'</span>',
5158
5896
  ].filter(Boolean).join('');
5159
5897
 
5898
+ var scopeBadgeEl=document.getElementById('skillScopeBadge');
5899
+ if(scopeBadgeEl) scopeBadgeEl.innerHTML=renderScopeBadge(detailSkillScope);
5900
+
5160
5901
  const visBtn=document.getElementById('skillVisibilityBtn');
5161
5902
  visBtn.className='skill-vis-btn';
5162
- if(skill.visibility==='public'){
5163
- visBtn.textContent='\\u{1F512} '+t('skills.setPrivate');
5164
- visBtn.classList.add('is-public');
5165
- visBtn.dataset.vis='public';
5166
- } else {
5167
- visBtn.textContent='\\u{1F310} '+t('skills.setPublic');
5168
- visBtn.classList.add('is-private');
5169
- visBtn.dataset.vis='private';
5170
- }
5903
+ visBtn.textContent='\\u270F '+t('share.shareBtn');
5904
+ visBtn.dataset.vis=detailSkillScope;
5905
+ visBtn.onclick=function(){openSkillScopeModal();};
5171
5906
 
5172
5907
  document.getElementById('skillDetailDesc').textContent=skill.description;
5173
5908
  currentSkillDetail=skill;
5174
- renderSkillShareActions(skill);
5175
5909
 
5176
5910
  if(files.length>0){
5177
5911
  const fileIcons={'skill':'\\u{1F4D6}','script':'\\u{2699}','reference':'\\u{1F4CE}','file':'\\u{1F4C4}'};
@@ -5577,7 +6311,7 @@ async function saveModelsConfig(){
5577
6311
  if(!hasSkillConfig){msgs.push(t('settings.save.skill.fallback'));}
5578
6312
  var fbInfo=fb.available?(fb.model+' ('+fb.baseUrl+')'):t('settings.save.fallback.none');
5579
6313
  var confirmMsg=msgs.join('\\n')+'\\n\\n'+t('settings.save.fallback.model')+fbInfo+'\\n\\n'+t('settings.save.fallback.confirm');
5580
- if(!confirm(confirmMsg)){done();return;}
6314
+ if(!(await confirmModal(confirmMsg))){done();return;}
5581
6315
  }catch(e){}
5582
6316
  }
5583
6317
 
@@ -5631,7 +6365,7 @@ async function saveHubConfig(){
5631
6365
  var prevRole=sharingStatusCache&&sharingStatusCache.role;
5632
6366
  if(prevSharingEnabled&&!sharingEnabled){
5633
6367
  var confirmMsg=prevRole==='hub'?t('sharing.disable.confirm.hub'):t('sharing.disable.confirm.client');
5634
- if(!confirm(confirmMsg)){done();return;}
6368
+ if(!(await confirmModal(confirmMsg,{danger:true}))){done();return;}
5635
6369
  }
5636
6370
 
5637
6371
  var ok=await doSaveConfig(cfg, saveBtn, 'hubSaved');
@@ -5734,7 +6468,7 @@ function closeSkillDetail(event){
5734
6468
  }
5735
6469
 
5736
6470
  async function deleteSkill(skillId){
5737
- if(!confirm(t('skill.delete.confirm'))) return;
6471
+ if(!(await confirmModal(t('skill.delete.confirm'),{danger:true}))) return;
5738
6472
  try{
5739
6473
  const r=await fetch('/api/skill/'+skillId,{method:'DELETE'});
5740
6474
  const d=await r.json();
@@ -5762,6 +6496,19 @@ function formatTime(ts){
5762
6496
  return new Date(ts).toLocaleString(dateLoc(),{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
5763
6497
  }
5764
6498
 
6499
+ function formatDateTimeSeconds(ts){
6500
+ if(!ts) return '-';
6501
+ return new Date(ts).toLocaleString(dateLoc(),{
6502
+ year:'numeric',
6503
+ month:'2-digit',
6504
+ day:'2-digit',
6505
+ hour:'2-digit',
6506
+ minute:'2-digit',
6507
+ second:'2-digit',
6508
+ hour12:false
6509
+ });
6510
+ }
6511
+
5765
6512
  function fillDays(rows,days){
5766
6513
  const map=new Map((rows||[]).map(r=>[r.date,{...r}]));
5767
6514
  const out=[];const now=new Date();
@@ -5846,19 +6593,43 @@ function renderChartCalls(rows){
5846
6593
 
5847
6594
  /* ─── Tool Performance Chart ─── */
5848
6595
  let toolMinutes=60;
6596
+ let toolCustomFrom='';
6597
+ let toolCustomTo='';
5849
6598
  const TOOL_COLORS=['#818cf8','#34d399','#fbbf24','#f87171','#38bdf8','#a78bfa','#fb923c'];
5850
6599
 
5851
6600
  function setToolMinutes(m){
5852
6601
  toolMinutes=m;
6602
+ toolCustomFrom='';
6603
+ toolCustomTo='';
6604
+ var fi=document.getElementById('toolRangeFrom');
6605
+ var ti=document.getElementById('toolRangeTo');
6606
+ if(fi) fi.value='';
6607
+ if(ti) ti.value='';
5853
6608
  document.querySelectorAll('.tool-range').forEach(b=>{
5854
6609
  b.classList.toggle('active',Number(b.dataset.mins)===m);
5855
6610
  });
5856
6611
  loadToolMetrics();
5857
6612
  }
5858
6613
 
6614
+ function applyCustomToolRange(){
6615
+ var fi=document.getElementById('toolRangeFrom');
6616
+ var ti=document.getElementById('toolRangeTo');
6617
+ var from=fi?fi.value:'';
6618
+ var to=ti?ti.value:'';
6619
+ if(!from&&!to){toast(t('chart.selectRange'),'warn');return;}
6620
+ toolCustomFrom=from;
6621
+ toolCustomTo=to;
6622
+ document.querySelectorAll('.tool-range').forEach(b=>b.classList.remove('active'));
6623
+ loadToolMetrics();
6624
+ }
6625
+
5859
6626
  async function loadToolMetrics(){
5860
6627
  try{
5861
- const r=await fetch('/api/tool-metrics?minutes='+toolMinutes);
6628
+ var qs='minutes='+toolMinutes;
6629
+ if(toolCustomFrom) qs='from='+encodeURIComponent(new Date(toolCustomFrom).toISOString());
6630
+ if(toolCustomTo) qs+='&to='+encodeURIComponent(new Date(toolCustomTo).toISOString());
6631
+ else if(toolCustomFrom) qs+='&to='+encodeURIComponent(new Date().toISOString());
6632
+ const r=await fetch('/api/tool-metrics?'+qs);
5862
6633
  if(!r.ok) return;
5863
6634
  const d=await r.json();
5864
6635
  if(d.error) return;
@@ -6032,6 +6803,147 @@ function stopSharingPoll(){
6032
6803
  if(_sharingPollTimer){clearInterval(_sharingPollTimer);_sharingPollTimer=null;}
6033
6804
  }
6034
6805
 
6806
+ /* ─── Notifications ─── */
6807
+ var _notifCache=[];
6808
+ var _notifUnread=0;
6809
+ var _notifPollTimer=null;
6810
+ var _notifPanelOpen=false;
6811
+
6812
+ function toggleNotifPanel(e){
6813
+ if(e)e.stopPropagation();
6814
+ var panel=document.getElementById('notifPanel');
6815
+ _notifPanelOpen=!_notifPanelOpen;
6816
+ if(_notifPanelOpen){
6817
+ panel.classList.add('show');
6818
+ loadNotifications();
6819
+ }else{
6820
+ panel.classList.remove('show');
6821
+ }
6822
+ }
6823
+
6824
+ document.addEventListener('click',function(e){
6825
+ if(!_notifPanelOpen) return;
6826
+ var wrap=document.getElementById('notifBellWrap');
6827
+ if(wrap&&!wrap.contains(e.target)){
6828
+ _notifPanelOpen=false;
6829
+ document.getElementById('notifPanel').classList.remove('show');
6830
+ }
6831
+ });
6832
+
6833
+ function notifTimeAgo(ts){
6834
+ var diff=Date.now()-ts;
6835
+ if(diff<60000) return t('notif.timeAgo.just');
6836
+ if(diff<3600000) return t('notif.timeAgo.min').replace('{n}',Math.floor(diff/60000));
6837
+ if(diff<86400000) return t('notif.timeAgo.hour').replace('{n}',Math.floor(diff/3600000));
6838
+ return t('notif.timeAgo.day').replace('{n}',Math.floor(diff/86400000));
6839
+ }
6840
+
6841
+ function notifIcon(resource){
6842
+ if(resource==='memory') return '\\u{1F4DD}';
6843
+ if(resource==='task') return '\\u{1F4CB}';
6844
+ if(resource==='skill') return '\\u{1F9E0}';
6845
+ return '\\u{1F514}';
6846
+ }
6847
+
6848
+ function notifTypeText(n){
6849
+ if(n.type==='resource_removed'){
6850
+ return t('notif.removed.'+n.resource)||t('notif.removed.memory');
6851
+ }
6852
+ return n.message||n.type;
6853
+ }
6854
+
6855
+ async function loadNotifications(){
6856
+ try{
6857
+ var r=await fetch('/api/sharing/notifications');
6858
+ var d=await r.json();
6859
+ _notifCache=d.notifications||[];
6860
+ _notifUnread=d.unreadCount||0;
6861
+ renderNotifBadge();
6862
+ renderNotifPanel();
6863
+ }catch(e){}
6864
+ }
6865
+
6866
+ async function pollNotifCount(){
6867
+ try{
6868
+ var r=await fetch('/api/sharing/notifications?unread=1');
6869
+ var d=await r.json();
6870
+ _notifUnread=d.unreadCount||0;
6871
+ renderNotifBadge();
6872
+ }catch(e){}
6873
+ }
6874
+
6875
+ function renderNotifBadge(){
6876
+ var badge=document.getElementById('notifBadge');
6877
+ if(!badge) return;
6878
+ if(_notifUnread>0){
6879
+ badge.textContent=_notifUnread>99?'99+':_notifUnread;
6880
+ badge.classList.add('show');
6881
+ }else{
6882
+ badge.classList.remove('show');
6883
+ }
6884
+ }
6885
+
6886
+ function renderNotifPanel(){
6887
+ var body=document.getElementById('notifPanelBody');
6888
+ if(!body) return;
6889
+ if(_notifCache.length===0){
6890
+ body.innerHTML='<div class="notif-empty">'+t('notif.empty')+'</div>';
6891
+ return;
6892
+ }
6893
+ body.innerHTML=_notifCache.map(function(n){
6894
+ var cls='notif-item'+(n.read?'':' unread');
6895
+ return '<div class="'+cls+'" onclick="markNotifRead(&quot;'+esc(n.id)+'&quot;)">'+
6896
+ '<div class="notif-item-icon">'+notifIcon(n.resource)+'</div>'+
6897
+ '<div class="notif-item-body">'+
6898
+ '<div class="notif-item-title">'+esc(notifTypeText(n))+'</div>'+
6899
+ '<div class="notif-item-name">'+esc(n.title)+'</div>'+
6900
+ '<div class="notif-item-time">'+notifTimeAgo(n.createdAt)+'</div>'+
6901
+ '</div>'+
6902
+ '<div class="notif-item-dot"></div>'+
6903
+ '</div>';
6904
+ }).join('');
6905
+ }
6906
+
6907
+ async function markNotifRead(id){
6908
+ try{
6909
+ await fetch('/api/sharing/notifications/read',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ids:[id]})});
6910
+ _notifCache.forEach(function(n){if(n.id===id)n.read=true;});
6911
+ _notifUnread=Math.max(0,_notifUnread-1);
6912
+ renderNotifBadge();
6913
+ renderNotifPanel();
6914
+ }catch(e){}
6915
+ }
6916
+
6917
+ async function markAllNotifsRead(){
6918
+ try{
6919
+ await fetch('/api/sharing/notifications/read',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({all:true})});
6920
+ _notifCache.forEach(function(n){n.read=true;});
6921
+ _notifUnread=0;
6922
+ renderNotifBadge();
6923
+ renderNotifPanel();
6924
+ }catch(e){}
6925
+ }
6926
+
6927
+ async function clearAllNotifs(){
6928
+ try{
6929
+ await fetch('/api/sharing/notifications/clear',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});
6930
+ _notifCache=[];
6931
+ _notifUnread=0;
6932
+ renderNotifBadge();
6933
+ renderNotifPanel();
6934
+ }catch(e){}
6935
+ }
6936
+
6937
+ function startNotifPoll(){
6938
+ if(_notifPollTimer) return;
6939
+ pollNotifCount();
6940
+ _notifPollTimer=setInterval(pollNotifCount,15000);
6941
+ }
6942
+
6943
+ function stopNotifPoll(){
6944
+ if(_notifPollTimer){clearInterval(_notifPollTimer);_notifPollTimer=null;}
6945
+ }
6946
+
6035
6947
  /* ─── Data loading ─── */
6036
6948
  async function loadAll(){
6037
6949
  await Promise.all([loadStats(),loadMemories(),loadSharingStatus(false)]);
@@ -6039,6 +6951,7 @@ async function loadAll(){
6039
6951
  connectPPSSE();
6040
6952
  checkForUpdate();
6041
6953
  startSharingPoll();
6954
+ startNotifPoll();
6042
6955
  }
6043
6956
 
6044
6957
  async function loadStats(ownerFilter){
@@ -6320,9 +7233,10 @@ function renderMemories(items){
6320
7233
  const importBadge=isImported?'<span class="import-badge">\u{1F990} '+t('card.imported')+'</span>':'';
6321
7234
  const ownerVal=m.owner||'agent:main';
6322
7235
  const isPublicMem=ownerVal==='public';
6323
- const ownerBadge=isPublicMem?'<span class="owner-badge public">\\u{1F310} '+t('filter.public')+'</span>':'<span class="owner-badge agent">\\u{1F512} '+t('filter.private')+'</span>';
7236
+ const localManaged=!!m.localSharingManaged;
6324
7237
  const memShared=m.sharingVisibility||null;
6325
- const memShareBadge=memShared?'<span style="display:inline-flex;align-items:center;gap:3px;padding:2px 8px;background:#22c55e22;border:1px solid #22c55e44;border-radius:10px;font-size:11px;color:#22c55e">\\u2713 '+(memShared==='group'?t('share.group'):t('share.public'))+'</span>':'';
7238
+ const memScope=memShared?'team':isPublicMem?'local':'private';
7239
+ const memScopeBadge=renderScopeBadge(memScope);
6326
7240
  let dedupInfo='';
6327
7241
  if(ds==='duplicate'||ds==='merged'){
6328
7242
  const reason=m.dedup_reason?'<span style="font-size:11px;color:var(--text-muted)">'+t('card.dedupReason')+esc(m.dedup_reason)+'</span>':'';
@@ -6347,7 +7261,7 @@ function renderMemories(items){
6347
7261
  }catch(e){}
6348
7262
  }
6349
7263
  return '<div class="memory-card'+(isInactive?' dedup-inactive':'')+'">'+
6350
- '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span>'+ownerBadge+importBadge+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
7264
+ '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span>'+memScopeBadge+importBadge+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
6351
7265
  '<div class="card-summary">'+cardTitle+'</div>'+
6352
7266
  (function(){
6353
7267
  if(mc<=0) return '';
@@ -6371,14 +7285,37 @@ function renderMemories(items){
6371
7285
  '<button class="btn btn-sm btn-ghost" onclick="toggleContent(\\''+id+'\\')">'+t('card.expand')+'</button>'+
6372
7286
  (mc>0?'<button class="btn btn-sm btn-ghost" onclick="toggleHistory(\\''+id+'\\')">'+t('card.evolveHistory')+'</button>':'')+
6373
7287
  '<button class="btn btn-sm btn-ghost" onclick="openEditModal(\\''+id+'\\')">'+t('card.edit')+'</button>'+
6374
- (isPublicMem?'<button class="btn btn-sm btn-ghost" onclick="toggleMemoryPublic(\\''+id+'\\',false)">\\u{1F512} '+t('skills.setPrivate')+'</button>':'<button class="btn btn-sm btn-ghost mem-public-btn" onclick="toggleMemoryPublic(\\''+id+'\\',true)">\\u{1F310} '+t('skills.setPublic')+'</button>')+
6375
- (memShared?'<button class="btn btn-sm btn-ghost" onclick="unshareMemory(\\''+id+'\\')">'+t('share.unshareBtn')+'</button>':'<button class="btn btn-sm btn-ghost" onclick="shareMemoryPrompt(\\''+id+'\\')">'+t('share.shareBtn')+'</button>')+
7288
+ (isInactive
7289
+ ?'<button class="btn btn-sm btn-ghost" style="opacity:0.45;cursor:not-allowed" onclick="toast(t(\\x27share.scope.inactiveDisabled\\x27),\\x27warn\\x27)">\\u270F '+t('share.shareBtn')+'</button>'
7290
+ :'<button class="btn btn-sm btn-ghost" onclick="openMemoryScopeModal(\\''+id+'\\',\\''+memScope+'\\')">\\u270F '+t('share.shareBtn')+'</button>')+
6376
7291
  '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteMemory(\\''+id+'\\')">'+t('card.delete')+'</button>'+
6377
7292
  vscore+
6378
7293
  '</div></div>';
6379
7294
  }).join('');
6380
7295
  }
6381
7296
 
7297
+ function updateMemoryCardBadge(chunkId,newScope){
7298
+ var cards=document.querySelectorAll('.memory-card');
7299
+ for(var i=0;i<cards.length;i++){
7300
+ var btns=cards[i].querySelectorAll('button');
7301
+ var shareBtn=null;
7302
+ for(var b=0;b<btns.length;b++){
7303
+ var oc=btns[b].getAttribute('onclick')||'';
7304
+ if(oc.indexOf('openMemoryScopeModal')>=0&&oc.indexOf(chunkId)>=0){shareBtn=btns[b];break;}
7305
+ }
7306
+ if(!shareBtn) continue;
7307
+ var metaDiv=cards[i].querySelector('.meta');
7308
+ if(!metaDiv) continue;
7309
+ var oldBadge=metaDiv.querySelectorAll('span[style*="border-radius:10px"]');
7310
+ for(var j=0;j<oldBadge.length;j++) oldBadge[j].remove();
7311
+ var roleTag=metaDiv.querySelector('.role-tag');
7312
+ if(roleTag&&roleTag.nextSibling) roleTag.insertAdjacentHTML('afterend',renderScopeBadge(newScope));
7313
+ else metaDiv.insertAdjacentHTML('beforeend',renderScopeBadge(newScope));
7314
+ shareBtn.setAttribute('onclick','openMemoryScopeModal(\\x27'+chunkId+'\\x27,\\x27'+newScope+'\\x27)');
7315
+ break;
7316
+ }
7317
+ }
7318
+
6382
7319
  function renderPagination(){
6383
7320
  const el=document.getElementById('pagination');
6384
7321
  if(totalPages<=1){el.innerHTML='';return}
@@ -6459,13 +7396,13 @@ async function showMemoryModal(chunkId){
6459
7396
  if(m.summary) h+='<div class="mm-summary">'+esc(m.summary)+'</div>';
6460
7397
  h+='</div>';
6461
7398
  if(m.content){
6462
- h+='<div class="mm-section"><div class="mm-section-label">Content</div><pre class="mm-content">'+esc(m.content)+'</pre></div>';
7399
+ h+='<div class="mm-section"><div class="mm-section-label">'+t('admin.content')+'</div><pre class="mm-content">'+esc(m.content)+'</pre></div>';
6463
7400
  }
6464
7401
  h+='<div class="mm-meta">';
6465
- if(m.session_key) h+='<div class="mm-meta-chip"><strong>Session</strong><span>'+esc(m.session_key.slice(0,12))+'</span></div>';
7402
+ if(m.session_key) h+='<div class="mm-meta-chip"><strong>'+t('admin.session')+'</strong><span>'+esc(m.session_key.slice(0,12))+'</span></div>';
6466
7403
  h+='<div class="mm-meta-chip"><strong>'+t('memory.detail.created')+'</strong><span>'+fmtModalDate(m.created_at)+'</span></div>';
6467
7404
  if(m.updated_at) h+='<div class="mm-meta-chip"><strong>'+t('memory.detail.updated')+'</strong><span>'+fmtModalDate(m.updated_at)+'</span></div>';
6468
- if(m.kind) h+='<div class="mm-meta-chip"><strong>Kind</strong><span>'+esc(m.kind)+'</span></div>';
7405
+ if(m.kind) h+='<div class="mm-meta-chip"><strong>'+t('admin.kind')+'</strong><span>'+esc(m.kind)+'</span></div>';
6469
7406
  h+='</div>';
6470
7407
  if(m.dedup_reason){
6471
7408
  h+='<div class="mm-dedup"><div class="mm-dedup-box">'+esc(m.dedup_reason)+'</div></div>';
@@ -6548,27 +7485,46 @@ async function submitModal(){
6548
7485
  }
6549
7486
 
6550
7487
  async function deleteMemory(id){
6551
- if(!confirm(t('confirm.delete')))return;
7488
+ if(!(await confirmModal(t('confirm.delete'),{danger:true})))return;
6552
7489
  const r=await fetch('/api/memory/'+id,{method:'DELETE'});
6553
7490
  const d=await r.json();
6554
7491
  if(d.ok){toast(t('toast.deleted'),'success');loadAll();}
6555
7492
  else{toast(t('toast.delfail'),'error')}
6556
7493
  }
6557
7494
 
7495
+ function openMemoryScopeModal(id,currentScope){
7496
+ openScopeSelectorModal('memory',id,currentScope,function(newScope){
7497
+ if(memoryCache[id]){
7498
+ if(newScope==='team'){memoryCache[id].sharingVisibility='public';}
7499
+ else if(newScope==='local'){memoryCache[id].sharingVisibility=null;memoryCache[id].owner='public';}
7500
+ else{memoryCache[id].sharingVisibility=null;memoryCache[id].owner='agent:main';}
7501
+ }
7502
+ updateMemoryCardBadge(id,newScope);
7503
+ });
7504
+ }
7505
+
6558
7506
  async function toggleMemoryPublic(id,setPublic){
6559
- const newOwner=setPublic?'public':'agent:main';
6560
7507
  try{
6561
- const r=await fetch('/api/memory/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({owner:newOwner})});
7508
+ if(!setPublic && !(await confirmModal(t('share.memoryLocalUnshareConfirm'),{danger:true}))) return;
7509
+ const memory=memoryCache[id]||{};
7510
+ const url=setPublic?'/api/memories/share-local':'/api/memories/unshare-local';
7511
+ const body=setPublic?{chunkId:id}:{chunkId:id,privateOwner:memory.localOriginalOwner||undefined};
7512
+ const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
6562
7513
  const d=await r.json();
6563
- if(d.ok){toast(setPublic?t('toast.setPublic'):t('toast.setPrivate'),'success');loadAll();}
6564
- else{toast(d.error||t('toast.opfail'),'error')}
6565
- }catch(e){toast('Error: '+e.message,'error')}
7514
+ if(d.ok){
7515
+ toast(setPublic?t('toast.setPublic'):t('toast.setPrivate'),'success');
7516
+ var newScope=setPublic?'local':'private';
7517
+ if(memoryCache[id]){memoryCache[id].owner=setPublic?'public':'agent:main';}
7518
+ updateMemoryCardBadge(id,newScope);
7519
+ }
7520
+ else{toast(localMemoryErrorMessage(d.error),'error')}
7521
+ }catch(e){toast(localMemoryErrorMessage(e.message),'error')}
6566
7522
  }
6567
7523
 
6568
7524
  async function clearAll(){
6569
7525
  try{
6570
- if(!confirm(t('confirm.clearall')))return;
6571
- if(!confirm(t('confirm.clearall2')))return;
7526
+ if(!(await confirmModal(t('confirm.clearall'),{danger:true})))return;
7527
+ if(!(await confirmModal(t('confirm.clearall2'),{danger:true})))return;
6572
7528
  const r=await fetch('/api/memories',{method:'DELETE'});
6573
7529
  if(r.status===401){toast(t('settings.session.expired'),'error');return;}
6574
7530
  const d=await r.json();
@@ -6670,14 +7626,14 @@ async function migrateScan(showToast){
6670
7626
  }
6671
7627
  }
6672
7628
 
6673
- function migrateStart(){
7629
+ async function migrateStart(){
6674
7630
  const isResume=document.getElementById('migrateStartBtn').textContent===t('migrate.resume');
6675
7631
  if(!isResume){
6676
7632
  if(!migrateScanData||!migrateScanData.configReady){
6677
7633
  toast(t('migrate.scan.required'),'error');
6678
7634
  return;
6679
7635
  }
6680
- if(!confirm(t('migrate.start')+'?'))return;
7636
+ if(!(await confirmModal(t('migrate.start')+'?')))return;
6681
7637
  }
6682
7638
 
6683
7639
  const concSel=document.getElementById('migrateConcurrency');
@@ -7177,12 +8133,32 @@ function toast(msg,type='info'){
7177
8133
  const c=document.getElementById('toasts');
7178
8134
  const t=document.createElement('div');
7179
8135
  t.className='toast '+type;
7180
- const icons={success:'\\u2705',error:'\\u274C',info:'\\u2139\\uFE0F'};
8136
+ const icons={success:'\\u2705',error:'\\u274C',info:'\\u2139\\uFE0F',warn:'\\u26A0\\uFE0F'};
7181
8137
  t.innerHTML=(icons[type]||'')+' '+esc(msg);
7182
8138
  c.appendChild(t);
7183
8139
  setTimeout(()=>t.remove(),3500);
7184
8140
  }
7185
8141
 
8142
+ var _confirmResolve=null;
8143
+ function confirmModal(message,opts){
8144
+ opts=opts||{};
8145
+ return new Promise(function(resolve){
8146
+ _confirmResolve=resolve;
8147
+ var overlay=document.getElementById('confirmOverlay');
8148
+ document.getElementById('confirmTitle').textContent=opts.title||t('confirm.title')||'\u786E\u8BA4';
8149
+ document.getElementById('confirmBody').textContent=message||'';
8150
+ var okBtn=document.getElementById('confirmOkBtn');
8151
+ okBtn.textContent=opts.okText||t('confirm.ok')||'\u786E\u5B9A';
8152
+ okBtn.className='btn-confirm-ok'+(opts.danger?' danger':'');
8153
+ document.getElementById('confirmCancelBtn').textContent=opts.cancelText||t('confirm.cancel')||'\u53D6\u6D88';
8154
+ overlay.classList.add('show');
8155
+ });
8156
+ }
8157
+ function confirmModalClose(result){
8158
+ document.getElementById('confirmOverlay').classList.remove('show');
8159
+ if(_confirmResolve){var r=_confirmResolve;_confirmResolve=null;r(result);}
8160
+ }
8161
+
7186
8162
  /* ─── Theme ─── */
7187
8163
  const VIEWER_THEME_KEY='memos-viewer-theme';
7188
8164
  function initViewerTheme(){const s=localStorage.getItem(VIEWER_THEME_KEY);const theme=(s==='light'||s==='dark')?s:'dark';document.documentElement.setAttribute('data-theme',theme);}
@@ -7247,30 +8223,34 @@ async function checkForUpdate(){
7247
8223
  const pkgSpec=d.installCommand?d.installCommand.replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/,''):(d.packageName+'@'+d.latest);
7248
8224
  var banner=document.createElement('div');
7249
8225
  banner.id='updateBanner';
7250
- banner.style.cssText='display:flex;align-items:center;gap:10px;padding:12px 20px;font-size:13px;font-weight:500;border-radius:10px;margin:0 32px;animation:slideIn .3s ease;background:rgba(245,158,11,.1);color:#d97706;border:1px solid rgba(245,158,11,.25)';
8226
+ banner.style.cssText='display:flex;align-items:center;gap:12px;padding:10px max(32px,calc((100% - 1400px)/2 + 32px));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)';
7251
8227
  var textNode=document.createElement('div');
7252
- textNode.style.cssText='display:flex;align-items:center;gap:8px;flex-shrink:0';
7253
- textNode.innerHTML='\u{1F4E6} '+t('update.available')+' <b style="margin:0 2px">v'+esc(d.current)+'</b> \u2192 <b style="margin:0 2px">v'+esc(d.latest)+'</b>';
8228
+ textNode.style.cssText='display:flex;align-items:center;gap:8px;flex-shrink:0;font-size:13px';
8229
+ 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>';
7254
8230
  var btnUpdate=document.createElement('button');
7255
- btnUpdate.className='emb-banner-btn';
8231
+ btnUpdate.style.cssText='background:var(--pri);color:#fff;border:none;border-radius:8px;padding:5px 16px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;transition:opacity .15s,transform .1s;margin-left:4px';
7256
8232
  btnUpdate.textContent=t('update.btn');
8233
+ btnUpdate.onmouseenter=function(){this.style.opacity='.9';this.style.transform='scale(1.02)'};
8234
+ btnUpdate.onmouseleave=function(){this.style.opacity='1';this.style.transform='scale(1)'};
7257
8235
  var statusDiv=document.createElement('div');
7258
- statusDiv.style.cssText='font-size:11px;opacity:.8;flex-shrink:0';
8236
+ statusDiv.style.cssText='font-size:11px;opacity:.7;flex-shrink:0';
7259
8237
  btnUpdate.onclick=function(){doUpdateInstall(pkgSpec,btnUpdate,statusDiv)};
7260
8238
  textNode.appendChild(btnUpdate);
7261
8239
  var spacer=document.createElement('div');
7262
8240
  spacer.style.cssText='flex:1';
7263
8241
  var btnClose=document.createElement('button');
7264
- btnClose.className='emb-banner-close';
8242
+ btnClose.style.cssText='background:none;border:none;font-size:16px;color:var(--text-muted);cursor:pointer;opacity:.5;padding:0 2px;line-height:1;transition:opacity .15s';
7265
8243
  btnClose.innerHTML='&times;';
8244
+ btnClose.onmouseenter=function(){this.style.opacity='1'};
8245
+ btnClose.onmouseleave=function(){this.style.opacity='.5'};
7266
8246
  btnClose.onclick=function(){banner.remove()};
7267
8247
  banner.appendChild(textNode);
7268
8248
  banner.appendChild(statusDiv);
7269
8249
  banner.appendChild(spacer);
7270
8250
  banner.appendChild(btnClose);
7271
- var embBanner=document.querySelector('.emb-banner');
7272
- if(embBanner&&embBanner.parentNode){embBanner.parentNode.insertBefore(banner,embBanner);}
7273
- else{var ct=document.querySelector('.content-area')||document.querySelector('main')||document.body;if(ct.firstChild)ct.insertBefore(banner,ct.firstChild);else ct.appendChild(banner);}
8251
+ var tb=document.querySelector('.topbar');
8252
+ if(tb&&tb.parentNode){tb.parentNode.insertBefore(banner,tb);}
8253
+ else{document.body.insertBefore(banner,document.body.firstChild);}
7274
8254
  }catch(e){}
7275
8255
  }
7276
8256
 
@@ -7292,6 +8272,16 @@ checkAuth();
7292
8272
  </div>
7293
8273
  </div>
7294
8274
 
8275
+ <div class="confirm-overlay" id="confirmOverlay" onclick="confirmModalClose(false)">
8276
+ <div class="confirm-panel" onclick="event.stopPropagation()">
8277
+ <div class="confirm-panel-header" id="confirmTitle"></div>
8278
+ <div class="confirm-panel-body" id="confirmBody"></div>
8279
+ <div class="confirm-panel-footer">
8280
+ <button class="btn-confirm-cancel" id="confirmCancelBtn" onclick="confirmModalClose(false)"></button>
8281
+ <button class="btn-confirm-ok" id="confirmOkBtn" onclick="confirmModalClose(true)"></button>
8282
+ </div>
8283
+ </div>
8284
+ </div>
7295
8285
  </body>
7296
8286
  </html>`;
7297
8287
  }