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