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

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.
@@ -8,7 +8,8 @@ function viewerHTML(pluginVersion) {
8
8
  <head>
9
9
  <meta charset="UTF-8">
10
10
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
11
- <title>OpenClaw Memory - Powered by MemOS</title>
11
+ <link rel="icon" href="https://statics.memtensor.com.cn/logo/color-m.svg" type="image/svg+xml">
12
+ <title>MemOS 记忆</title>
12
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
13
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
15
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -61,8 +62,9 @@ function viewerHTML(pluginVersion) {
61
62
  [data-theme="light"] .analytics-card.amber .ac-value{color:#d97706}
62
63
  [data-theme="light"] .analytics-section{background:#fff;border-color:var(--border);box-shadow:0 1px 3px rgba(0,0,0,.04)}
63
64
  [data-theme="light"] .analytics-section::before{background:none}
64
- [data-theme="light"] .chart-bar{box-shadow:none}
65
- [data-theme="light"] .chart-bar:hover{box-shadow:0 2px 8px rgba(79,70,229,.15)}
65
+ [data-theme="light"] .chart-bar{box-shadow:0 1px 4px rgba(99,102,241,.12)}
66
+ [data-theme="light"] .chart-bar:hover{box-shadow:0 3px 12px rgba(79,70,229,.2)}
67
+ [data-theme="light"] .chart-bars::before{opacity:.3}
66
68
  [data-theme="light"] .tool-chart-tooltip{background:rgba(17,24,39,.92);color:#e8eaed;border-color:rgba(99,102,241,.3);box-shadow:0 8px 24px rgba(0,0,0,.2)}
67
69
  [data-theme="light"] .tool-chart-tooltip .tt-time{color:#a5b4fc}
68
70
  [data-theme="light"] .tool-chart-tooltip .tt-val{color:#e8eaed}
@@ -108,9 +110,15 @@ input,textarea,select{font-family:inherit;font-size:inherit}
108
110
  /* ─── App Layout (dark dashboard, same as www) ─── */
109
111
  .app{display:none;flex-direction:column;min-height:100vh}
110
112
  .topbar{background:rgba(11,13,17,.88);border-bottom:1px solid var(--border);padding:0 28px;height:56px;display:flex;align-items:center;position:sticky;top:0;z-index:100;backdrop-filter:blur(12px)}
111
- .topbar .brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em;flex-shrink:0}
112
- .topbar .brand .icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:22px;background:none;border-radius:0}
113
+ .topbar .brand{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em;flex-shrink:0}
114
+ .topbar .brand .icon{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:18px;background:none;border-radius:0}
115
+ .topbar .brand .brand-title{font-size:13px;font-weight:500;opacity:.7}
116
+ .topbar .brand .brand-col{display:flex;flex-direction:column;gap:0}
117
+ .topbar .brand .brand-powered{font-size:9px;font-weight:500;color:var(--text-sec);opacity:.55;letter-spacing:.02em;line-height:1}
113
118
  .topbar .brand .sub{font-weight:400;color:var(--text-muted);font-size:11px}
119
+ .memos-logo{display:inline-flex;align-items:center}
120
+ .memos-logo svg{height:28px;width:28px}
121
+ .brand-sep{width:1px;height:20px;background:var(--border);opacity:.5;margin:0 2px}
114
122
  .version-badge{font-size:10px;font-weight:600;color:var(--text-muted);background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.1);padding:1px 7px;border-radius:6px;margin-left:6px;letter-spacing:.02em;user-select:all}
115
123
  [data-theme="light"] .version-badge{background:rgba(0,0,0,.05);border-color:rgba(0,0,0,.08);color:var(--text-sec)}
116
124
  .topbar-center{flex:1;display:flex;justify-content:center}
@@ -289,20 +297,67 @@ input,textarea,select{font-family:inherit;font-size:inherit}
289
297
  .memory-card.dedup-inactive{opacity:.55;border-style:dashed}
290
298
  .memory-card.dedup-inactive:hover{opacity:.85}
291
299
  .dedup-target-link{font-size:11px;color:var(--pri);cursor:pointer;text-decoration:underline;margin-left:4px}
292
- .memory-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9999;display:none;align-items:center;justify-content:center;backdrop-filter:blur(4px)}
300
+ .memory-modal-overlay{position:fixed;inset:0;background:rgba(5,8,16,.75);z-index:9999;display:none;align-items:center;justify-content:center;backdrop-filter:blur(12px) saturate(1.2)}
293
301
  .memory-modal-overlay.show{display:flex}
294
- .memory-modal{background:var(--bg-card);border:1px solid var(--border);border-radius:16px;width:min(600px,90vw);max-height:80vh;display:flex;flex-direction:column;box-shadow:0 20px 60px rgba(0,0,0,.4);animation:modalIn .2s ease-out}
295
- @keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}
296
- .memory-modal-title{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);font-size:14px;font-weight:700}
297
- .memory-modal-body{padding:20px;overflow-y:auto;flex:1}
298
- .modal-memory-card{display:flex;flex-direction:column;gap:14px}
299
- .modal-header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
300
- .modal-field{display:flex;flex-direction:column;gap:4px}
301
- .modal-field-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec)}
302
- .modal-field-val{font-size:13px;color:var(--text);line-height:1.5}
303
- .modal-field-content{font-family:'SF Mono',Consolas,monospace;font-size:12px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.15);border-radius:8px;padding:12px;max-height:240px;overflow-y:auto;margin:0}
304
- [data-theme="light"] .modal-field-content{background:rgba(0,0,0,.04)}
305
- .modal-meta-row{display:flex;flex-wrap:wrap;gap:12px;font-size:11px;color:var(--text-sec);padding:8px 0;border-top:1px dashed var(--border)}
302
+ [data-theme="light"] .memory-modal-overlay{background:rgba(0,0,0,.45)}
303
+ .memory-modal{position:relative;background:linear-gradient(160deg,#0d1117 0%,#161b22 50%,#0d1117 100%);border:1px solid rgba(99,102,241,.2);border-radius:20px;width:min(600px,92vw);max-height:82vh;display:flex;flex-direction:column;box-shadow:0 32px 100px rgba(0,0,0,.6),0 0 60px rgba(99,102,241,.08),inset 0 1px 0 rgba(255,255,255,.06);animation:mmSlideIn .35s cubic-bezier(.22,1,.36,1);overflow:hidden}
304
+ [data-theme="light"] .memory-modal{background:linear-gradient(160deg,#ffffff 0%,#f8f9fc 50%,#ffffff 100%);border-color:rgba(99,102,241,.15);box-shadow:0 32px 100px rgba(0,0,0,.12),0 0 40px rgba(99,102,241,.06)}
305
+ @keyframes mmSlideIn{from{opacity:0;transform:scale(.92) translateY(20px)}to{opacity:1;transform:scale(1) translateY(0)}}
306
+ .memory-modal::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,#6366f1,#8b5cf6,#ec4899,#f43f5e,#6366f1);background-size:200% 100%;animation:mmGradientMove 3s linear infinite;border-radius:20px 20px 0 0;z-index:1}
307
+ @keyframes mmGradientMove{0%{background-position:0% 50%}100%{background-position:200% 50%}}
308
+ .memory-modal::after{content:'';position:absolute;inset:0;background:radial-gradient(ellipse at 20% 0%,rgba(99,102,241,.06) 0%,transparent 60%),radial-gradient(ellipse at 80% 100%,rgba(236,72,153,.04) 0%,transparent 60%);pointer-events:none;border-radius:20px}
309
+ [data-theme="light"] .memory-modal::after{background:radial-gradient(ellipse at 20% 0%,rgba(99,102,241,.04) 0%,transparent 60%),radial-gradient(ellipse at 80% 100%,rgba(236,72,153,.03) 0%,transparent 60%)}
310
+ .memory-modal-title{position:relative;z-index:2;display:flex;align-items:center;justify-content:space-between;padding:18px 24px 14px;font-size:12px;font-weight:700;color:var(--text-sec);letter-spacing:.06em;text-transform:uppercase}
311
+ .memory-modal-title .mm-tl{display:flex;align-items:center;gap:8px}
312
+ .memory-modal-title .mm-tl-icon{width:28px;height:28px;border-radius:8px;background:linear-gradient(135deg,rgba(99,102,241,.15),rgba(139,92,246,.1));display:flex;align-items:center;justify-content:center;font-size:14px;border:1px solid rgba(99,102,241,.2)}
313
+ [data-theme="light"] .memory-modal-title .mm-tl-icon{background:linear-gradient(135deg,rgba(99,102,241,.1),rgba(139,92,246,.06));border-color:rgba(99,102,241,.12)}
314
+ .memory-modal-title .mm-close{width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:10px;border:1px solid rgba(255,255,255,.06);background:rgba(255,255,255,.04);color:var(--text-sec);cursor:pointer;font-size:16px;transition:all .2s}
315
+ .memory-modal-title .mm-close:hover{background:rgba(255,77,77,.12);border-color:rgba(255,77,77,.2);color:#f87171;transform:rotate(90deg)}
316
+ [data-theme="light"] .memory-modal-title .mm-close{border-color:rgba(0,0,0,.06);background:rgba(0,0,0,.03)}
317
+ [data-theme="light"] .memory-modal-title .mm-close:hover{background:rgba(255,77,77,.08);border-color:rgba(255,77,77,.15)}
318
+ .memory-modal-body{position:relative;z-index:2;padding:0;overflow-y:auto;flex:1}
319
+ .mm-hero{padding:0 24px 20px}
320
+ .mm-hero-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:14px}
321
+ .mm-role-chip{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;padding:4px 12px;border-radius:20px;background:linear-gradient(135deg,rgba(99,102,241,.15),rgba(139,92,246,.1));color:#a5b4fc;border:1px solid rgba(99,102,241,.2)}
322
+ .mm-role-chip.user{background:linear-gradient(135deg,rgba(16,185,129,.15),rgba(52,211,153,.1));color:#6ee7b7;border-color:rgba(16,185,129,.2)}
323
+ .mm-role-chip.assistant{background:linear-gradient(135deg,rgba(99,102,241,.15),rgba(139,92,246,.1));color:#a5b4fc;border-color:rgba(99,102,241,.2)}
324
+ .mm-role-chip.system{background:linear-gradient(135deg,rgba(251,191,36,.15),rgba(245,158,11,.1));color:#fcd34d;border-color:rgba(251,191,36,.2)}
325
+ [data-theme="light"] .mm-role-chip{color:#6366f1}
326
+ [data-theme="light"] .mm-role-chip.user{color:#059669}
327
+ [data-theme="light"] .mm-role-chip.assistant{color:#6366f1}
328
+ [data-theme="light"] .mm-role-chip.system{color:#d97706}
329
+ .mm-dedup-chip{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:3px 10px;border-radius:20px;text-transform:uppercase;letter-spacing:.04em}
330
+ .mm-dedup-chip.duplicate{background:rgba(239,68,68,.12);color:#fca5a5;border:1px solid rgba(239,68,68,.2)}
331
+ .mm-dedup-chip.merged{background:rgba(251,191,36,.12);color:#fcd34d;border:1px solid rgba(251,191,36,.2)}
332
+ [data-theme="light"] .mm-dedup-chip.duplicate{color:#dc2626}
333
+ [data-theme="light"] .mm-dedup-chip.merged{color:#d97706}
334
+ .mm-id{font-family:'SF Mono',Consolas,'Courier New',monospace;font-size:10px;color:var(--text-muted);background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);padding:3px 10px;border-radius:20px;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:4px}
335
+ .mm-id::before{content:'\u{1F517}';font-size:9px}
336
+ .mm-id:hover{background:rgba(99,102,241,.12);border-color:rgba(99,102,241,.25);color:var(--text)}
337
+ [data-theme="light"] .mm-id{background:rgba(0,0,0,.03);border-color:rgba(0,0,0,.06)}
338
+ [data-theme="light"] .mm-id:hover{background:rgba(99,102,241,.08);border-color:rgba(99,102,241,.15)}
339
+ .mm-summary{font-size:15px;font-weight:600;color:var(--text);line-height:1.6;letter-spacing:-.01em;padding:16px 20px;background:rgba(255,255,255,.02);border-radius:12px;border:1px solid rgba(255,255,255,.04);position:relative}
340
+ .mm-summary::before{content:'';position:absolute;left:0;top:12px;bottom:12px;width:3px;border-radius:2px;background:linear-gradient(180deg,#6366f1,#8b5cf6)}
341
+ [data-theme="light"] .mm-summary{background:rgba(99,102,241,.02);border-color:rgba(99,102,241,.06)}
342
+ .mm-section{padding:0 24px 16px}
343
+ .mm-section-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
344
+ .mm-section-label::before{content:'';width:6px;height:6px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#8b5cf6);flex-shrink:0}
345
+ .mm-content{font-family:'SF Mono',Consolas,'Courier New',monospace;font-size:12px;line-height:1.75;color:var(--text);white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.3);border-radius:12px;padding:16px 18px;max-height:240px;overflow-y:auto;margin:0;border:1px solid rgba(255,255,255,.04);position:relative}
346
+ [data-theme="light"] .mm-content{background:rgba(0,0,0,.03);border-color:rgba(0,0,0,.06)}
347
+ .mm-meta{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px;padding:16px 24px;border-top:1px solid rgba(255,255,255,.04);background:rgba(0,0,0,.1)}
348
+ [data-theme="light"] .mm-meta{border-top-color:rgba(0,0,0,.04);background:rgba(0,0,0,.015)}
349
+ .mm-meta-chip{display:flex;flex-direction:column;gap:3px;font-size:11px;color:var(--text-sec);background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.04);padding:10px 12px;border-radius:10px;transition:all .2s}
350
+ .mm-meta-chip:hover{background:rgba(255,255,255,.06);border-color:rgba(99,102,241,.15)}
351
+ [data-theme="light"] .mm-meta-chip{background:rgba(0,0,0,.02);border-color:rgba(0,0,0,.04)}
352
+ [data-theme="light"] .mm-meta-chip:hover{background:rgba(99,102,241,.04);border-color:rgba(99,102,241,.1)}
353
+ .mm-meta-chip strong{color:var(--text-muted);font-weight:600;font-size:9px;text-transform:uppercase;letter-spacing:.06em}
354
+ .mm-meta-chip span{color:var(--text);font-weight:500;font-size:12px;word-break:break-all}
355
+ .mm-dedup{padding:0 24px 16px}
356
+ .mm-dedup-box{background:linear-gradient(135deg,rgba(251,191,36,.06),rgba(245,158,11,.03));border:1px solid rgba(251,191,36,.12);border-radius:12px;padding:14px 16px;font-size:12px;color:var(--text-sec);line-height:1.6;display:flex;align-items:flex-start;gap:8px}
357
+ .mm-dedup-box::before{content:'\u26A0\uFE0F';flex-shrink:0;font-size:14px}
358
+ .mm-footer{padding:12px 24px 16px;display:flex;align-items:center;justify-content:center}
359
+ .mm-footer .dedup-target-link{font-size:11px;color:#818cf8;cursor:pointer;padding:6px 16px;border-radius:8px;border:1px solid rgba(99,102,241,.2);background:rgba(99,102,241,.06);transition:all .2s;font-weight:500}
360
+ .mm-footer .dedup-target-link:hover{background:rgba(99,102,241,.12);border-color:rgba(99,102,241,.3)}
306
361
  [data-theme="light"] .merge-history{background:rgba(0,0,0,.04)}
307
362
  [data-theme="light"] .merge-history-item{border-bottom-color:rgba(0,0,0,.06)}
308
363
  .card-merged-info{margin-top:8px;padding:8px 12px;background:rgba(16,185,129,.06);border:1px dashed rgba(16,185,129,.2);border-radius:8px;font-size:12px;line-height:1.6;color:var(--text-sec)}
@@ -742,8 +797,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
742
797
  .test-result.ok{color:#22c55e}
743
798
  .test-result.fail{color:var(--rose)}
744
799
  .test-result.loading{color:var(--text-muted)}
745
- .settings-actions{display:flex;gap:12px;justify-content:flex-end;align-items:center;margin-top:16px;padding-top:16px;border-top:1px solid var(--border)}
746
- .settings-actions .btn{min-width:110px;padding:10px 20px;font-size:13px}
800
+ .settings-actions{display:flex;gap:12px;justify-content:flex-end;align-items:center;margin-top:20px;padding-top:16px;border-top:1px solid var(--border);flex-wrap:nowrap}
801
+ .settings-actions .btn{flex:0 0 auto;min-width:0;padding:8px 24px;font-size:13px}
747
802
  .settings-actions .btn-primary{background:rgba(99,102,241,.08);color:var(--pri);border:1px solid rgba(99,102,241,.25);font-weight:600}
748
803
  .settings-actions .btn-primary:hover{background:rgba(99,102,241,.14);border-color:var(--pri)}
749
804
  [data-theme="light"] .settings-actions .btn-primary{background:rgba(79,70,229,.06);color:#4f46e5;border:1px solid rgba(79,70,229,.2)}
@@ -809,18 +864,20 @@ input,textarea,select{font-family:inherit;font-size:inherit}
809
864
  .analytics-section::before{display:none}
810
865
  .analytics-section h3{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:16px;display:flex;align-items:center;gap:8px}
811
866
  .analytics-section h3 .icon{font-size:14px;opacity:.6}
812
- .chart-bars{display:flex;align-items:flex-end;gap:4px;padding:8px 0;overflow-x:auto;justify-content:center}
813
- .chart-bar-wrap{flex:1;min-width:28px;max-width:80px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative}
814
- .chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}
815
- .chart-bar-wrap:hover .chart-bar{opacity:1}
867
+ .chart-bars{display:flex;align-items:flex-end;gap:1px;padding:12px 4px 4px;min-height:200px;position:relative;width:100%}
868
+ .chart-bars::before{content:'';position:absolute;left:0;right:0;bottom:24px;height:1px;background:var(--border);opacity:.4}
869
+ .chart-bar-wrap{flex:1 1 0;min-width:0;max-width:32px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative;cursor:pointer}
870
+ .chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:center}
871
+ .chart-bar-wrap:hover .chart-bar{opacity:1;filter:brightness(1.2);transform:scaleY(1.02);transform-origin:bottom}
816
872
  .chart-bar-wrap:hover .chart-bar-label{color:var(--text)}
817
873
  .chart-bar-wrap:hover .chart-tip{opacity:1;transform:translateX(-50%) translateY(0)}
818
- .chart-tip{position:absolute;top:-6px;left:50%;transform:translateX(-50%) translateY(4px);background:var(--bg-card);border:1px solid var(--border-glow);color:var(--text);padding:2px 8px;border-radius:6px;font-size:10px;font-weight:600;white-space:nowrap;z-index:5;pointer-events:none;box-shadow:var(--shadow);opacity:0;transition:all .15s ease}
819
- .chart-bar{width:100%;border-radius:3px 3px 1px 1px;background:#818cf8;opacity:.75;transition:all .2s ease}
820
- .chart-bar.violet{background:#6366f1}
821
- .chart-bar.green{background:var(--green)}
822
- .chart-bar.zero{background:var(--border);opacity:.3;border-radius:2px}
823
- .chart-bar-label{font-size:9px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;text-align:center;transition:color .15s}
874
+ .chart-tip{position:absolute;top:-8px;left:50%;transform:translateX(-50%) translateY(6px);background:rgba(15,18,25,.95);border:1px solid rgba(99,102,241,.3);color:#e8eaed;padding:3px 10px;border-radius:8px;font-size:10px;font-weight:600;white-space:nowrap;z-index:5;pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.3);opacity:0;transition:all .2s cubic-bezier(.22,1,.36,1)}
875
+ [data-theme="light"] .chart-tip{background:rgba(17,24,39,.9)}
876
+ .chart-bar{width:100%;max-width:20px;min-width:4px;border-radius:3px 3px 1px 1px;background:linear-gradient(180deg,#818cf8 0%,#6366f1 100%);opacity:.85;transition:all .25s cubic-bezier(.22,1,.36,1);box-shadow:0 1px 4px rgba(99,102,241,.12)}
877
+ .chart-bar.violet{background:linear-gradient(180deg,#8b5cf6 0%,#6366f1 100%)}
878
+ .chart-bar.green{background:linear-gradient(180deg,#34d399 0%,#10b981 100%);box-shadow:0 1px 4px rgba(16,185,129,.12)}
879
+ .chart-bar.zero{width:100%;max-width:20px;min-width:4px;height:2px!important;background:var(--border);opacity:.25;border-radius:1px;box-shadow:none}
880
+ .chart-bar-label{font-size:8px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40px;text-align:center;transition:color .15s;letter-spacing:0}
824
881
  .chart-legend{display:flex;gap:14px;margin-top:12px;flex-wrap:wrap;font-size:11px;color:var(--text-sec);font-weight:500}
825
882
  .chart-legend span{display:inline-flex;align-items:center;gap:5px}
826
883
  .chart-legend .dot{width:8px;height:8px;border-radius:2px}
@@ -874,7 +931,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
874
931
  [data-theme="light"] .auth-theme-toggle .theme-icon-dark{display:none}
875
932
 
876
933
  @media(max-width:1100px){.analytics-cards{grid-template-columns:repeat(3,1fr)}}
877
- @media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}.analytics-cards{grid-template-columns:repeat(2,1fr)}.topbar{padding:0 16px;gap:8px}.topbar .brand span{display:none}.topbar-center{justify-content:flex-start}}
934
+ @media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}.analytics-cards{grid-template-columns:repeat(2,1fr)}.topbar{padding:0 16px;gap:8px}.topbar .brand .brand-title{display:none}.topbar .brand .brand-powered{display:none}.topbar-center{justify-content:flex-start}}
878
935
  </style>
879
936
  </head>
880
937
  <body>
@@ -886,7 +943,9 @@ input,textarea,select{font-family:inherit;font-size:inherit}
886
943
  <button class="auth-theme-toggle" onclick="toggleLang()" aria-label="Switch language"><span data-i18n="lang.switch">EN</span></button>
887
944
  </div>
888
945
  <div class="auth-card">
889
- <div class="logo"><svg width="60" height="60" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="aLG" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff4d4d"/><stop offset="100%" stop-color="#991b1b"/></linearGradient></defs><path d="M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z" fill="url(#aLG)"/><path d="M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z" fill="url(#aLG)"/><path d="M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z" fill="url(#aLG)"/><path d="M45 15Q35 5 30 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><path d="M75 15Q85 5 90 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><circle cx="45" cy="35" r="6" fill="#050810"/><circle cx="75" cy="35" r="6" fill="#050810"/><circle cx="46" cy="34" r="2" fill="#00e5cc"/><circle cx="76" cy="34" r="2" fill="#00e5cc"/></svg></div>
946
+ <div class="logo" style="display:flex;flex-direction:column;align-items:center;gap:10px">
947
+ <svg width="56" height="56" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="aLG" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff4d4d"/><stop offset="100%" stop-color="#991b1b"/></linearGradient></defs><path d="M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z" fill="url(#aLG)"/><path d="M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z" fill="url(#aLG)"/><path d="M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z" fill="url(#aLG)"/><path d="M45 15Q35 5 30 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><path d="M75 15Q85 5 90 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><circle cx="45" cy="35" r="6" fill="#050810"/><circle cx="75" cy="35" r="6" fill="#050810"/><circle cx="46" cy="34" r="2" fill="#00e5cc"/><circle cx="76" cy="34" r="2" fill="#00e5cc"/></svg>
948
+ </div>
890
949
  <h1 data-i18n="title">OpenClaw Memory</h1>
891
950
  <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px" data-i18n="subtitle">Powered by MemOS</p>
892
951
  <p data-i18n="setup.desc">Set a password to protect your memories</p>
@@ -904,7 +963,9 @@ input,textarea,select{font-family:inherit;font-size:inherit}
904
963
  <button class="auth-theme-toggle" onclick="toggleLang()" aria-label="Switch language"><span data-i18n="lang.switch">EN</span></button>
905
964
  </div>
906
965
  <div class="auth-card">
907
- <div class="logo"><svg width="60" height="60" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="bLG" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff4d4d"/><stop offset="100%" stop-color="#991b1b"/></linearGradient></defs><path d="M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z" fill="url(#bLG)"/><path d="M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z" fill="url(#bLG)"/><path d="M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z" fill="url(#bLG)"/><path d="M45 15Q35 5 30 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><path d="M75 15Q85 5 90 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><circle cx="45" cy="35" r="6" fill="#050810"/><circle cx="75" cy="35" r="6" fill="#050810"/><circle cx="46" cy="34" r="2" fill="#00e5cc"/><circle cx="76" cy="34" r="2" fill="#00e5cc"/></svg></div>
966
+ <div class="logo" style="display:flex;flex-direction:column;align-items:center;gap:10px">
967
+ <svg width="56" height="56" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="bLG" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff4d4d"/><stop offset="100%" stop-color="#991b1b"/></linearGradient></defs><path d="M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z" fill="url(#bLG)"/><path d="M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z" fill="url(#bLG)"/><path d="M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z" fill="url(#bLG)"/><path d="M45 15Q35 5 30 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><path d="M75 15Q85 5 90 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><circle cx="45" cy="35" r="6" fill="#050810"/><circle cx="75" cy="35" r="6" fill="#050810"/><circle cx="46" cy="34" r="2" fill="#00e5cc"/><circle cx="76" cy="34" r="2" fill="#00e5cc"/></svg>
968
+ </div>
908
969
  <h1 data-i18n="title">OpenClaw Memory</h1>
909
970
  <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px" data-i18n="subtitle">Powered by MemOS</p>
910
971
  <p data-i18n="login.desc">Enter your password to access memories</p>
@@ -956,8 +1017,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
956
1017
  <div class="app" id="app">
957
1018
  <div class="topbar">
958
1019
  <div class="brand">
959
- <div class="icon"><svg width="24" height="24" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg" style="filter:drop-shadow(0 0 8px rgba(255,77,77,.3))"><defs><linearGradient id="tLG" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff4d4d"/><stop offset="100%" stop-color="#991b1b"/></linearGradient></defs><path d="M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z" fill="url(#tLG)"/><path d="M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z" fill="url(#tLG)"/><path d="M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z" fill="url(#tLG)"/><path d="M45 15Q35 5 30 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><path d="M75 15Q85 5 90 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><circle cx="45" cy="35" r="6" fill="#050810"/><circle cx="75" cy="35" r="6" fill="#050810"/><circle cx="46" cy="34" r="2" fill="#00e5cc"/><circle cx="76" cy="34" r="2" fill="#00e5cc"/></svg></div>
960
- <span data-i18n="title">OpenClaw Memory</span>${vBadge}
1020
+ <span class="memos-logo"><svg width="28" height="28" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="topLG" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#ff4d4d"/><stop offset="100%" stop-color="#991b1b"/></linearGradient></defs><path d="M60 10C30 10 15 35 15 55C15 75 30 95 45 100L45 110L55 110L55 100C55 100 60 102 65 100L65 110L75 110L75 100C90 95 105 75 105 55C105 35 90 10 60 10Z" fill="url(#topLG)"/><path d="M20 45C5 40 0 50 5 60C10 70 20 65 25 55C28 48 25 45 20 45Z" fill="url(#topLG)"/><path d="M100 45C115 40 120 50 115 60C110 70 100 65 95 55C92 48 95 45 100 45Z" fill="url(#topLG)"/><path d="M45 15Q35 5 30 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><path d="M75 15Q85 5 90 8" stroke="#ff4d4d" stroke-width="2" stroke-linecap="round"/><circle cx="45" cy="35" r="6" fill="#050810"/><circle cx="75" cy="35" r="6" fill="#050810"/><circle cx="46" cy="34" r="2" fill="#00e5cc"/><circle cx="76" cy="34" r="2" fill="#00e5cc"/></svg></span>
1021
+ <div class="brand-col"><span data-i18n="title" class="brand-title">MemOS</span><span data-i18n="subtitle" class="brand-powered">Powered by MemOS</span></div>${vBadge}
961
1022
  </div>
962
1023
  <div class="topbar-center">
963
1024
  <nav class="nav-tabs">
@@ -1009,9 +1070,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1009
1070
  <option value="public" data-i18n="filter.public">Public</option>
1010
1071
  </select>
1011
1072
  <select id="memorySearchScope" class="filter-select" onchange="onMemoryScopeChange()">
1012
- <option value="local">Local</option>
1013
- <option value="group">Group</option>
1014
- <option value="all">All</option>
1073
+ <option value="local" data-i18n="scope.local">Local</option>
1074
+ <option value="all" data-i18n="scope.hub">Hub</option>
1015
1075
  </select>
1016
1076
  </div>
1017
1077
  <div class="search-meta" id="searchMeta"></div>
@@ -1054,9 +1114,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1054
1114
  <button class="filter-chip" data-task-status="completed" onclick="setTaskStatusFilter(this,'completed')" data-i18n="tasks.status.completed">Completed</button>
1055
1115
  <button class="filter-chip" data-task-status="skipped" onclick="setTaskStatusFilter(this,'skipped')" data-i18n="tasks.status.skipped">Skipped</button>
1056
1116
  <select id="taskSearchScope" class="scope-select" onchange="onTaskScopeChange()">
1057
- <option value="local">Local</option>
1058
- <option value="group">Group</option>
1059
- <option value="all">All</option>
1117
+ <option value="local" data-i18n="scope.local">Local</option>
1118
+ <option value="all" data-i18n="scope.hub">Hub</option>
1060
1119
  </select>
1061
1120
  <button class="btn btn-sm btn-ghost" onclick="loadTasks()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
1062
1121
  </div>
@@ -1098,8 +1157,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1098
1157
  <input type="text" id="skillSearchInput" placeholder="Search skills..." data-i18n-ph="skills.search.placeholder" oninput="debounceSkillSearch()">
1099
1158
  <select id="skillSearchScope" class="scope-select" onchange="onSkillScopeChange()">
1100
1159
  <option value="local" data-i18n="scope.local">Local</option>
1101
- <option value="group" data-i18n="scope.group">Group</option>
1102
- <option value="all" data-i18n="scope.all">All</option>
1160
+ <option value="all" data-i18n="scope.hub">Hub</option>
1103
1161
  </select>
1104
1162
  </div>
1105
1163
  <div class="search-meta" id="skillSearchMeta"></div>
@@ -1414,16 +1472,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1414
1472
  <button class="btn btn-sm" id="btnRoleHub" onclick="selectSharingRole('hub')" data-i18n="settings.hub.role.hub">Hub (Server)</button>
1415
1473
  <button class="btn btn-sm" id="btnRoleClient" onclick="selectSharingRole('client')" data-i18n="settings.hub.role.client">Client (Connect to Hub)</button>
1416
1474
  </div>
1417
- <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.</div>
1475
+ <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>
1418
1476
  </div>
1419
1477
 
1420
1478
  <div id="hubModeConfig" style="display:none">
1421
- <div style="background:rgba(52,199,89,.08);border:1px solid rgba(52,199,89,.2);border-radius:10px;padding:14px 18px;margin-bottom:14px;font-size:12px;line-height:1.7;color:var(--text-sec)">
1422
- <div style="font-weight:700;color:var(--text);margin-bottom:4px" data-i18n="settings.hub.hubSteps.title">Quick Setup (3 steps)</div>
1423
- <div><span style="color:var(--accent)">1.</span> <span data-i18n="settings.hub.hubSteps.s1">Fill in Team Name below (or keep default)</span></div>
1424
- <div><span style="color:var(--accent)">2.</span> <span data-i18n="settings.hub.hubSteps.s2">Click "Save Settings", then restart OpenClaw gateway</span></div>
1425
- <div><span style="color:var(--accent)">3.</span> <span data-i18n="settings.hub.hubSteps.s3">Share the Hub Address and Team Token below with your team members</span></div>
1426
- </div>
1479
+ <input type="hidden" id="cfgHubTeamToken" value="">
1427
1480
  <div class="settings-grid">
1428
1481
  <div class="settings-field">
1429
1482
  <label data-i18n="settings.hub.port">Hub Port</label>
@@ -1435,15 +1488,13 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1435
1488
  <input type="text" id="cfgHubTeamName" placeholder="My Team">
1436
1489
  <div class="field-hint" data-i18n="settings.hub.teamName.hint">Your team display name</div>
1437
1490
  </div>
1438
- <div class="settings-field full-width">
1439
- <label data-i18n="settings.hub.teamToken">Team Token</label>
1440
- <input type="text" id="cfgHubTeamToken" readonly style="background:var(--bg);cursor:pointer" onclick="this.select();document.execCommand('copy');toast(t('settings.hub.tokenCopied'),'success')">
1441
- <div class="field-hint" data-i18n="settings.hub.teamToken.hint">Auto-generated secret for clients to join. Click to copy. Share this with your team members.</div>
1442
- </div>
1443
1491
  </div>
1444
- <div id="hubShareInfo" class="hub-info-card hic-share" style="display:none;margin-top:14px">
1445
- <div class="hic-title"><span class="hic-icon">\u{1F4CB}</span> <span data-i18n="settings.hub.shareInfo.title">Share this info with your team members:</span></div>
1446
- <div id="hubShareInfoContent" class="hic-grid"></div>
1492
+ <div id="hubShareInfo" style="display:none;margin-top:14px;background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px">
1493
+ <div style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;margin-bottom:10px" data-i18n="settings.hub.shareInfo.title">Share this info with your team members:</div>
1494
+ <div id="hubShareInfoContent" style="display:grid;grid-template-columns:auto 1fr;gap:6px 12px;font-size:12px;align-items:center"></div>
1495
+ </div>
1496
+ <div style="margin-top:16px;display:flex;align-items:center;gap:12px">
1497
+ <button class="btn btn-sm btn-primary" onclick="switchView('admin')" id="hubAdminEntryBtn" style="display:none" data-i18n="sharing.openAdmin">\u{1F6E1} Open Admin Panel</button>
1447
1498
  </div>
1448
1499
  </div>
1449
1500
 
@@ -1465,11 +1516,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1465
1516
  <input type="text" id="cfgClientTeamToken" placeholder="">
1466
1517
  <div class="field-hint" data-i18n="settings.hub.teamTokenClient.hint">Get this from your Hub admin to join the team</div>
1467
1518
  </div>
1468
- <div class="settings-field">
1469
- <label data-i18n="settings.hub.userToken">User Token</label>
1470
- <input type="text" id="cfgClientUserToken" placeholder="">
1471
- <div class="field-hint" data-i18n="settings.hub.userToken.hint">Usually auto-obtained after joining. Leave blank in most cases.</div>
1472
- </div>
1519
+ <input type="hidden" id="cfgClientUserToken" value="">
1473
1520
  </div>
1474
1521
  <div style="margin-top:12px">
1475
1522
  <button class="btn btn-sm btn-primary" id="btnTestHubConn" onclick="testHubConnection()" data-i18n="settings.hub.testConnection">Test Connection</button>
@@ -1538,13 +1585,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1538
1585
  </div>
1539
1586
  <div class="admin-tabs" id="adminTabsBar">
1540
1587
  <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>
1541
- <button class="admin-tab" onclick="switchAdminTab('groups',this)"><span class="at-icon">\u{1F4C2}</span> <span data-i18n="admin.tab.groups">Groups</span> <span class="at-count" id="adminTabCountGroups">0</span></button>
1542
1588
  <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>
1543
1589
  <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>
1544
1590
  <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>
1545
1591
  </div>
1546
1592
  <div class="admin-panel active" id="adminUsersPanel"></div>
1547
- <div class="admin-panel" id="adminGroupsPanel"></div>
1548
1593
  <div class="admin-panel" id="adminSharedMemoriesPanel"></div>
1549
1594
  <div class="admin-panel" id="adminMemoriesPanel"></div>
1550
1595
  <div class="admin-panel" id="adminSkillsPanel"></div>
@@ -1749,7 +1794,7 @@ let _embeddingWarningShown=false;
1749
1794
  /* ─── i18n ─── */
1750
1795
  const I18N={
1751
1796
  en:{
1752
- 'title':'OpenClaw Memory',
1797
+ 'title':'MemOS',
1753
1798
  'subtitle':'Powered by MemOS',
1754
1799
  'setup.desc':'Set a password to protect your memories',
1755
1800
  'setup.pw':'Enter a password (4+ characters)',
@@ -2069,7 +2114,7 @@ const I18N={
2069
2114
  'settings.hub.role':'Role',
2070
2115
  'settings.hub.role.hub':'Hub (Server)',
2071
2116
  'settings.hub.role.client':'Client (Connect to Hub)',
2072
- 'settings.hub.role.hint':'Hub = run the server; Client = connect to one.',
2117
+ 'settings.hub.role.hint':'Hub = run the server; Client = connect to one. These two modes are mutually exclusive.',
2073
2118
  'settings.hub.port':'Hub Port',
2074
2119
  'settings.hub.port.hint':'Port for Hub service. Default: 18800',
2075
2120
  'settings.hub.teamName':'Team Name',
@@ -2106,13 +2151,31 @@ const I18N={
2106
2151
  'sidebar.hub':'\u{1F310} Team Sharing',
2107
2152
  'sharing.sidebar.connected':'Connected',
2108
2153
  'sharing.sidebar.disconnected':'Disconnected',
2154
+ 'sharing.sidebar.pending':'Pending Approval',
2155
+ 'sharing.sidebar.rejected':'Rejected',
2109
2156
  'sharing.sidebar.starting':'Starting...',
2110
2157
  'sharing.sidebar.notConfigured':'Not configured',
2111
2158
  'sharing.sidebar.identity':'Identity:',
2112
2159
  'sharing.sidebar.admin':'Admin',
2113
2160
  'sharing.sidebar.targetHub':'Target Hub:',
2161
+ 'sharing.pendingApproval.hint':'Your join request has been submitted. Please wait for the Hub admin to approve.',
2162
+ 'sharing.rejected.hint':'Your join request was rejected by the Hub admin. Please contact the admin or retry.',
2163
+ 'sharing.retryJoin':'Retry Join',
2164
+ 'sharing.retryJoin.hint':'Clears local data and re-submits the join request',
2165
+ 'sharing.retryJoin.confirm':'This will clear your current connection and re-submit a join request. Continue?',
2166
+ 'sharing.retryJoin.success':'Join request re-submitted. Waiting for admin approval.',
2167
+ 'sharing.retryJoin.fail':'Failed to retry join',
2168
+ 'sharing.cannotJoinSelf':'Cannot join your own Hub. Please enter a remote Hub address.',
2169
+ 'scope.hub':'Hub',
2170
+ 'memory.detail.title':'Memory Detail',
2171
+ 'memory.detail.loading':'Loading...',
2172
+ 'memory.detail.notFound':'Memory not found',
2173
+ 'memory.detail.copyId':'Click to copy ID',
2174
+ 'memory.detail.created':'Created ',
2175
+ 'memory.detail.updated':'Updated ',
2176
+ 'memory.detail.viewTarget':'View target: ',
2114
2177
  'admin.title':'Hub Admin Panel',
2115
- 'admin.subtitle':'Manage team members, groups, and shared resources',
2178
+ 'admin.subtitle':'Manage team members and shared resources',
2116
2179
  'admin.refresh':'\u21BB Refresh',
2117
2180
  'admin.tab.users':'Users',
2118
2181
  'admin.tab.groups':'Groups',
@@ -2158,6 +2221,7 @@ const I18N={
2158
2221
  'admin.membersCount':'Members ({n}):',
2159
2222
  'admin.noMembersYet':'No members yet.',
2160
2223
  'admin.loadFailed':'Failed to load admin data: ',
2224
+ 'admin.noPermission':'You do not have admin permissions to access this panel.',
2161
2225
  'admin.groupsFailed':'Failed to load groups: ',
2162
2226
  'toast.userApproved':'User approved',
2163
2227
  'toast.userRejected':'User rejected',
@@ -2286,7 +2350,7 @@ const I18N={
2286
2350
  'update.dismiss':'Dismiss'
2287
2351
  },
2288
2352
  zh:{
2289
- 'title':'OpenClaw 记忆',
2353
+ 'title':'MemOS 记忆',
2290
2354
  'subtitle':'由 MemOS 驱动',
2291
2355
  'setup.desc':'设置密码以保护你的记忆数据',
2292
2356
  'setup.pw':'输入密码(至少4位)',
@@ -2606,7 +2670,7 @@ const I18N={
2606
2670
  'settings.hub.role':'角色',
2607
2671
  'settings.hub.role.hub':'Hub(服务端)',
2608
2672
  'settings.hub.role.client':'Client(连接到 Hub)',
2609
- 'settings.hub.role.hint':'Hub = 本机做服务端;Client = 连接别人的服务端。',
2673
+ 'settings.hub.role.hint':'Hub = 本机做服务端;Client = 连接别人的服务端。两种模式互斥,不能同时使用。',
2610
2674
  'settings.hub.port':'Hub 端口',
2611
2675
  'settings.hub.port.hint':'Hub 服务端口,默认 18800',
2612
2676
  'settings.hub.teamName':'团队名称',
@@ -2643,13 +2707,31 @@ const I18N={
2643
2707
  'sidebar.hub':'\u{1F310} 团队共享',
2644
2708
  'sharing.sidebar.connected':'已连接',
2645
2709
  'sharing.sidebar.disconnected':'已断开',
2710
+ 'sharing.sidebar.pending':'等待审核',
2711
+ 'sharing.sidebar.rejected':'已拒绝',
2646
2712
  'sharing.sidebar.starting':'启动中...',
2647
2713
  'sharing.sidebar.notConfigured':'未配置',
2648
2714
  'sharing.sidebar.identity':'身份:',
2649
2715
  'sharing.sidebar.admin':'管理员',
2650
2716
  'sharing.sidebar.targetHub':'目标 Hub:',
2717
+ 'sharing.pendingApproval.hint':'加入申请已提交,请等待 Hub 管理员审核通过。',
2718
+ 'sharing.rejected.hint':'您的加入申请已被 Hub 管理员拒绝,请联系管理员或重新申请。',
2719
+ 'sharing.retryJoin':'重新申请',
2720
+ 'sharing.retryJoin.hint':'清除本地连接数据并重新提交加入申请',
2721
+ 'sharing.retryJoin.confirm':'这将清除当前连接数据并重新提交加入申请,是否继续?',
2722
+ 'sharing.retryJoin.success':'加入申请已重新提交,请等待管理员审核。',
2723
+ 'sharing.retryJoin.fail':'重新申请失败',
2724
+ 'sharing.cannotJoinSelf':'不能加入自己的 Hub,请输入远程 Hub 地址。',
2725
+ 'scope.hub':'Hub',
2726
+ 'memory.detail.title':'记忆详情',
2727
+ 'memory.detail.loading':'加载中...',
2728
+ 'memory.detail.notFound':'未找到该记忆',
2729
+ 'memory.detail.copyId':'点击复制 ID',
2730
+ 'memory.detail.created':'创建于 ',
2731
+ 'memory.detail.updated':'更新于 ',
2732
+ 'memory.detail.viewTarget':'查看目标: ',
2651
2733
  'admin.title':'Hub 管理面板',
2652
- 'admin.subtitle':'管理团队成员、分组和共享资源',
2734
+ 'admin.subtitle':'管理团队成员和共享资源',
2653
2735
  'admin.refresh':'\u21BB 刷新',
2654
2736
  'admin.tab.users':'用户',
2655
2737
  'admin.tab.groups':'分组',
@@ -2695,6 +2777,7 @@ const I18N={
2695
2777
  'admin.membersCount':'成员({n}):',
2696
2778
  'admin.noMembersYet':'暂无成员。',
2697
2779
  'admin.loadFailed':'加载管理数据失败:',
2780
+ 'admin.noPermission':'您没有管理员权限,无法访问此面板。',
2698
2781
  'admin.groupsFailed':'加载分组失败:',
2699
2782
  'toast.userApproved':'用户已批准',
2700
2783
  'toast.userRejected':'用户已拒绝',
@@ -2938,6 +3021,20 @@ function selectSharingRole(role){
2938
3021
  document.getElementById('btnRoleClient').className='btn btn-sm'+(role==='client'?' btn-primary':'');
2939
3022
  document.getElementById('hubModeConfig').style.display=role==='hub'?'block':'none';
2940
3023
  document.getElementById('clientModeConfig').style.display=role==='client'?'block':'none';
3024
+ var sp=document.getElementById('sharingStatusPanel');
3025
+ var tp=document.getElementById('sharingTeamPanel');
3026
+ var ap=document.getElementById('sharingAdminPanel');
3027
+ if(role==='client'){
3028
+ if(sp) sp.style.display='none';
3029
+ if(tp) tp.style.display='none';
3030
+ if(ap) ap.style.display='none';
3031
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3032
+ if(adminTab) adminTab.style.display='none';
3033
+ }else{
3034
+ if(sp) sp.style.display='';
3035
+ if(tp) tp.style.display='';
3036
+ if(ap) ap.style.display='';
3037
+ }
2941
3038
  if(role==='hub'){
2942
3039
  var tk=document.getElementById('cfgHubTeamToken');
2943
3040
  if(!tk.value.trim()) tk.value=_genToken(18);
@@ -2950,16 +3047,20 @@ function updateHubShareInfo(){
2950
3047
  var panel=document.getElementById('hubShareInfo');
2951
3048
  var content=document.getElementById('hubShareInfoContent');
2952
3049
  if(!panel||!content) return;
2953
- var token=document.getElementById('cfgHubTeamToken').value.trim();
3050
+ var tokenEl=document.getElementById('cfgHubTeamToken');
3051
+ var token=tokenEl?tokenEl.value.trim():'';
2954
3052
  var port=document.getElementById('cfgHubPort').value.trim()||'18800';
2955
3053
  if(!token||_sharingRole!=='hub'){panel.style.display='none';return;}
2956
3054
  panel.style.display='block';
3055
+ var cpStyle='cursor:pointer;background:rgba(99,102,241,.06);border:1px solid rgba(99,102,241,.15);border-radius:6px;padding:4px 10px;font-family:var(--mono);font-size:12px;color:var(--text);transition:all .15s;user-select:all';
2957
3056
  var renderShare=function(ip){
2958
3057
  var addr=ip?(ip+':'+esc(port)):('&lt;'+t('settings.hub.shareInfo.yourIP')+'&gt;:'+esc(port));
2959
3058
  var tip=t('settings.hub.shareInfo.clickCopy');
2960
3059
  content.innerHTML=
2961
- '<span class="hic-label">Hub '+t('settings.hub.hubAddress')+'</span><span class="hic-value mono" onclick="navigator.clipboard.writeText(this.textContent)" title="'+tip+'">'+addr+'</span>'+
2962
- '<span class="hic-label">Team Token</span><span class="hic-value mono" onclick="navigator.clipboard.writeText(this.textContent)" title="'+tip+'">'+esc(token)+'</span>';
3060
+ '<span style="font-size:11px;color:var(--text-muted);font-weight:500">Hub '+t('settings.hub.hubAddress')+'</span>'+
3061
+ '<span style="'+cpStyle+'" onclick="navigator.clipboard.writeText(this.textContent);toast(&#39;Copied!&#39;,&#39;success&#39;)" title="'+tip+'">'+addr+'</span>'+
3062
+ '<span style="font-size:11px;color:var(--text-muted);font-weight:500">Team Token</span>'+
3063
+ '<span style="'+cpStyle+'" onclick="navigator.clipboard.writeText(this.textContent);toast(&#39;Copied!&#39;,&#39;success&#39;)" title="'+tip+'">'+esc(token)+'</span>';
2963
3064
  };
2964
3065
  if(_cachedLocalIP){renderShare(_cachedLocalIP);return;}
2965
3066
  renderShare('');
@@ -2973,6 +3074,15 @@ async function testHubConnection(){
2973
3074
  var addr=document.getElementById('cfgClientHubAddress').value.trim();
2974
3075
  if(!addr){result.innerHTML='<span style="color:var(--rose)">\u274C '+t('settings.hub.test.noAddr')+'</span>';return;}
2975
3076
  btn.disabled=true;result.innerHTML=t('settings.hub.test.testing');
3077
+ try{
3078
+ var ipsData=await fetch('/api/local-ips').then(function(r){return r.json();});
3079
+ var localAddrs=['127.0.0.1','localhost','0.0.0.0'].concat(ipsData.ips||[]);
3080
+ var parsed=new URL(addr.indexOf('://')>-1?addr:'http://'+addr);
3081
+ if(localAddrs.indexOf(parsed.hostname)>=0){
3082
+ result.innerHTML='<span style="color:var(--rose)">\u274C '+t('sharing.cannotJoinSelf')+'</span>';
3083
+ btn.disabled=false;return;
3084
+ }
3085
+ }catch(e){}
2976
3086
  try{
2977
3087
  var url=addr.match(/^https?:\\/\\//)?addr:'http://'+addr;
2978
3088
  url=url.replace(/\\/+$/,'');
@@ -2981,7 +3091,8 @@ async function testHubConnection(){
2981
3091
  if(d.ok){
2982
3092
  result.innerHTML='<span style="color:var(--green)">\u2705 '+t('settings.hub.test.ok')+(d.teamName?' — '+esc(d.teamName):'')+'</span>';
2983
3093
  }else{
2984
- result.innerHTML='<span style="color:var(--rose)">\u274C '+(d.error||t('settings.hub.test.fail'))+'</span>';
3094
+ var errMsg=d.error==='cannot_join_self'?t('sharing.cannotJoinSelf'):(d.error||t('settings.hub.test.fail'));
3095
+ result.innerHTML='<span style="color:var(--rose)">\u274C '+errMsg+'</span>';
2985
3096
  }
2986
3097
  }catch(e){
2987
3098
  result.innerHTML='<span style="color:var(--rose)">\u274C '+esc(String(e))+'</span>';
@@ -3090,6 +3201,7 @@ function renderSharingSidebar(data){
3090
3201
  if(!statusEl||!hintEl) return;
3091
3202
  if(!data||!data.enabled){
3092
3203
  if(section) section.style.display='none';
3204
+ window._isHubAdmin=false;
3093
3205
  return;
3094
3206
  }
3095
3207
  if(section) section.style.display='block';
@@ -3098,25 +3210,43 @@ function renderSharingSidebar(data){
3098
3210
  if(!badgeEl)return;
3099
3211
  badgeEl.innerHTML='<span style="display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:2px 8px;border-radius:9999px;background:'+color+'15;color:'+color+'"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:'+color+(glow?';box-shadow:0 0 4px '+color:'')+'"></span>'+esc(text)+'</span>';
3100
3212
  }
3101
- if(conn.connected&&conn.user){
3102
- var groups=(conn.user.groups||[]).map(function(g){return g.name;}).join(', ')||'';
3213
+ if(data.role==='hub'){
3214
+ setBadge('#34d399',t('sharing.sidebar.connected'),true);
3215
+ statusEl.innerHTML='';
3216
+ hintEl.textContent='';
3217
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3218
+ if(adminTab) adminTab.style.display='';
3219
+ }else if(conn.pendingApproval&&conn.user){
3220
+ setBadge('#fbbf24',t('sharing.sidebar.pending'),false);
3221
+ var html='<div class="info-grid">';
3222
+ html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username)+'</span>';
3223
+ if(conn.teamName) html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName)+'</span>';
3224
+ html+='</div>';
3225
+ statusEl.innerHTML=html;
3226
+ hintEl.textContent=t('sharing.pendingApproval.hint');
3227
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3228
+ if(adminTab) adminTab.style.display='none';
3229
+ }else if(conn.rejected&&conn.user){
3230
+ setBadge('#ef4444',t('sharing.sidebar.rejected'),false);
3231
+ var html='<div class="info-grid">';
3232
+ html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username||'-')+'</span>';
3233
+ if(conn.teamName) html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName)+'</span>';
3234
+ html+='</div>';
3235
+ statusEl.innerHTML=html;
3236
+ hintEl.textContent=t('sharing.rejected.hint');
3237
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3238
+ if(adminTab) adminTab.style.display='none';
3239
+ }else if(conn.connected&&conn.user){
3103
3240
  var isAdmin=conn.user.role==='admin';
3104
3241
  setBadge('#34d399',t('sharing.sidebar.connected'),true);
3105
3242
  var html='<div class="info-grid">';
3106
3243
  html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username)+(isAdmin?' <span class="role-badge admin">'+t('sharing.sidebar.admin')+'</span>':'')+'</span>';
3107
3244
  html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName||'-')+'</span>';
3108
- if(groups) html+='<span class="label">'+t('sharing.groups')+'</span><span class="value">'+esc(groups)+'</span>';
3109
3245
  html+='</div>';
3110
3246
  statusEl.innerHTML=html;
3111
3247
  hintEl.innerHTML='';
3112
3248
  var adminTab=document.querySelector('.tab[data-view="admin"]');
3113
3249
  if(adminTab) adminTab.style.display=isAdmin?'':'none';
3114
- }else if(data.role==='hub'){
3115
- setBadge('#fbbf24',t('sharing.sidebar.starting'),false);
3116
- statusEl.innerHTML='';
3117
- hintEl.textContent=t('sharing.hubMode.hint');
3118
- var adminTab=document.querySelector('.tab[data-view="admin"]');
3119
- if(adminTab) adminTab.style.display=(data.admin&&data.admin.canManageUsers)?'':'none';
3120
3250
  }else if(data.clientConfigured){
3121
3251
  setBadge('#ef4444',t('sharing.sidebar.disconnected'),false);
3122
3252
  statusEl.innerHTML='<div style="font-size:11px;color:var(--text-muted)">'+t('sharing.sidebar.targetHub')+' '+esc(data.hubUrl||'')+'</div>';
@@ -3139,49 +3269,75 @@ function renderSharingSettings(data){
3139
3269
  }
3140
3270
  var conn=data.connection||{};
3141
3271
  var user=conn.user||{};
3142
- var groups=(user.groups||[]).map(function(g){return g.name;}).join(', ')||'-';
3143
- var connBadge=conn.connected
3144
- ?'<span class="hic-badge connected"><span class="hic-dot green"></span>'+t('sharing.sidebar.connected')+'</span>'
3145
- :'<span class="hic-badge disconnected"><span class="hic-dot red"></span>'+t('sharing.sidebar.disconnected')+'</span>';
3146
- var roleLabel=data.role==='hub'?t('sharing.role.hub'):t('sharing.role.client');
3147
- var sh='<div class="hub-info-card hic-status"><div class="hic-title"><span class="hic-icon">\u{1F517}</span>'+t('settings.hub.connection')+' '+connBadge+'</div><div class="hic-grid">';
3148
- sh+='<span class="hic-label">'+t('sharing.mode')+'</span><span class="hic-value">'+roleLabel+'</span>';
3149
- if(conn.connected&&user.username){
3150
- sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value" style="display:flex;align-items:center;gap:6px">'+
3151
- '<input type="text" id="hubUsernameInput" value="'+esc(user.username)+'" style="border:1px solid var(--border);border-radius:6px;padding:3px 8px;font-size:12px;background:var(--bg);color:var(--text);width:120px;font-family:inherit" />'+
3152
- (user.role==='admin'?' <span class="hic-badge admin">admin</span>':'')+
3153
- '<button class="btn btn-sm" onclick="updateHubUsername()" style="padding:2px 10px;font-size:11px">'+t('sharing.saveUsername')+'</button>'+
3154
- '</span>';
3272
+ var actualRole=data.role||_sharingRole||'client';
3273
+ if(data.role) _sharingRole=data.role;
3274
+ var isAdmin=(data.admin&&data.admin.canManageUsers)||(conn.connected&&user.role==='admin')||(actualRole==='hub');
3275
+ window._isHubAdmin=isAdmin;
3276
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3277
+ if(adminTab) adminTab.style.display=isAdmin?'':'none';
3278
+ var hubAdminBtn=document.getElementById('hubAdminEntryBtn');
3279
+
3280
+ if(actualRole==='hub'){
3281
+ statusEl.innerHTML='';teamEl.innerHTML='';adminEl.innerHTML='';
3282
+ if(hubAdminBtn) hubAdminBtn.style.display=isAdmin?'':'none';
3283
+ return;
3155
3284
  }
3156
- sh+='</div></div>';
3157
- statusEl.innerHTML=sh;
3158
-
3159
- var th='';
3160
- if(conn.connected&&conn.user){
3161
- th='<div class="hub-info-card hic-team"><div class="hic-title"><span class="hic-icon">\u{1F465}</span>'+t('settings.hub.team')+'</div><div class="hic-grid">';
3162
- th+='<span class="hic-label">'+t('sharing.team')+'</span><span class="hic-value">'+esc(conn.teamName||'-')+'</span>';
3163
- th+='<span class="hic-label">'+t('sharing.groups')+'</span><span class="hic-value">'+esc(groups)+'</span>';
3164
- th+='</div>';
3165
- if(user.role==='admin'){
3166
- th+='<div class="hic-actions">'+
3167
- '<button class="btn btn-sm" onclick="loadGroupManager()">'+t('sharing.manageGroups')+'</button>'+
3168
- '<button class="btn btn-sm btn-primary" onclick="switchView(\\\'admin\\\')">'+t('sharing.openAdmin')+'</button></div>';
3285
+
3286
+ if(actualRole==='client'){
3287
+ statusEl.style.display='none';teamEl.style.display='none';adminEl.style.display='none';
3288
+ if(adminTab) adminTab.style.display='none';
3289
+ if(hubAdminBtn) hubAdminBtn.style.display='none';
3290
+
3291
+ var connBadge;
3292
+ if(conn.pendingApproval){
3293
+ connBadge='<span class="hic-badge pending"><span class="hic-dot amber"></span>'+t('sharing.sidebar.pending')+'</span>';
3294
+ }else if(conn.rejected){
3295
+ connBadge='<span class="hic-badge disconnected"><span class="hic-dot red"></span>'+t('sharing.sidebar.rejected')+'</span>';
3296
+ }else if(conn.connected){
3297
+ connBadge='<span class="hic-badge connected"><span class="hic-dot green"></span>'+t('sharing.sidebar.connected')+'</span>';
3298
+ }else{
3299
+ connBadge='<span class="hic-badge disconnected"><span class="hic-dot red"></span>'+t('sharing.sidebar.disconnected')+'</span>';
3169
3300
  }
3170
- th+='<div id="groupManagerPanel" style="margin-top:12px;display:none"></div></div>';
3171
- }else if(data.role==='hub'&&!data.clientConfigured){
3172
- th='<div class="hub-info-card hic-team"><div class="hic-title"><span class="hic-icon">\u{1F465}</span>'+t('settings.hub.team')+'</div>';
3173
- th+='<div class="hic-empty">'+t('sharing.hubNotConfigured')+'</div></div>';
3301
+ statusEl.style.display='';
3302
+ var sh='<div class="hub-info-card hic-status"><div class="hic-title"><span class="hic-icon">\u{1F517}</span>'+t('settings.hub.connection')+' '+connBadge+'</div><div class="hic-grid">';
3303
+ if(conn.pendingApproval&&user.username){
3304
+ sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value">'+esc(user.username)+'</span>';
3305
+ sh+='</div><div class="hic-empty" style="color:#f59e0b">'+t('sharing.pendingApproval.hint')+'</div></div>';
3306
+ }else if(conn.rejected){
3307
+ if(user.username) sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value">'+esc(user.username)+'</span>';
3308
+ sh+='</div><div class="hic-empty" style="color:#ef4444">'+t('sharing.rejected.hint')+'</div>'+
3309
+ '<div style="margin-top:10px;padding:0 16px 14px"><button class="btn btn-sm btn-primary" onclick="retryHubJoin()">'+t('sharing.retryJoin')+'</button>'+
3310
+ '<span style="font-size:11px;color:var(--text-muted);margin-left:8px">'+t('sharing.retryJoin.hint')+'</span></div></div>';
3311
+ }else if(conn.connected&&user.username){
3312
+ sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value" style="display:flex;align-items:center;gap:6px">'+
3313
+ '<input type="text" id="hubUsernameInput" value="'+esc(user.username)+'" style="border:1px solid var(--border);border-radius:6px;padding:3px 8px;font-size:12px;background:var(--bg);color:var(--text);width:120px;font-family:inherit" />'+
3314
+ '<button class="btn btn-sm" onclick="updateHubUsername()" style="padding:2px 10px;font-size:11px">'+t('sharing.saveUsername')+'</button>'+
3315
+ '</span>';
3316
+ sh+='<span class="hic-label">'+t('sharing.team')+'</span><span class="hic-value">'+esc(conn.teamName||'-')+'</span>';
3317
+ sh+='</div></div>';
3318
+ }else{
3319
+ sh+='</div></div>';
3320
+ }
3321
+ statusEl.innerHTML=sh;
3322
+ teamEl.innerHTML='';adminEl.innerHTML='';
3323
+ return;
3174
3324
  }
3175
- teamEl.innerHTML=th;
3176
3325
 
3177
- var isAdmin=(data.admin&&data.admin.canManageUsers)||(conn.connected&&user.role==='admin')||(data.role==='hub');
3178
- if(isAdmin){
3179
- adminEl.innerHTML='<div class="hub-info-card hic-pending"><div class="hic-title"><span class="hic-icon">\u{1F6E1}</span>'+t('settings.hub.adminPending')+' <span class="hic-badge pending"><span class="hic-dot amber"></span>'+t('sharing.adminEnabled')+'</span></div><div class="hic-empty">'+t('sharing.adminPendingHint')+'</div></div>';
3180
- }else{
3181
- adminEl.innerHTML='';
3182
- }
3183
- var adminTab=document.querySelector('.tab[data-view="admin"]');
3184
- if(adminTab) adminTab.style.display=isAdmin?'':'none';
3326
+ statusEl.innerHTML='';teamEl.innerHTML='';adminEl.innerHTML='';
3327
+ }
3328
+
3329
+ async function retryHubJoin(){
3330
+ if(!confirm(t('sharing.retryJoin.confirm'))) return;
3331
+ try{
3332
+ var r=await fetch('/api/sharing/retry-join',{method:'POST',headers:{'Content-Type':'application/json'}});
3333
+ var d=await r.json();
3334
+ if(d.ok){
3335
+ toast(t('sharing.retryJoin.success'),'success');
3336
+ setTimeout(function(){location.reload();},1500);
3337
+ }else{
3338
+ toast(d.error||t('sharing.retryJoin.fail'),'error');
3339
+ }
3340
+ }catch(e){toast(t('sharing.retryJoin.fail')+': '+e.message,'error');}
3185
3341
  }
3186
3342
 
3187
3343
  async function updateHubUsername(){
@@ -3215,6 +3371,7 @@ async function updateHubUsername(){
3215
3371
  }
3216
3372
 
3217
3373
  async function loadSharingPendingUsers(){
3374
+ if(_sharingRole==='client') return;
3218
3375
  var el=document.getElementById('sharingAdminPanel');
3219
3376
  if(!el) return;
3220
3377
  el.innerHTML=t('sharing.loading');
@@ -3422,24 +3579,27 @@ function switchAdminTab(tab,btn){
3422
3579
  }
3423
3580
 
3424
3581
  async function loadAdminData(){
3582
+ if(!window._isHubAdmin){
3583
+ var statsEl=document.getElementById('adminStats');
3584
+ if(statsEl) statsEl.innerHTML='<div class="admin-empty">'+t('admin.noPermission')+'</div>';
3585
+ return;
3586
+ }
3425
3587
  try{
3426
- var [usersR,groupsR,tasksR,skillsR,pendingR,memoriesR]=await Promise.all([
3588
+ var [usersR,tasksR,skillsR,pendingR,memoriesR]=await Promise.all([
3427
3589
  fetch('/api/sharing/users').then(function(r){return r.json();}),
3428
- fetch('/api/sharing/groups').then(function(r){return r.json();}),
3429
3590
  fetch('/api/admin/shared-tasks').then(function(r){return r.json();}),
3430
3591
  fetch('/api/admin/shared-skills').then(function(r){return r.json();}),
3431
3592
  fetch('/api/sharing/pending-users').then(function(r){return r.json();}),
3432
3593
  fetch('/api/admin/shared-memories').then(function(r){return r.json();})
3433
3594
  ]);
3434
3595
  adminDataCache.users=Array.isArray(usersR.users)?usersR.users:[];
3435
- adminDataCache.groups=Array.isArray(groupsR.groups)?groupsR.groups:[];
3596
+ adminDataCache.groups=[];
3436
3597
  adminDataCache.tasks=Array.isArray(tasksR.tasks)?tasksR.tasks:[];
3437
3598
  adminDataCache.skills=Array.isArray(skillsR.skills)?skillsR.skills:[];
3438
3599
  adminDataCache.memories=Array.isArray(memoriesR.memories)?memoriesR.memories:[];
3439
3600
  var pending=Array.isArray(pendingR.users)?pendingR.users:[];
3440
3601
  renderAdminStats(pending.length);
3441
3602
  renderAdminUsers(adminDataCache.users, pending);
3442
- renderAdminGroups(adminDataCache.groups);
3443
3603
  renderAdminMemories(adminDataCache.tasks);
3444
3604
  renderAdminSkills(adminDataCache.skills);
3445
3605
  renderAdminSharedMemories(adminDataCache.memories);
@@ -3455,12 +3615,10 @@ function renderAdminStats(pendingCount){
3455
3615
  el.innerHTML=
3456
3616
  '<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>'+
3457
3617
  '<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>'+
3458
- '<div class="admin-stat-box"><span class="as-icon">\u{1F4C2}</span><div class="val">'+adminDataCache.groups.length+'</div><div class="lbl">'+t('admin.stat.groups')+'</div></div>'+
3459
3618
  '<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>'+
3460
3619
  '<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>'+
3461
3620
  '<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>';
3462
3621
  var tc=document.getElementById('adminTabCountUsers');if(tc)tc.textContent=adminDataCache.users.length+pendingCount;
3463
- tc=document.getElementById('adminTabCountGroups');if(tc)tc.textContent=adminDataCache.groups.length;
3464
3622
  tc=document.getElementById('adminTabCountMemories');if(tc)tc.textContent=(adminDataCache.memories||[]).length;
3465
3623
  tc=document.getElementById('adminTabCountTasks');if(tc)tc.textContent=adminDataCache.tasks.length;
3466
3624
  tc=document.getElementById('adminTabCountSkills');if(tc)tc.textContent=adminDataCache.skills.length;
@@ -3623,10 +3781,9 @@ function renderAdminMemories(tasks){
3623
3781
  for(var i=0;i<tasks.length;i++){
3624
3782
  var tk=tasks[i];
3625
3783
  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>'+
3626
- '<span class="admin-badge '+(tk.visibility==='public'?'public':'group')+'">'+esc(tk.visibility||'public')+'</span></div>'+
3784
+ '</div>'+
3627
3785
  '<div class="admin-card-meta">'+
3628
3786
  t('admin.owner')+esc(tk.ownerName||tk.sourceUserId||'unknown')+
3629
- (tk.groupName?' \u00B7 '+t('admin.group')+esc(tk.groupName):'')+
3630
3787
  (tk.chunkCount!=null?' \u00B7 '+t('admin.chunks')+tk.chunkCount:'')+
3631
3788
  ' \u00B7 '+t('admin.updated')+new Date(tk.updatedAt||tk.createdAt).toLocaleDateString()+
3632
3789
  '</div>'+
@@ -3658,11 +3815,10 @@ function renderAdminSkills(skills){
3658
3815
  for(var i=0;i<skills.length;i++){
3659
3816
  var s=skills[i];
3660
3817
  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>'+
3661
- '<span class="admin-badge '+(s.visibility==='public'?'public':'group')+'">'+esc(s.visibility||'public')+'</span></div>'+
3818
+ '</div>'+
3662
3819
  '<div class="admin-card-meta">'+
3663
3820
  (s.description?esc(s.description)+'<br>':'')+
3664
3821
  t('admin.owner')+esc(s.ownerName||s.sourceUserId||'unknown')+
3665
- (s.groupName?' \u00B7 '+t('admin.group')+esc(s.groupName):'')+
3666
3822
  (s.version!=null?' \u00B7 '+t('admin.version')+s.version:'')+
3667
3823
  (s.qualityScore!=null?' \u00B7 '+t('admin.quality')+s.qualityScore:'')+
3668
3824
  '</div>'+
@@ -3694,10 +3850,9 @@ function renderAdminSharedMemories(memories){
3694
3850
  for(var i=0;i<memories.length;i++){
3695
3851
  var m=memories[i];
3696
3852
  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>'+
3697
- '<span class="admin-badge '+(m.visibility==='public'?'public':'group')+'">'+esc(m.visibility||'public')+'</span></div>'+
3853
+ '</div>'+
3698
3854
  '<div class="admin-card-meta">'+
3699
3855
  t('admin.owner')+esc(m.ownerName||m.sourceUserId||'unknown')+
3700
- (m.groupName?' \u00B7 '+t('admin.group')+esc(m.groupName):'')+
3701
3856
  (m.kind?' \u00B7 Kind: '+esc(m.kind):'')+
3702
3857
  (m.role?' \u00B7 Role: '+esc(m.role):'')+
3703
3858
  ' \u00B7 '+t('admin.updated')+new Date(m.updatedAt||m.createdAt).toLocaleDateString()+
@@ -3886,21 +4041,16 @@ function renderTaskShareActions(task){
3886
4041
  const isShared=!!current;
3887
4042
  var statusHtml='';
3888
4043
  if(isShared){
3889
- var scopeLabel=current==='group'?'Group':'Public';
3890
- 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')+' ('+scopeLabel+')</span>';
4044
+ 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>';
3891
4045
  }
3892
4046
  el.innerHTML=statusHtml+
3893
- '<select id="taskShareScope" class="filter-select" style="min-width:120px">'+
3894
- '<option value="group"'+(current==='group'?' selected':'')+'>Share to Group</option>'+
3895
- '<option value="public"'+(current==='public'?' selected':'')+'>Share to Public</option>'+
3896
- '</select>'+
3897
4047
  '<button class="btn btn-sm" onclick="shareCurrentTask()">'+(isShared?t('share.updateBtn'):t('share.shareBtn'))+'</button>'+
3898
4048
  (isShared?'<button class="btn btn-sm btn-ghost" onclick="unshareCurrentTask()">'+t('share.unshareBtn')+'</button>':'');
3899
4049
  }
3900
4050
 
3901
4051
  async function shareCurrentTask(){
3902
4052
  if(!currentTaskDetail) return;
3903
- const visibility=document.getElementById('taskShareScope').value||'public';
4053
+ const visibility='public';
3904
4054
  try{
3905
4055
  const r=await fetch('/api/sharing/tasks/share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({taskId:currentTaskDetail.id,visibility:visibility})});
3906
4056
  const d=await r.json();
@@ -3925,21 +4075,16 @@ function renderSkillShareActions(skill){
3925
4075
  const isShared=!!current;
3926
4076
  var statusHtml='';
3927
4077
  if(isShared){
3928
- var scopeLabel=current==='group'?'Group':'Public';
3929
- 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')+' ('+scopeLabel+')</span>';
4078
+ 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>';
3930
4079
  }
3931
4080
  el.innerHTML=statusHtml+
3932
- '<select id="skillShareScope" class="filter-select" style="min-width:120px">'+
3933
- '<option value="group"'+(current==='group'?' selected':'')+'>Share to Group</option>'+
3934
- '<option value="public"'+(current==='public'?' selected':'')+'>Share to Public</option>'+
3935
- '</select>'+
3936
4081
  '<button class="btn btn-sm" onclick="shareCurrentSkill()">'+(isShared?t('share.updateBtn'):t('share.shareBtn'))+'</button>'+
3937
4082
  (isShared?'<button class="btn btn-sm btn-ghost" onclick="unshareCurrentSkill()">'+t('share.unshareBtn')+'</button>':'');
3938
4083
  }
3939
4084
 
3940
4085
  async function shareCurrentSkill(){
3941
4086
  if(!currentSkillDetail) return;
3942
- const visibility=document.getElementById('skillShareScope').value||'public';
4087
+ const visibility='public';
3943
4088
  try{
3944
4089
  const r=await fetch('/api/sharing/skills/share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({skillId:currentSkillDetail.id,visibility:visibility})});
3945
4090
  const d=await r.json();
@@ -5191,6 +5336,7 @@ async function saveConfig(){
5191
5336
  cfg.sharing.hub={port:hubPort?Number(hubPort):18800};
5192
5337
  if(hubTeamName) cfg.sharing.hub.teamName=hubTeamName;
5193
5338
  if(hubTeamToken) cfg.sharing.hub.teamToken=hubTeamToken;
5339
+ cfg.sharing.client={hubAddress:'',userToken:'',teamToken:''};
5194
5340
  }
5195
5341
  if(sharingEnabled&&_sharingRole==='client'){
5196
5342
  var clientAddr=document.getElementById('cfgClientHubAddress').value.trim();
@@ -5200,6 +5346,17 @@ async function saveConfig(){
5200
5346
  if(clientAddr) cfg.sharing.client.hubAddress=clientAddr;
5201
5347
  if(clientTeamToken) cfg.sharing.client.teamToken=clientTeamToken;
5202
5348
  if(clientUserToken) cfg.sharing.client.userToken=clientUserToken;
5349
+ cfg.sharing.hub={port:18800,teamName:'',teamToken:''};
5350
+ if(clientAddr){
5351
+ try{
5352
+ var ips=await fetch('/api/local-ips').then(function(r){return r.json();});
5353
+ var localAddrs=['127.0.0.1','localhost','0.0.0.0'].concat(ips.ips||[]);
5354
+ var parsed=new URL(clientAddr.indexOf('://')>-1?clientAddr:'http://'+clientAddr);
5355
+ if(localAddrs.indexOf(parsed.hostname)>=0){
5356
+ done();toast(t('sharing.cannotJoinSelf'),'error');return;
5357
+ }
5358
+ }catch(e){}
5359
+ }
5203
5360
  }
5204
5361
 
5205
5362
  // 1) Embedding model is required
@@ -5377,7 +5534,7 @@ function fillDays(rows,days){
5377
5534
  const row=map.get(dateStr)||{};
5378
5535
  out.push({date:dateStr,count:row.count??0,list:row.list??0,search:row.search??0,total:(row.list??0)+(row.search??0)});
5379
5536
  }
5380
- if(days>21){
5537
+ if(days>60){
5381
5538
  const weeks=[];let i=0;
5382
5539
  while(i<out.length){
5383
5540
  const chunk=out.slice(i,i+7);
@@ -5395,19 +5552,23 @@ function fillDays(rows,days){
5395
5552
  }
5396
5553
 
5397
5554
  function renderBars(el,data,valueKey,H){
5398
- const vals=data.map(d=>d[valueKey]??0);
5399
- if(vals.every(v=>v===0)){el.innerHTML='<div style="color:var(--text-muted);font-size:13px;padding:20px;text-align:center">'+t('chart.nodata')+'</div>';return;}
5400
- const max=Math.max(1,...vals);
5401
- const nonZero=vals.filter(v=>v>0).length;
5402
- const barStyle=data.length<=7?'min-width:40px;max-width:120px':'';
5403
- el.innerHTML=data.map(r=>{
5404
- const v=r[valueKey]??0;
5405
- const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
5555
+ var vals=data.map(function(d){return d[valueKey]??0;});
5556
+ if(vals.every(function(v){return v===0;})){el.innerHTML='<div style="color:var(--text-muted);font-size:13px;padding:40px;text-align:center">'+t('chart.nodata')+'</div>';return;}
5557
+ var max=Math.max(1,Math.max.apply(null,vals));
5558
+ var n=data.length;
5559
+ var labelStep=n<=7?1:(n<=14?2:5);
5560
+ el.innerHTML=data.map(function(r,idx){
5561
+ var v=r[valueKey]??0;
5562
+ var rawDate=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):'');
5563
+ var showLabel=(idx%labelStep===0)||(idx===n-1);
5564
+ var label=showLabel?rawDate:'';
5565
+ var tipDate=r.date.length>=10?r.date.slice(5,10):'';
5566
+ var tipText=tipDate?(tipDate+': '+v):(''+v);
5406
5567
  if(v===0){
5407
- return '<div class="chart-bar-wrap" style="'+barStyle+'"><div class="chart-tip">0</div><div class="chart-bar-col"><div class="chart-bar zero" style="height:2px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
5568
+ return '<div class="chart-bar-wrap"><div class="chart-tip">'+tipText+'</div><div class="chart-bar-col"><div class="chart-bar zero" style="height:3px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
5408
5569
  }
5409
- const h=Math.max(8,Math.round((v/max)*H));
5410
- return '<div class="chart-bar-wrap" style="'+barStyle+'"><div class="chart-tip">'+v+'</div><div class="chart-bar-col"><div class="chart-bar" style="height:'+h+'px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
5570
+ var h=Math.max(8,Math.round((v/max)*H));
5571
+ return '<div class="chart-bar-wrap"><div class="chart-tip">'+tipText+'</div><div class="chart-bar-col"><div class="chart-bar" style="height:'+h+'px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
5411
5572
  }).join('');
5412
5573
  }
5413
5574
 
@@ -5418,25 +5579,31 @@ function renderChartWrites(rows){
5418
5579
  }
5419
5580
 
5420
5581
  function renderChartCalls(rows){
5421
- const el=document.getElementById('chartCalls');
5422
- const filled=fillDays(rows?.map(r=>({date:r.date,list:r.list,search:r.search})),metricsDays);
5423
- const vals=filled.map(f=>f.total);
5424
- if(vals.every(v=>v===0)){el.innerHTML='<div style="color:var(--text-muted);font-size:13px;padding:20px;text-align:center">'+t('chart.nocalls')+'</div>';return;}
5425
- const max=Math.max(1,...vals);
5426
- const H=160;
5427
- el.innerHTML=filled.map(r=>{
5428
- const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
5582
+ var el=document.getElementById('chartCalls');
5583
+ var filled=fillDays(rows?.map(function(r){return {date:r.date,list:r.list,search:r.search};}),metricsDays);
5584
+ var vals=filled.map(function(f){return f.total;});
5585
+ if(vals.every(function(v){return v===0;})){el.innerHTML='<div style="color:var(--text-muted);font-size:13px;padding:40px;text-align:center">'+t('chart.nocalls')+'</div>';return;}
5586
+ var max=Math.max(1,Math.max.apply(null,vals));
5587
+ var H=160;
5588
+ var n=filled.length;
5589
+ var labelStep=n<=7?1:(n<=14?2:5);
5590
+ el.innerHTML=filled.map(function(r,idx){
5591
+ var rawDate=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):'');
5592
+ var showLabel=(idx%labelStep===0)||(idx===n-1);
5593
+ var label=showLabel?rawDate:'';
5594
+ var tipDate=r.date.length>=10?r.date.slice(5,10):'';
5429
5595
  if(r.total===0){
5430
- return '<div class="chart-bar-wrap"><div class="chart-tip">0</div><div class="chart-bar-col"><div class="chart-bar zero" style="height:2px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
5596
+ var tipZero=tipDate?(tipDate+': 0'):'0';
5597
+ return '<div class="chart-bar-wrap"><div class="chart-tip">'+tipZero+'</div><div class="chart-bar-col"><div class="chart-bar zero" style="height:3px"></div></div><div class="chart-bar-label">'+label+'</div></div>';
5431
5598
  }
5432
- const totalH=Math.max(8,Math.round((r.total/max)*H));
5433
- const listH=r.list?Math.max(3,Math.round((r.list/r.total)*totalH)):0;
5434
- const searchH=r.search?totalH-listH:0;
5435
- const tip='List: '+r.list+', Search: '+r.search;
5436
- let bars='';
5599
+ var totalH=Math.max(8,Math.round((r.total/max)*H));
5600
+ var listH=r.list?Math.max(4,Math.round((r.list/r.total)*totalH)):0;
5601
+ var searchH=r.search?totalH-listH:0;
5602
+ var tip=(tipDate?tipDate+' - ':'')+'List: '+r.list+', Search: '+r.search;
5603
+ var bars='';
5437
5604
  if(searchH>0) bars+='<div class="chart-bar violet" style="height:'+searchH+'px"></div>';
5438
5605
  if(listH>0) bars+='<div class="chart-bar" style="height:'+listH+'px"></div>';
5439
- return '<div class="chart-bar-wrap"><div class="chart-tip">'+tip+'</div><div class="chart-bar-col"><div style="display:flex;flex-direction:column;gap:1px">'+bars+'</div></div><div class="chart-bar-label">'+label+'</div></div>';
5606
+ return '<div class="chart-bar-wrap"><div class="chart-tip">'+tip+'</div><div class="chart-bar-col"><div style="display:flex;flex-direction:column;gap:1px;align-items:center;width:100%">'+bars+'</div></div><div class="chart-bar-label">'+label+'</div></div>';
5440
5607
  }).join('');
5441
5608
  }
5442
5609
 
@@ -6014,38 +6181,50 @@ function scrollToMemory(targetId){
6014
6181
  }
6015
6182
  showMemoryModal(targetId);
6016
6183
  }
6184
+ function fmtModalDate(v){
6185
+ if(!v) return '-';
6186
+ var d=new Date(v);
6187
+ if(isNaN(d.getTime())) return '-';
6188
+ return d.toLocaleString(dateLoc());
6189
+ }
6017
6190
  async function showMemoryModal(chunkId){
6018
- const overlay=document.getElementById('memoryModal');
6019
- const body=document.getElementById('memoryModalBody');
6020
- body.innerHTML='<div style="text-align:center;padding:40px;color:var(--text-sec)">Loading...</div>';
6191
+ var overlay=document.getElementById('memoryModal');
6192
+ var body=document.getElementById('memoryModalBody');
6193
+ body.innerHTML='<div style="text-align:center;padding:56px;color:var(--text-sec)"><div class="spinner" style="margin:0 auto 14px"></div><div style="font-size:12px;letter-spacing:.04em">'+t('memory.detail.loading')+'</div></div>';
6021
6194
  overlay.classList.add('show');
6022
6195
  try{
6023
- const res=await fetch('/api/memory/'+encodeURIComponent(chunkId));
6024
- if(!res.ok){body.innerHTML='<div style="text-align:center;padding:40px;color:#f87171">Memory not found</div>';return;}
6025
- const data=await res.json();
6026
- const m=data.memory;
6027
- const role=(m.role||'unknown').toUpperCase();
6028
- const roleCls=(m.role||'').toLowerCase();
6029
- const ds=m.dedup_status||'active';
6030
- const time=new Date(m.created_at).toLocaleString(dateLoc());
6031
- const updated=m.updated_at?new Date(m.updated_at).toLocaleString(dateLoc()):'';
6032
- let html='<div class="modal-memory-card">';
6033
- html+='<div class="modal-header-row"><span class="role-tag '+roleCls+'">'+role+'</span>';
6034
- if(ds!=='active') html+='<span class="dedup-badge '+(ds==='duplicate'?'duplicate':'merged')+'">'+ds+'</span>';
6035
- html+='</div>';
6036
- html+='<div class="modal-field"><div class="modal-field-label">ID</div><div class="modal-field-val" style="font-family:monospace;font-size:11px">'+esc(m.id)+'</div></div>';
6037
- html+='<div class="modal-field"><div class="modal-field-label">Summary</div><div class="modal-field-val" style="font-size:14px;font-weight:600">'+esc(m.summary||'')+'</div></div>';
6038
- html+='<div class="modal-field"><div class="modal-field-label">Content</div><pre class="modal-field-content">'+esc(m.content||'')+'</pre></div>';
6039
- html+='<div class="modal-meta-row">';
6040
- html+='<span><strong>Session:</strong> '+esc(m.session_key||'')+'</span>';
6041
- html+='<span><strong>Created:</strong> '+time+'</span>';
6042
- if(updated) html+='<span><strong>Updated:</strong> '+updated+'</span>';
6043
- html+='</div>';
6044
- if(m.dedup_reason) html+='<div class="modal-field"><div class="modal-field-label">Dedup Reason</div><div class="modal-field-val">'+esc(m.dedup_reason)+'</div></div>';
6045
- if(m.dedup_target&&m.dedup_target!==chunkId) html+='<div class="modal-field"><span class="dedup-target-link" onclick="closeMemoryModal();scrollToMemory(\\''+m.dedup_target+'\\')">View target: '+m.dedup_target.slice(0,8)+'...</span></div>';
6046
- html+='</div>';
6047
- body.innerHTML=html;
6048
- }catch(e){body.innerHTML='<div style="text-align:center;padding:40px;color:#f87171">Error: '+esc(String(e))+'</div>';}
6196
+ var res=await fetch('/api/memory/'+encodeURIComponent(chunkId));
6197
+ if(!res.ok){body.innerHTML='<div style="text-align:center;padding:56px"><div style="font-size:32px;margin-bottom:12px">\u{1F50D}</div><div style="color:#f87171;font-size:13px">'+t('memory.detail.notFound')+'</div></div>';return;}
6198
+ var data=await res.json();
6199
+ var m=data.memory;
6200
+ var role=(m.role||'unknown').toUpperCase();
6201
+ var roleCls=(m.role||'').toLowerCase();
6202
+ var ds=m.dedup_status||'active';
6203
+ var h='<div class="mm-hero">';
6204
+ h+='<div class="mm-hero-row">';
6205
+ h+='<span class="mm-role-chip '+roleCls+'">'+role+'</span>';
6206
+ if(ds!=='active') h+='<span class="mm-dedup-chip '+(ds==='duplicate'?'duplicate':'merged')+'">'+(ds==='duplicate'?'\u274C':'\u{1F504}')+' '+ds+'</span>';
6207
+ h+='<span class="mm-id" onclick="navigator.clipboard.writeText(\\''+esc(m.id)+'\\');toast(\\'ID copied\\',\\'success\\')" title="'+t('memory.detail.copyId')+'">'+esc(m.id.slice(0,12))+'</span>';
6208
+ h+='</div>';
6209
+ if(m.summary) h+='<div class="mm-summary">'+esc(m.summary)+'</div>';
6210
+ h+='</div>';
6211
+ if(m.content){
6212
+ h+='<div class="mm-section"><div class="mm-section-label">Content</div><pre class="mm-content">'+esc(m.content)+'</pre></div>';
6213
+ }
6214
+ h+='<div class="mm-meta">';
6215
+ if(m.session_key) h+='<div class="mm-meta-chip"><strong>Session</strong><span>'+esc(m.session_key.slice(0,12))+'</span></div>';
6216
+ h+='<div class="mm-meta-chip"><strong>'+t('memory.detail.created')+'</strong><span>'+fmtModalDate(m.created_at)+'</span></div>';
6217
+ if(m.updated_at) h+='<div class="mm-meta-chip"><strong>'+t('memory.detail.updated')+'</strong><span>'+fmtModalDate(m.updated_at)+'</span></div>';
6218
+ if(m.kind) h+='<div class="mm-meta-chip"><strong>Kind</strong><span>'+esc(m.kind)+'</span></div>';
6219
+ h+='</div>';
6220
+ if(m.dedup_reason){
6221
+ h+='<div class="mm-dedup"><div class="mm-dedup-box">'+esc(m.dedup_reason)+'</div></div>';
6222
+ }
6223
+ if(m.dedup_target&&m.dedup_target!==chunkId){
6224
+ h+='<div class="mm-footer"><span class="dedup-target-link" onclick="closeMemoryModal();scrollToMemory(\\''+m.dedup_target+'\\')">'+t('memory.detail.viewTarget')+' '+m.dedup_target.slice(0,8)+'...</span></div>';
6225
+ }
6226
+ body.innerHTML=h;
6227
+ }catch(e){body.innerHTML='<div style="text-align:center;padding:56px"><div style="font-size:32px;margin-bottom:12px">\u26A0\uFE0F</div><div style="color:#f87171;font-size:13px">'+esc(String(e))+'</div></div>';}
6049
6228
  }
6050
6229
  function closeMemoryModal(){document.getElementById('memoryModal').classList.remove('show');}
6051
6230
 
@@ -6843,8 +7022,8 @@ checkAuth();
6843
7022
  <div class="memory-modal-overlay" id="memoryModal" onclick="if(event.target===this)closeMemoryModal()">
6844
7023
  <div class="memory-modal">
6845
7024
  <div class="memory-modal-title">
6846
- <span>Memory Detail</span>
6847
- <button class="btn btn-sm btn-ghost" onclick="closeMemoryModal()" style="font-size:16px;padding:2px 8px">&times;</button>
7025
+ <div class="mm-tl"><div class="mm-tl-icon">\u{1F9E0}</div><span data-i18n="memory.detail.title">Memory Detail</span></div>
7026
+ <button class="mm-close" onclick="closeMemoryModal()" title="Close">&times;</button>
6848
7027
  </div>
6849
7028
  <div class="memory-modal-body" id="memoryModalBody"></div>
6850
7029
  </div>