@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.
@@ -5,7 +5,8 @@ return `<!DOCTYPE html>
5
5
  <head>
6
6
  <meta charset="UTF-8">
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
- <title>OpenClaw Memory - Powered by MemOS</title>
8
+ <link rel="icon" href="https://statics.memtensor.com.cn/logo/color-m.svg" type="image/svg+xml">
9
+ <title>MemOS 记忆</title>
9
10
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
12
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
@@ -58,8 +59,9 @@ return `<!DOCTYPE html>
58
59
  [data-theme="light"] .analytics-card.amber .ac-value{color:#d97706}
59
60
  [data-theme="light"] .analytics-section{background:#fff;border-color:var(--border);box-shadow:0 1px 3px rgba(0,0,0,.04)}
60
61
  [data-theme="light"] .analytics-section::before{background:none}
61
- [data-theme="light"] .chart-bar{box-shadow:none}
62
- [data-theme="light"] .chart-bar:hover{box-shadow:0 2px 8px rgba(79,70,229,.15)}
62
+ [data-theme="light"] .chart-bar{box-shadow:0 1px 4px rgba(99,102,241,.12)}
63
+ [data-theme="light"] .chart-bar:hover{box-shadow:0 3px 12px rgba(79,70,229,.2)}
64
+ [data-theme="light"] .chart-bars::before{opacity:.3}
63
65
  [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)}
64
66
  [data-theme="light"] .tool-chart-tooltip .tt-time{color:#a5b4fc}
65
67
  [data-theme="light"] .tool-chart-tooltip .tt-val{color:#e8eaed}
@@ -105,9 +107,15 @@ input,textarea,select{font-family:inherit;font-size:inherit}
105
107
  /* ─── App Layout (dark dashboard, same as www) ─── */
106
108
  .app{display:none;flex-direction:column;min-height:100vh}
107
109
  .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)}
108
- .topbar .brand{display:flex;align-items:center;gap:10px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em;flex-shrink:0}
109
- .topbar .brand .icon{width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:22px;background:none;border-radius:0}
110
+ .topbar .brand{display:flex;align-items:center;gap:8px;font-weight:700;font-size:15px;color:var(--text);letter-spacing:-.02em;flex-shrink:0}
111
+ .topbar .brand .icon{width:24px;height:24px;display:flex;align-items:center;justify-content:center;font-size:18px;background:none;border-radius:0}
112
+ .topbar .brand .brand-title{font-size:13px;font-weight:500;opacity:.7}
113
+ .topbar .brand .brand-col{display:flex;flex-direction:column;gap:0}
114
+ .topbar .brand .brand-powered{font-size:9px;font-weight:500;color:var(--text-sec);opacity:.55;letter-spacing:.02em;line-height:1}
110
115
  .topbar .brand .sub{font-weight:400;color:var(--text-muted);font-size:11px}
116
+ .memos-logo{display:inline-flex;align-items:center}
117
+ .memos-logo svg{height:28px;width:28px}
118
+ .brand-sep{width:1px;height:20px;background:var(--border);opacity:.5;margin:0 2px}
111
119
  .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}
112
120
  [data-theme="light"] .version-badge{background:rgba(0,0,0,.05);border-color:rgba(0,0,0,.08);color:var(--text-sec)}
113
121
  .topbar-center{flex:1;display:flex;justify-content:center}
@@ -286,20 +294,67 @@ input,textarea,select{font-family:inherit;font-size:inherit}
286
294
  .memory-card.dedup-inactive{opacity:.55;border-style:dashed}
287
295
  .memory-card.dedup-inactive:hover{opacity:.85}
288
296
  .dedup-target-link{font-size:11px;color:var(--pri);cursor:pointer;text-decoration:underline;margin-left:4px}
289
- .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)}
297
+ .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)}
290
298
  .memory-modal-overlay.show{display:flex}
291
- .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}
292
- @keyframes modalIn{from{opacity:0;transform:scale(.95) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}
293
- .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}
294
- .memory-modal-body{padding:20px;overflow-y:auto;flex:1}
295
- .modal-memory-card{display:flex;flex-direction:column;gap:14px}
296
- .modal-header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
297
- .modal-field{display:flex;flex-direction:column;gap:4px}
298
- .modal-field-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-sec)}
299
- .modal-field-val{font-size:13px;color:var(--text);line-height:1.5}
300
- .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}
301
- [data-theme="light"] .modal-field-content{background:rgba(0,0,0,.04)}
302
- .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)}
299
+ [data-theme="light"] .memory-modal-overlay{background:rgba(0,0,0,.45)}
300
+ .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}
301
+ [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)}
302
+ @keyframes mmSlideIn{from{opacity:0;transform:scale(.92) translateY(20px)}to{opacity:1;transform:scale(1) translateY(0)}}
303
+ .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}
304
+ @keyframes mmGradientMove{0%{background-position:0% 50%}100%{background-position:200% 50%}}
305
+ .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}
306
+ [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%)}
307
+ .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}
308
+ .memory-modal-title .mm-tl{display:flex;align-items:center;gap:8px}
309
+ .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)}
310
+ [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)}
311
+ .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}
312
+ .memory-modal-title .mm-close:hover{background:rgba(255,77,77,.12);border-color:rgba(255,77,77,.2);color:#f87171;transform:rotate(90deg)}
313
+ [data-theme="light"] .memory-modal-title .mm-close{border-color:rgba(0,0,0,.06);background:rgba(0,0,0,.03)}
314
+ [data-theme="light"] .memory-modal-title .mm-close:hover{background:rgba(255,77,77,.08);border-color:rgba(255,77,77,.15)}
315
+ .memory-modal-body{position:relative;z-index:2;padding:0;overflow-y:auto;flex:1}
316
+ .mm-hero{padding:0 24px 20px}
317
+ .mm-hero-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:14px}
318
+ .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)}
319
+ .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)}
320
+ .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)}
321
+ .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)}
322
+ [data-theme="light"] .mm-role-chip{color:#6366f1}
323
+ [data-theme="light"] .mm-role-chip.user{color:#059669}
324
+ [data-theme="light"] .mm-role-chip.assistant{color:#6366f1}
325
+ [data-theme="light"] .mm-role-chip.system{color:#d97706}
326
+ .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}
327
+ .mm-dedup-chip.duplicate{background:rgba(239,68,68,.12);color:#fca5a5;border:1px solid rgba(239,68,68,.2)}
328
+ .mm-dedup-chip.merged{background:rgba(251,191,36,.12);color:#fcd34d;border:1px solid rgba(251,191,36,.2)}
329
+ [data-theme="light"] .mm-dedup-chip.duplicate{color:#dc2626}
330
+ [data-theme="light"] .mm-dedup-chip.merged{color:#d97706}
331
+ .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}
332
+ .mm-id::before{content:'\u{1F517}';font-size:9px}
333
+ .mm-id:hover{background:rgba(99,102,241,.12);border-color:rgba(99,102,241,.25);color:var(--text)}
334
+ [data-theme="light"] .mm-id{background:rgba(0,0,0,.03);border-color:rgba(0,0,0,.06)}
335
+ [data-theme="light"] .mm-id:hover{background:rgba(99,102,241,.08);border-color:rgba(99,102,241,.15)}
336
+ .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}
337
+ .mm-summary::before{content:'';position:absolute;left:0;top:12px;bottom:12px;width:3px;border-radius:2px;background:linear-gradient(180deg,#6366f1,#8b5cf6)}
338
+ [data-theme="light"] .mm-summary{background:rgba(99,102,241,.02);border-color:rgba(99,102,241,.06)}
339
+ .mm-section{padding:0 24px 16px}
340
+ .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}
341
+ .mm-section-label::before{content:'';width:6px;height:6px;border-radius:50%;background:linear-gradient(135deg,#6366f1,#8b5cf6);flex-shrink:0}
342
+ .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}
343
+ [data-theme="light"] .mm-content{background:rgba(0,0,0,.03);border-color:rgba(0,0,0,.06)}
344
+ .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)}
345
+ [data-theme="light"] .mm-meta{border-top-color:rgba(0,0,0,.04);background:rgba(0,0,0,.015)}
346
+ .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}
347
+ .mm-meta-chip:hover{background:rgba(255,255,255,.06);border-color:rgba(99,102,241,.15)}
348
+ [data-theme="light"] .mm-meta-chip{background:rgba(0,0,0,.02);border-color:rgba(0,0,0,.04)}
349
+ [data-theme="light"] .mm-meta-chip:hover{background:rgba(99,102,241,.04);border-color:rgba(99,102,241,.1)}
350
+ .mm-meta-chip strong{color:var(--text-muted);font-weight:600;font-size:9px;text-transform:uppercase;letter-spacing:.06em}
351
+ .mm-meta-chip span{color:var(--text);font-weight:500;font-size:12px;word-break:break-all}
352
+ .mm-dedup{padding:0 24px 16px}
353
+ .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}
354
+ .mm-dedup-box::before{content:'\u26A0\uFE0F';flex-shrink:0;font-size:14px}
355
+ .mm-footer{padding:12px 24px 16px;display:flex;align-items:center;justify-content:center}
356
+ .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}
357
+ .mm-footer .dedup-target-link:hover{background:rgba(99,102,241,.12);border-color:rgba(99,102,241,.3)}
303
358
  [data-theme="light"] .merge-history{background:rgba(0,0,0,.04)}
304
359
  [data-theme="light"] .merge-history-item{border-bottom-color:rgba(0,0,0,.06)}
305
360
  .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)}
@@ -739,8 +794,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
739
794
  .test-result.ok{color:#22c55e}
740
795
  .test-result.fail{color:var(--rose)}
741
796
  .test-result.loading{color:var(--text-muted)}
742
- .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)}
743
- .settings-actions .btn{min-width:110px;padding:10px 20px;font-size:13px}
797
+ .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}
798
+ .settings-actions .btn{flex:0 0 auto;min-width:0;padding:8px 24px;font-size:13px}
744
799
  .settings-actions .btn-primary{background:rgba(99,102,241,.08);color:var(--pri);border:1px solid rgba(99,102,241,.25);font-weight:600}
745
800
  .settings-actions .btn-primary:hover{background:rgba(99,102,241,.14);border-color:var(--pri)}
746
801
  [data-theme="light"] .settings-actions .btn-primary{background:rgba(79,70,229,.06);color:#4f46e5;border:1px solid rgba(79,70,229,.2)}
@@ -806,18 +861,20 @@ input,textarea,select{font-family:inherit;font-size:inherit}
806
861
  .analytics-section::before{display:none}
807
862
  .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}
808
863
  .analytics-section h3 .icon{font-size:14px;opacity:.6}
809
- .chart-bars{display:flex;align-items:flex-end;gap:4px;padding:8px 0;overflow-x:auto;justify-content:center}
810
- .chart-bar-wrap{flex:1;min-width:28px;max-width:80px;display:flex;flex-direction:column;align-items:center;gap:4px;position:relative}
811
- .chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:stretch}
812
- .chart-bar-wrap:hover .chart-bar{opacity:1}
864
+ .chart-bars{display:flex;align-items:flex-end;gap:1px;padding:12px 4px 4px;min-height:200px;position:relative;width:100%}
865
+ .chart-bars::before{content:'';position:absolute;left:0;right:0;bottom:24px;height:1px;background:var(--border);opacity:.4}
866
+ .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}
867
+ .chart-bar-col{width:100%;height:160px;display:flex;flex-direction:column;justify-content:flex-end;align-items:center}
868
+ .chart-bar-wrap:hover .chart-bar{opacity:1;filter:brightness(1.2);transform:scaleY(1.02);transform-origin:bottom}
813
869
  .chart-bar-wrap:hover .chart-bar-label{color:var(--text)}
814
870
  .chart-bar-wrap:hover .chart-tip{opacity:1;transform:translateX(-50%) translateY(0)}
815
- .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}
816
- .chart-bar{width:100%;border-radius:3px 3px 1px 1px;background:#818cf8;opacity:.75;transition:all .2s ease}
817
- .chart-bar.violet{background:#6366f1}
818
- .chart-bar.green{background:var(--green)}
819
- .chart-bar.zero{background:var(--border);opacity:.3;border-radius:2px}
820
- .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}
871
+ .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)}
872
+ [data-theme="light"] .chart-tip{background:rgba(17,24,39,.9)}
873
+ .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)}
874
+ .chart-bar.violet{background:linear-gradient(180deg,#8b5cf6 0%,#6366f1 100%)}
875
+ .chart-bar.green{background:linear-gradient(180deg,#34d399 0%,#10b981 100%);box-shadow:0 1px 4px rgba(16,185,129,.12)}
876
+ .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}
877
+ .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}
821
878
  .chart-legend{display:flex;gap:14px;margin-top:12px;flex-wrap:wrap;font-size:11px;color:var(--text-sec);font-weight:500}
822
879
  .chart-legend span{display:inline-flex;align-items:center;gap:5px}
823
880
  .chart-legend .dot{width:8px;height:8px;border-radius:2px}
@@ -871,7 +928,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
871
928
  [data-theme="light"] .auth-theme-toggle .theme-icon-dark{display:none}
872
929
 
873
930
  @media(max-width:1100px){.analytics-cards{grid-template-columns:repeat(3,1fr)}}
874
- @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}}
931
+ @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}}
875
932
  </style>
876
933
  </head>
877
934
  <body>
@@ -883,7 +940,9 @@ input,textarea,select{font-family:inherit;font-size:inherit}
883
940
  <button class="auth-theme-toggle" onclick="toggleLang()" aria-label="Switch language"><span data-i18n="lang.switch">EN</span></button>
884
941
  </div>
885
942
  <div class="auth-card">
886
- <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>
943
+ <div class="logo" style="display:flex;flex-direction:column;align-items:center;gap:10px">
944
+ <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>
945
+ </div>
887
946
  <h1 data-i18n="title">OpenClaw Memory</h1>
888
947
  <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px" data-i18n="subtitle">Powered by MemOS</p>
889
948
  <p data-i18n="setup.desc">Set a password to protect your memories</p>
@@ -901,7 +960,9 @@ input,textarea,select{font-family:inherit;font-size:inherit}
901
960
  <button class="auth-theme-toggle" onclick="toggleLang()" aria-label="Switch language"><span data-i18n="lang.switch">EN</span></button>
902
961
  </div>
903
962
  <div class="auth-card">
904
- <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>
963
+ <div class="logo" style="display:flex;flex-direction:column;align-items:center;gap:10px">
964
+ <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>
965
+ </div>
905
966
  <h1 data-i18n="title">OpenClaw Memory</h1>
906
967
  <p style="font-size:12px;color:var(--text-sec);margin-bottom:6px" data-i18n="subtitle">Powered by MemOS</p>
907
968
  <p data-i18n="login.desc">Enter your password to access memories</p>
@@ -953,8 +1014,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
953
1014
  <div class="app" id="app">
954
1015
  <div class="topbar">
955
1016
  <div class="brand">
956
- <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>
957
- <span data-i18n="title">OpenClaw Memory</span>${vBadge}
1017
+ <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>
1018
+ <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}
958
1019
  </div>
959
1020
  <div class="topbar-center">
960
1021
  <nav class="nav-tabs">
@@ -1006,9 +1067,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1006
1067
  <option value="public" data-i18n="filter.public">Public</option>
1007
1068
  </select>
1008
1069
  <select id="memorySearchScope" class="filter-select" onchange="onMemoryScopeChange()">
1009
- <option value="local">Local</option>
1010
- <option value="group">Group</option>
1011
- <option value="all">All</option>
1070
+ <option value="local" data-i18n="scope.local">Local</option>
1071
+ <option value="all" data-i18n="scope.hub">Hub</option>
1012
1072
  </select>
1013
1073
  </div>
1014
1074
  <div class="search-meta" id="searchMeta"></div>
@@ -1051,9 +1111,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1051
1111
  <button class="filter-chip" data-task-status="completed" onclick="setTaskStatusFilter(this,'completed')" data-i18n="tasks.status.completed">Completed</button>
1052
1112
  <button class="filter-chip" data-task-status="skipped" onclick="setTaskStatusFilter(this,'skipped')" data-i18n="tasks.status.skipped">Skipped</button>
1053
1113
  <select id="taskSearchScope" class="scope-select" onchange="onTaskScopeChange()">
1054
- <option value="local">Local</option>
1055
- <option value="group">Group</option>
1056
- <option value="all">All</option>
1114
+ <option value="local" data-i18n="scope.local">Local</option>
1115
+ <option value="all" data-i18n="scope.hub">Hub</option>
1057
1116
  </select>
1058
1117
  <button class="btn btn-sm btn-ghost" onclick="loadTasks()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
1059
1118
  </div>
@@ -1095,8 +1154,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1095
1154
  <input type="text" id="skillSearchInput" placeholder="Search skills..." data-i18n-ph="skills.search.placeholder" oninput="debounceSkillSearch()">
1096
1155
  <select id="skillSearchScope" class="scope-select" onchange="onSkillScopeChange()">
1097
1156
  <option value="local" data-i18n="scope.local">Local</option>
1098
- <option value="group" data-i18n="scope.group">Group</option>
1099
- <option value="all" data-i18n="scope.all">All</option>
1157
+ <option value="all" data-i18n="scope.hub">Hub</option>
1100
1158
  </select>
1101
1159
  </div>
1102
1160
  <div class="search-meta" id="skillSearchMeta"></div>
@@ -1411,16 +1469,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1411
1469
  <button class="btn btn-sm" id="btnRoleHub" onclick="selectSharingRole('hub')" data-i18n="settings.hub.role.hub">Hub (Server)</button>
1412
1470
  <button class="btn btn-sm" id="btnRoleClient" onclick="selectSharingRole('client')" data-i18n="settings.hub.role.client">Client (Connect to Hub)</button>
1413
1471
  </div>
1414
- <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>
1472
+ <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>
1415
1473
  </div>
1416
1474
 
1417
1475
  <div id="hubModeConfig" style="display:none">
1418
- <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)">
1419
- <div style="font-weight:700;color:var(--text);margin-bottom:4px" data-i18n="settings.hub.hubSteps.title">Quick Setup (3 steps)</div>
1420
- <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>
1421
- <div><span style="color:var(--accent)">2.</span> <span data-i18n="settings.hub.hubSteps.s2">Click "Save Settings", then restart OpenClaw gateway</span></div>
1422
- <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>
1423
- </div>
1476
+ <input type="hidden" id="cfgHubTeamToken" value="">
1424
1477
  <div class="settings-grid">
1425
1478
  <div class="settings-field">
1426
1479
  <label data-i18n="settings.hub.port">Hub Port</label>
@@ -1432,15 +1485,13 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1432
1485
  <input type="text" id="cfgHubTeamName" placeholder="My Team">
1433
1486
  <div class="field-hint" data-i18n="settings.hub.teamName.hint">Your team display name</div>
1434
1487
  </div>
1435
- <div class="settings-field full-width">
1436
- <label data-i18n="settings.hub.teamToken">Team Token</label>
1437
- <input type="text" id="cfgHubTeamToken" readonly style="background:var(--bg);cursor:pointer" onclick="this.select();document.execCommand('copy');toast(t('settings.hub.tokenCopied'),'success')">
1438
- <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>
1439
- </div>
1440
1488
  </div>
1441
- <div id="hubShareInfo" class="hub-info-card hic-share" style="display:none;margin-top:14px">
1442
- <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>
1443
- <div id="hubShareInfoContent" class="hic-grid"></div>
1489
+ <div id="hubShareInfo" style="display:none;margin-top:14px;background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px 18px">
1490
+ <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>
1491
+ <div id="hubShareInfoContent" style="display:grid;grid-template-columns:auto 1fr;gap:6px 12px;font-size:12px;align-items:center"></div>
1492
+ </div>
1493
+ <div style="margin-top:16px;display:flex;align-items:center;gap:12px">
1494
+ <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>
1444
1495
  </div>
1445
1496
  </div>
1446
1497
 
@@ -1462,11 +1513,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1462
1513
  <input type="text" id="cfgClientTeamToken" placeholder="">
1463
1514
  <div class="field-hint" data-i18n="settings.hub.teamTokenClient.hint">Get this from your Hub admin to join the team</div>
1464
1515
  </div>
1465
- <div class="settings-field">
1466
- <label data-i18n="settings.hub.userToken">User Token</label>
1467
- <input type="text" id="cfgClientUserToken" placeholder="">
1468
- <div class="field-hint" data-i18n="settings.hub.userToken.hint">Usually auto-obtained after joining. Leave blank in most cases.</div>
1469
- </div>
1516
+ <input type="hidden" id="cfgClientUserToken" value="">
1470
1517
  </div>
1471
1518
  <div style="margin-top:12px">
1472
1519
  <button class="btn btn-sm btn-primary" id="btnTestHubConn" onclick="testHubConnection()" data-i18n="settings.hub.testConnection">Test Connection</button>
@@ -1535,13 +1582,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1535
1582
  </div>
1536
1583
  <div class="admin-tabs" id="adminTabsBar">
1537
1584
  <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>
1538
- <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>
1539
1585
  <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>
1540
1586
  <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>
1541
1587
  <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>
1542
1588
  </div>
1543
1589
  <div class="admin-panel active" id="adminUsersPanel"></div>
1544
- <div class="admin-panel" id="adminGroupsPanel"></div>
1545
1590
  <div class="admin-panel" id="adminSharedMemoriesPanel"></div>
1546
1591
  <div class="admin-panel" id="adminMemoriesPanel"></div>
1547
1592
  <div class="admin-panel" id="adminSkillsPanel"></div>
@@ -1746,7 +1791,7 @@ let _embeddingWarningShown=false;
1746
1791
  /* ─── i18n ─── */
1747
1792
  const I18N={
1748
1793
  en:{
1749
- 'title':'OpenClaw Memory',
1794
+ 'title':'MemOS',
1750
1795
  'subtitle':'Powered by MemOS',
1751
1796
  'setup.desc':'Set a password to protect your memories',
1752
1797
  'setup.pw':'Enter a password (4+ characters)',
@@ -2066,7 +2111,7 @@ const I18N={
2066
2111
  'settings.hub.role':'Role',
2067
2112
  'settings.hub.role.hub':'Hub (Server)',
2068
2113
  'settings.hub.role.client':'Client (Connect to Hub)',
2069
- 'settings.hub.role.hint':'Hub = run the server; Client = connect to one.',
2114
+ 'settings.hub.role.hint':'Hub = run the server; Client = connect to one. These two modes are mutually exclusive.',
2070
2115
  'settings.hub.port':'Hub Port',
2071
2116
  'settings.hub.port.hint':'Port for Hub service. Default: 18800',
2072
2117
  'settings.hub.teamName':'Team Name',
@@ -2103,13 +2148,31 @@ const I18N={
2103
2148
  'sidebar.hub':'\u{1F310} Team Sharing',
2104
2149
  'sharing.sidebar.connected':'Connected',
2105
2150
  'sharing.sidebar.disconnected':'Disconnected',
2151
+ 'sharing.sidebar.pending':'Pending Approval',
2152
+ 'sharing.sidebar.rejected':'Rejected',
2106
2153
  'sharing.sidebar.starting':'Starting...',
2107
2154
  'sharing.sidebar.notConfigured':'Not configured',
2108
2155
  'sharing.sidebar.identity':'Identity:',
2109
2156
  'sharing.sidebar.admin':'Admin',
2110
2157
  'sharing.sidebar.targetHub':'Target Hub:',
2158
+ 'sharing.pendingApproval.hint':'Your join request has been submitted. Please wait for the Hub admin to approve.',
2159
+ 'sharing.rejected.hint':'Your join request was rejected by the Hub admin. Please contact the admin or retry.',
2160
+ 'sharing.retryJoin':'Retry Join',
2161
+ 'sharing.retryJoin.hint':'Clears local data and re-submits the join request',
2162
+ 'sharing.retryJoin.confirm':'This will clear your current connection and re-submit a join request. Continue?',
2163
+ 'sharing.retryJoin.success':'Join request re-submitted. Waiting for admin approval.',
2164
+ 'sharing.retryJoin.fail':'Failed to retry join',
2165
+ 'sharing.cannotJoinSelf':'Cannot join your own Hub. Please enter a remote Hub address.',
2166
+ 'scope.hub':'Hub',
2167
+ 'memory.detail.title':'Memory Detail',
2168
+ 'memory.detail.loading':'Loading...',
2169
+ 'memory.detail.notFound':'Memory not found',
2170
+ 'memory.detail.copyId':'Click to copy ID',
2171
+ 'memory.detail.created':'Created ',
2172
+ 'memory.detail.updated':'Updated ',
2173
+ 'memory.detail.viewTarget':'View target: ',
2111
2174
  'admin.title':'Hub Admin Panel',
2112
- 'admin.subtitle':'Manage team members, groups, and shared resources',
2175
+ 'admin.subtitle':'Manage team members and shared resources',
2113
2176
  'admin.refresh':'\u21BB Refresh',
2114
2177
  'admin.tab.users':'Users',
2115
2178
  'admin.tab.groups':'Groups',
@@ -2155,6 +2218,7 @@ const I18N={
2155
2218
  'admin.membersCount':'Members ({n}):',
2156
2219
  'admin.noMembersYet':'No members yet.',
2157
2220
  'admin.loadFailed':'Failed to load admin data: ',
2221
+ 'admin.noPermission':'You do not have admin permissions to access this panel.',
2158
2222
  'admin.groupsFailed':'Failed to load groups: ',
2159
2223
  'toast.userApproved':'User approved',
2160
2224
  'toast.userRejected':'User rejected',
@@ -2283,7 +2347,7 @@ const I18N={
2283
2347
  'update.dismiss':'Dismiss'
2284
2348
  },
2285
2349
  zh:{
2286
- 'title':'OpenClaw 记忆',
2350
+ 'title':'MemOS 记忆',
2287
2351
  'subtitle':'由 MemOS 驱动',
2288
2352
  'setup.desc':'设置密码以保护你的记忆数据',
2289
2353
  'setup.pw':'输入密码(至少4位)',
@@ -2603,7 +2667,7 @@ const I18N={
2603
2667
  'settings.hub.role':'角色',
2604
2668
  'settings.hub.role.hub':'Hub(服务端)',
2605
2669
  'settings.hub.role.client':'Client(连接到 Hub)',
2606
- 'settings.hub.role.hint':'Hub = 本机做服务端;Client = 连接别人的服务端。',
2670
+ 'settings.hub.role.hint':'Hub = 本机做服务端;Client = 连接别人的服务端。两种模式互斥,不能同时使用。',
2607
2671
  'settings.hub.port':'Hub 端口',
2608
2672
  'settings.hub.port.hint':'Hub 服务端口,默认 18800',
2609
2673
  'settings.hub.teamName':'团队名称',
@@ -2640,13 +2704,31 @@ const I18N={
2640
2704
  'sidebar.hub':'\u{1F310} 团队共享',
2641
2705
  'sharing.sidebar.connected':'已连接',
2642
2706
  'sharing.sidebar.disconnected':'已断开',
2707
+ 'sharing.sidebar.pending':'等待审核',
2708
+ 'sharing.sidebar.rejected':'已拒绝',
2643
2709
  'sharing.sidebar.starting':'启动中...',
2644
2710
  'sharing.sidebar.notConfigured':'未配置',
2645
2711
  'sharing.sidebar.identity':'身份:',
2646
2712
  'sharing.sidebar.admin':'管理员',
2647
2713
  'sharing.sidebar.targetHub':'目标 Hub:',
2714
+ 'sharing.pendingApproval.hint':'加入申请已提交,请等待 Hub 管理员审核通过。',
2715
+ 'sharing.rejected.hint':'您的加入申请已被 Hub 管理员拒绝,请联系管理员或重新申请。',
2716
+ 'sharing.retryJoin':'重新申请',
2717
+ 'sharing.retryJoin.hint':'清除本地连接数据并重新提交加入申请',
2718
+ 'sharing.retryJoin.confirm':'这将清除当前连接数据并重新提交加入申请,是否继续?',
2719
+ 'sharing.retryJoin.success':'加入申请已重新提交,请等待管理员审核。',
2720
+ 'sharing.retryJoin.fail':'重新申请失败',
2721
+ 'sharing.cannotJoinSelf':'不能加入自己的 Hub,请输入远程 Hub 地址。',
2722
+ 'scope.hub':'Hub',
2723
+ 'memory.detail.title':'记忆详情',
2724
+ 'memory.detail.loading':'加载中...',
2725
+ 'memory.detail.notFound':'未找到该记忆',
2726
+ 'memory.detail.copyId':'点击复制 ID',
2727
+ 'memory.detail.created':'创建于 ',
2728
+ 'memory.detail.updated':'更新于 ',
2729
+ 'memory.detail.viewTarget':'查看目标: ',
2648
2730
  'admin.title':'Hub 管理面板',
2649
- 'admin.subtitle':'管理团队成员、分组和共享资源',
2731
+ 'admin.subtitle':'管理团队成员和共享资源',
2650
2732
  'admin.refresh':'\u21BB 刷新',
2651
2733
  'admin.tab.users':'用户',
2652
2734
  'admin.tab.groups':'分组',
@@ -2692,6 +2774,7 @@ const I18N={
2692
2774
  'admin.membersCount':'成员({n}):',
2693
2775
  'admin.noMembersYet':'暂无成员。',
2694
2776
  'admin.loadFailed':'加载管理数据失败:',
2777
+ 'admin.noPermission':'您没有管理员权限,无法访问此面板。',
2695
2778
  'admin.groupsFailed':'加载分组失败:',
2696
2779
  'toast.userApproved':'用户已批准',
2697
2780
  'toast.userRejected':'用户已拒绝',
@@ -2935,6 +3018,20 @@ function selectSharingRole(role){
2935
3018
  document.getElementById('btnRoleClient').className='btn btn-sm'+(role==='client'?' btn-primary':'');
2936
3019
  document.getElementById('hubModeConfig').style.display=role==='hub'?'block':'none';
2937
3020
  document.getElementById('clientModeConfig').style.display=role==='client'?'block':'none';
3021
+ var sp=document.getElementById('sharingStatusPanel');
3022
+ var tp=document.getElementById('sharingTeamPanel');
3023
+ var ap=document.getElementById('sharingAdminPanel');
3024
+ if(role==='client'){
3025
+ if(sp) sp.style.display='none';
3026
+ if(tp) tp.style.display='none';
3027
+ if(ap) ap.style.display='none';
3028
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3029
+ if(adminTab) adminTab.style.display='none';
3030
+ }else{
3031
+ if(sp) sp.style.display='';
3032
+ if(tp) tp.style.display='';
3033
+ if(ap) ap.style.display='';
3034
+ }
2938
3035
  if(role==='hub'){
2939
3036
  var tk=document.getElementById('cfgHubTeamToken');
2940
3037
  if(!tk.value.trim()) tk.value=_genToken(18);
@@ -2947,16 +3044,20 @@ function updateHubShareInfo(){
2947
3044
  var panel=document.getElementById('hubShareInfo');
2948
3045
  var content=document.getElementById('hubShareInfoContent');
2949
3046
  if(!panel||!content) return;
2950
- var token=document.getElementById('cfgHubTeamToken').value.trim();
3047
+ var tokenEl=document.getElementById('cfgHubTeamToken');
3048
+ var token=tokenEl?tokenEl.value.trim():'';
2951
3049
  var port=document.getElementById('cfgHubPort').value.trim()||'18800';
2952
3050
  if(!token||_sharingRole!=='hub'){panel.style.display='none';return;}
2953
3051
  panel.style.display='block';
3052
+ 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';
2954
3053
  var renderShare=function(ip){
2955
3054
  var addr=ip?(ip+':'+esc(port)):('&lt;'+t('settings.hub.shareInfo.yourIP')+'&gt;:'+esc(port));
2956
3055
  var tip=t('settings.hub.shareInfo.clickCopy');
2957
3056
  content.innerHTML=
2958
- '<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>'+
2959
- '<span class="hic-label">Team Token</span><span class="hic-value mono" onclick="navigator.clipboard.writeText(this.textContent)" title="'+tip+'">'+esc(token)+'</span>';
3057
+ '<span style="font-size:11px;color:var(--text-muted);font-weight:500">Hub '+t('settings.hub.hubAddress')+'</span>'+
3058
+ '<span style="'+cpStyle+'" onclick="navigator.clipboard.writeText(this.textContent);toast(&#39;Copied!&#39;,&#39;success&#39;)" title="'+tip+'">'+addr+'</span>'+
3059
+ '<span style="font-size:11px;color:var(--text-muted);font-weight:500">Team Token</span>'+
3060
+ '<span style="'+cpStyle+'" onclick="navigator.clipboard.writeText(this.textContent);toast(&#39;Copied!&#39;,&#39;success&#39;)" title="'+tip+'">'+esc(token)+'</span>';
2960
3061
  };
2961
3062
  if(_cachedLocalIP){renderShare(_cachedLocalIP);return;}
2962
3063
  renderShare('');
@@ -2970,6 +3071,15 @@ async function testHubConnection(){
2970
3071
  var addr=document.getElementById('cfgClientHubAddress').value.trim();
2971
3072
  if(!addr){result.innerHTML='<span style="color:var(--rose)">\u274C '+t('settings.hub.test.noAddr')+'</span>';return;}
2972
3073
  btn.disabled=true;result.innerHTML=t('settings.hub.test.testing');
3074
+ try{
3075
+ var ipsData=await fetch('/api/local-ips').then(function(r){return r.json();});
3076
+ var localAddrs=['127.0.0.1','localhost','0.0.0.0'].concat(ipsData.ips||[]);
3077
+ var parsed=new URL(addr.indexOf('://')>-1?addr:'http://'+addr);
3078
+ if(localAddrs.indexOf(parsed.hostname)>=0){
3079
+ result.innerHTML='<span style="color:var(--rose)">\u274C '+t('sharing.cannotJoinSelf')+'</span>';
3080
+ btn.disabled=false;return;
3081
+ }
3082
+ }catch(e){}
2973
3083
  try{
2974
3084
  var url=addr.match(/^https?:\\/\\//)?addr:'http://'+addr;
2975
3085
  url=url.replace(/\\/+$/,'');
@@ -2978,7 +3088,8 @@ async function testHubConnection(){
2978
3088
  if(d.ok){
2979
3089
  result.innerHTML='<span style="color:var(--green)">\u2705 '+t('settings.hub.test.ok')+(d.teamName?' — '+esc(d.teamName):'')+'</span>';
2980
3090
  }else{
2981
- result.innerHTML='<span style="color:var(--rose)">\u274C '+(d.error||t('settings.hub.test.fail'))+'</span>';
3091
+ var errMsg=d.error==='cannot_join_self'?t('sharing.cannotJoinSelf'):(d.error||t('settings.hub.test.fail'));
3092
+ result.innerHTML='<span style="color:var(--rose)">\u274C '+errMsg+'</span>';
2982
3093
  }
2983
3094
  }catch(e){
2984
3095
  result.innerHTML='<span style="color:var(--rose)">\u274C '+esc(String(e))+'</span>';
@@ -3087,6 +3198,7 @@ function renderSharingSidebar(data){
3087
3198
  if(!statusEl||!hintEl) return;
3088
3199
  if(!data||!data.enabled){
3089
3200
  if(section) section.style.display='none';
3201
+ window._isHubAdmin=false;
3090
3202
  return;
3091
3203
  }
3092
3204
  if(section) section.style.display='block';
@@ -3095,25 +3207,43 @@ function renderSharingSidebar(data){
3095
3207
  if(!badgeEl)return;
3096
3208
  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>';
3097
3209
  }
3098
- if(conn.connected&&conn.user){
3099
- var groups=(conn.user.groups||[]).map(function(g){return g.name;}).join(', ')||'';
3210
+ if(data.role==='hub'){
3211
+ setBadge('#34d399',t('sharing.sidebar.connected'),true);
3212
+ statusEl.innerHTML='';
3213
+ hintEl.textContent='';
3214
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3215
+ if(adminTab) adminTab.style.display='';
3216
+ }else if(conn.pendingApproval&&conn.user){
3217
+ setBadge('#fbbf24',t('sharing.sidebar.pending'),false);
3218
+ var html='<div class="info-grid">';
3219
+ html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username)+'</span>';
3220
+ if(conn.teamName) html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName)+'</span>';
3221
+ html+='</div>';
3222
+ statusEl.innerHTML=html;
3223
+ hintEl.textContent=t('sharing.pendingApproval.hint');
3224
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3225
+ if(adminTab) adminTab.style.display='none';
3226
+ }else if(conn.rejected&&conn.user){
3227
+ setBadge('#ef4444',t('sharing.sidebar.rejected'),false);
3228
+ var html='<div class="info-grid">';
3229
+ html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username||'-')+'</span>';
3230
+ if(conn.teamName) html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName)+'</span>';
3231
+ html+='</div>';
3232
+ statusEl.innerHTML=html;
3233
+ hintEl.textContent=t('sharing.rejected.hint');
3234
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3235
+ if(adminTab) adminTab.style.display='none';
3236
+ }else if(conn.connected&&conn.user){
3100
3237
  var isAdmin=conn.user.role==='admin';
3101
3238
  setBadge('#34d399',t('sharing.sidebar.connected'),true);
3102
3239
  var html='<div class="info-grid">';
3103
3240
  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>';
3104
3241
  html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName||'-')+'</span>';
3105
- if(groups) html+='<span class="label">'+t('sharing.groups')+'</span><span class="value">'+esc(groups)+'</span>';
3106
3242
  html+='</div>';
3107
3243
  statusEl.innerHTML=html;
3108
3244
  hintEl.innerHTML='';
3109
3245
  var adminTab=document.querySelector('.tab[data-view="admin"]');
3110
3246
  if(adminTab) adminTab.style.display=isAdmin?'':'none';
3111
- }else if(data.role==='hub'){
3112
- setBadge('#fbbf24',t('sharing.sidebar.starting'),false);
3113
- statusEl.innerHTML='';
3114
- hintEl.textContent=t('sharing.hubMode.hint');
3115
- var adminTab=document.querySelector('.tab[data-view="admin"]');
3116
- if(adminTab) adminTab.style.display=(data.admin&&data.admin.canManageUsers)?'':'none';
3117
3247
  }else if(data.clientConfigured){
3118
3248
  setBadge('#ef4444',t('sharing.sidebar.disconnected'),false);
3119
3249
  statusEl.innerHTML='<div style="font-size:11px;color:var(--text-muted)">'+t('sharing.sidebar.targetHub')+' '+esc(data.hubUrl||'')+'</div>';
@@ -3136,49 +3266,75 @@ function renderSharingSettings(data){
3136
3266
  }
3137
3267
  var conn=data.connection||{};
3138
3268
  var user=conn.user||{};
3139
- var groups=(user.groups||[]).map(function(g){return g.name;}).join(', ')||'-';
3140
- var connBadge=conn.connected
3141
- ?'<span class="hic-badge connected"><span class="hic-dot green"></span>'+t('sharing.sidebar.connected')+'</span>'
3142
- :'<span class="hic-badge disconnected"><span class="hic-dot red"></span>'+t('sharing.sidebar.disconnected')+'</span>';
3143
- var roleLabel=data.role==='hub'?t('sharing.role.hub'):t('sharing.role.client');
3144
- 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">';
3145
- sh+='<span class="hic-label">'+t('sharing.mode')+'</span><span class="hic-value">'+roleLabel+'</span>';
3146
- if(conn.connected&&user.username){
3147
- sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value" style="display:flex;align-items:center;gap:6px">'+
3148
- '<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" />'+
3149
- (user.role==='admin'?' <span class="hic-badge admin">admin</span>':'')+
3150
- '<button class="btn btn-sm" onclick="updateHubUsername()" style="padding:2px 10px;font-size:11px">'+t('sharing.saveUsername')+'</button>'+
3151
- '</span>';
3269
+ var actualRole=data.role||_sharingRole||'client';
3270
+ if(data.role) _sharingRole=data.role;
3271
+ var isAdmin=(data.admin&&data.admin.canManageUsers)||(conn.connected&&user.role==='admin')||(actualRole==='hub');
3272
+ window._isHubAdmin=isAdmin;
3273
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3274
+ if(adminTab) adminTab.style.display=isAdmin?'':'none';
3275
+ var hubAdminBtn=document.getElementById('hubAdminEntryBtn');
3276
+
3277
+ if(actualRole==='hub'){
3278
+ statusEl.innerHTML='';teamEl.innerHTML='';adminEl.innerHTML='';
3279
+ if(hubAdminBtn) hubAdminBtn.style.display=isAdmin?'':'none';
3280
+ return;
3152
3281
  }
3153
- sh+='</div></div>';
3154
- statusEl.innerHTML=sh;
3155
-
3156
- var th='';
3157
- if(conn.connected&&conn.user){
3158
- 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">';
3159
- th+='<span class="hic-label">'+t('sharing.team')+'</span><span class="hic-value">'+esc(conn.teamName||'-')+'</span>';
3160
- th+='<span class="hic-label">'+t('sharing.groups')+'</span><span class="hic-value">'+esc(groups)+'</span>';
3161
- th+='</div>';
3162
- if(user.role==='admin'){
3163
- th+='<div class="hic-actions">'+
3164
- '<button class="btn btn-sm" onclick="loadGroupManager()">'+t('sharing.manageGroups')+'</button>'+
3165
- '<button class="btn btn-sm btn-primary" onclick="switchView(\\\'admin\\\')">'+t('sharing.openAdmin')+'</button></div>';
3282
+
3283
+ if(actualRole==='client'){
3284
+ statusEl.style.display='none';teamEl.style.display='none';adminEl.style.display='none';
3285
+ if(adminTab) adminTab.style.display='none';
3286
+ if(hubAdminBtn) hubAdminBtn.style.display='none';
3287
+
3288
+ var connBadge;
3289
+ if(conn.pendingApproval){
3290
+ connBadge='<span class="hic-badge pending"><span class="hic-dot amber"></span>'+t('sharing.sidebar.pending')+'</span>';
3291
+ }else if(conn.rejected){
3292
+ connBadge='<span class="hic-badge disconnected"><span class="hic-dot red"></span>'+t('sharing.sidebar.rejected')+'</span>';
3293
+ }else if(conn.connected){
3294
+ connBadge='<span class="hic-badge connected"><span class="hic-dot green"></span>'+t('sharing.sidebar.connected')+'</span>';
3295
+ }else{
3296
+ connBadge='<span class="hic-badge disconnected"><span class="hic-dot red"></span>'+t('sharing.sidebar.disconnected')+'</span>';
3166
3297
  }
3167
- th+='<div id="groupManagerPanel" style="margin-top:12px;display:none"></div></div>';
3168
- }else if(data.role==='hub'&&!data.clientConfigured){
3169
- th='<div class="hub-info-card hic-team"><div class="hic-title"><span class="hic-icon">\u{1F465}</span>'+t('settings.hub.team')+'</div>';
3170
- th+='<div class="hic-empty">'+t('sharing.hubNotConfigured')+'</div></div>';
3298
+ statusEl.style.display='';
3299
+ 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">';
3300
+ if(conn.pendingApproval&&user.username){
3301
+ sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value">'+esc(user.username)+'</span>';
3302
+ sh+='</div><div class="hic-empty" style="color:#f59e0b">'+t('sharing.pendingApproval.hint')+'</div></div>';
3303
+ }else if(conn.rejected){
3304
+ if(user.username) 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:#ef4444">'+t('sharing.rejected.hint')+'</div>'+
3306
+ '<div style="margin-top:10px;padding:0 16px 14px"><button class="btn btn-sm btn-primary" onclick="retryHubJoin()">'+t('sharing.retryJoin')+'</button>'+
3307
+ '<span style="font-size:11px;color:var(--text-muted);margin-left:8px">'+t('sharing.retryJoin.hint')+'</span></div></div>';
3308
+ }else if(conn.connected&&user.username){
3309
+ sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value" style="display:flex;align-items:center;gap:6px">'+
3310
+ '<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" />'+
3311
+ '<button class="btn btn-sm" onclick="updateHubUsername()" style="padding:2px 10px;font-size:11px">'+t('sharing.saveUsername')+'</button>'+
3312
+ '</span>';
3313
+ sh+='<span class="hic-label">'+t('sharing.team')+'</span><span class="hic-value">'+esc(conn.teamName||'-')+'</span>';
3314
+ sh+='</div></div>';
3315
+ }else{
3316
+ sh+='</div></div>';
3317
+ }
3318
+ statusEl.innerHTML=sh;
3319
+ teamEl.innerHTML='';adminEl.innerHTML='';
3320
+ return;
3171
3321
  }
3172
- teamEl.innerHTML=th;
3173
3322
 
3174
- var isAdmin=(data.admin&&data.admin.canManageUsers)||(conn.connected&&user.role==='admin')||(data.role==='hub');
3175
- if(isAdmin){
3176
- 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>';
3177
- }else{
3178
- adminEl.innerHTML='';
3179
- }
3180
- var adminTab=document.querySelector('.tab[data-view="admin"]');
3181
- if(adminTab) adminTab.style.display=isAdmin?'':'none';
3323
+ statusEl.innerHTML='';teamEl.innerHTML='';adminEl.innerHTML='';
3324
+ }
3325
+
3326
+ async function retryHubJoin(){
3327
+ if(!confirm(t('sharing.retryJoin.confirm'))) return;
3328
+ try{
3329
+ var r=await fetch('/api/sharing/retry-join',{method:'POST',headers:{'Content-Type':'application/json'}});
3330
+ var d=await r.json();
3331
+ if(d.ok){
3332
+ toast(t('sharing.retryJoin.success'),'success');
3333
+ setTimeout(function(){location.reload();},1500);
3334
+ }else{
3335
+ toast(d.error||t('sharing.retryJoin.fail'),'error');
3336
+ }
3337
+ }catch(e){toast(t('sharing.retryJoin.fail')+': '+e.message,'error');}
3182
3338
  }
3183
3339
 
3184
3340
  async function updateHubUsername(){
@@ -3212,6 +3368,7 @@ async function updateHubUsername(){
3212
3368
  }
3213
3369
 
3214
3370
  async function loadSharingPendingUsers(){
3371
+ if(_sharingRole==='client') return;
3215
3372
  var el=document.getElementById('sharingAdminPanel');
3216
3373
  if(!el) return;
3217
3374
  el.innerHTML=t('sharing.loading');
@@ -3419,24 +3576,27 @@ function switchAdminTab(tab,btn){
3419
3576
  }
3420
3577
 
3421
3578
  async function loadAdminData(){
3579
+ if(!window._isHubAdmin){
3580
+ var statsEl=document.getElementById('adminStats');
3581
+ if(statsEl) statsEl.innerHTML='<div class="admin-empty">'+t('admin.noPermission')+'</div>';
3582
+ return;
3583
+ }
3422
3584
  try{
3423
- var [usersR,groupsR,tasksR,skillsR,pendingR,memoriesR]=await Promise.all([
3585
+ var [usersR,tasksR,skillsR,pendingR,memoriesR]=await Promise.all([
3424
3586
  fetch('/api/sharing/users').then(function(r){return r.json();}),
3425
- fetch('/api/sharing/groups').then(function(r){return r.json();}),
3426
3587
  fetch('/api/admin/shared-tasks').then(function(r){return r.json();}),
3427
3588
  fetch('/api/admin/shared-skills').then(function(r){return r.json();}),
3428
3589
  fetch('/api/sharing/pending-users').then(function(r){return r.json();}),
3429
3590
  fetch('/api/admin/shared-memories').then(function(r){return r.json();})
3430
3591
  ]);
3431
3592
  adminDataCache.users=Array.isArray(usersR.users)?usersR.users:[];
3432
- adminDataCache.groups=Array.isArray(groupsR.groups)?groupsR.groups:[];
3593
+ adminDataCache.groups=[];
3433
3594
  adminDataCache.tasks=Array.isArray(tasksR.tasks)?tasksR.tasks:[];
3434
3595
  adminDataCache.skills=Array.isArray(skillsR.skills)?skillsR.skills:[];
3435
3596
  adminDataCache.memories=Array.isArray(memoriesR.memories)?memoriesR.memories:[];
3436
3597
  var pending=Array.isArray(pendingR.users)?pendingR.users:[];
3437
3598
  renderAdminStats(pending.length);
3438
3599
  renderAdminUsers(adminDataCache.users, pending);
3439
- renderAdminGroups(adminDataCache.groups);
3440
3600
  renderAdminMemories(adminDataCache.tasks);
3441
3601
  renderAdminSkills(adminDataCache.skills);
3442
3602
  renderAdminSharedMemories(adminDataCache.memories);
@@ -3452,12 +3612,10 @@ function renderAdminStats(pendingCount){
3452
3612
  el.innerHTML=
3453
3613
  '<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>'+
3454
3614
  '<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>'+
3455
- '<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>'+
3456
3615
  '<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>'+
3457
3616
  '<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>'+
3458
3617
  '<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>';
3459
3618
  var tc=document.getElementById('adminTabCountUsers');if(tc)tc.textContent=adminDataCache.users.length+pendingCount;
3460
- tc=document.getElementById('adminTabCountGroups');if(tc)tc.textContent=adminDataCache.groups.length;
3461
3619
  tc=document.getElementById('adminTabCountMemories');if(tc)tc.textContent=(adminDataCache.memories||[]).length;
3462
3620
  tc=document.getElementById('adminTabCountTasks');if(tc)tc.textContent=adminDataCache.tasks.length;
3463
3621
  tc=document.getElementById('adminTabCountSkills');if(tc)tc.textContent=adminDataCache.skills.length;
@@ -3620,10 +3778,9 @@ function renderAdminMemories(tasks){
3620
3778
  for(var i=0;i<tasks.length;i++){
3621
3779
  var tk=tasks[i];
3622
3780
  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>'+
3623
- '<span class="admin-badge '+(tk.visibility==='public'?'public':'group')+'">'+esc(tk.visibility||'public')+'</span></div>'+
3781
+ '</div>'+
3624
3782
  '<div class="admin-card-meta">'+
3625
3783
  t('admin.owner')+esc(tk.ownerName||tk.sourceUserId||'unknown')+
3626
- (tk.groupName?' \u00B7 '+t('admin.group')+esc(tk.groupName):'')+
3627
3784
  (tk.chunkCount!=null?' \u00B7 '+t('admin.chunks')+tk.chunkCount:'')+
3628
3785
  ' \u00B7 '+t('admin.updated')+new Date(tk.updatedAt||tk.createdAt).toLocaleDateString()+
3629
3786
  '</div>'+
@@ -3655,11 +3812,10 @@ function renderAdminSkills(skills){
3655
3812
  for(var i=0;i<skills.length;i++){
3656
3813
  var s=skills[i];
3657
3814
  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>'+
3658
- '<span class="admin-badge '+(s.visibility==='public'?'public':'group')+'">'+esc(s.visibility||'public')+'</span></div>'+
3815
+ '</div>'+
3659
3816
  '<div class="admin-card-meta">'+
3660
3817
  (s.description?esc(s.description)+'<br>':'')+
3661
3818
  t('admin.owner')+esc(s.ownerName||s.sourceUserId||'unknown')+
3662
- (s.groupName?' \u00B7 '+t('admin.group')+esc(s.groupName):'')+
3663
3819
  (s.version!=null?' \u00B7 '+t('admin.version')+s.version:'')+
3664
3820
  (s.qualityScore!=null?' \u00B7 '+t('admin.quality')+s.qualityScore:'')+
3665
3821
  '</div>'+
@@ -3691,10 +3847,9 @@ function renderAdminSharedMemories(memories){
3691
3847
  for(var i=0;i<memories.length;i++){
3692
3848
  var m=memories[i];
3693
3849
  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>'+
3694
- '<span class="admin-badge '+(m.visibility==='public'?'public':'group')+'">'+esc(m.visibility||'public')+'</span></div>'+
3850
+ '</div>'+
3695
3851
  '<div class="admin-card-meta">'+
3696
3852
  t('admin.owner')+esc(m.ownerName||m.sourceUserId||'unknown')+
3697
- (m.groupName?' \u00B7 '+t('admin.group')+esc(m.groupName):'')+
3698
3853
  (m.kind?' \u00B7 Kind: '+esc(m.kind):'')+
3699
3854
  (m.role?' \u00B7 Role: '+esc(m.role):'')+
3700
3855
  ' \u00B7 '+t('admin.updated')+new Date(m.updatedAt||m.createdAt).toLocaleDateString()+
@@ -3883,21 +4038,16 @@ function renderTaskShareActions(task){
3883
4038
  const isShared=!!current;
3884
4039
  var statusHtml='';
3885
4040
  if(isShared){
3886
- var scopeLabel=current==='group'?'Group':'Public';
3887
- 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>';
4041
+ 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>';
3888
4042
  }
3889
4043
  el.innerHTML=statusHtml+
3890
- '<select id="taskShareScope" class="filter-select" style="min-width:120px">'+
3891
- '<option value="group"'+(current==='group'?' selected':'')+'>Share to Group</option>'+
3892
- '<option value="public"'+(current==='public'?' selected':'')+'>Share to Public</option>'+
3893
- '</select>'+
3894
4044
  '<button class="btn btn-sm" onclick="shareCurrentTask()">'+(isShared?t('share.updateBtn'):t('share.shareBtn'))+'</button>'+
3895
4045
  (isShared?'<button class="btn btn-sm btn-ghost" onclick="unshareCurrentTask()">'+t('share.unshareBtn')+'</button>':'');
3896
4046
  }
3897
4047
 
3898
4048
  async function shareCurrentTask(){
3899
4049
  if(!currentTaskDetail) return;
3900
- const visibility=document.getElementById('taskShareScope').value||'public';
4050
+ const visibility='public';
3901
4051
  try{
3902
4052
  const r=await fetch('/api/sharing/tasks/share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({taskId:currentTaskDetail.id,visibility:visibility})});
3903
4053
  const d=await r.json();
@@ -3922,21 +4072,16 @@ function renderSkillShareActions(skill){
3922
4072
  const isShared=!!current;
3923
4073
  var statusHtml='';
3924
4074
  if(isShared){
3925
- var scopeLabel=current==='group'?'Group':'Public';
3926
- 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>';
4075
+ 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>';
3927
4076
  }
3928
4077
  el.innerHTML=statusHtml+
3929
- '<select id="skillShareScope" class="filter-select" style="min-width:120px">'+
3930
- '<option value="group"'+(current==='group'?' selected':'')+'>Share to Group</option>'+
3931
- '<option value="public"'+(current==='public'?' selected':'')+'>Share to Public</option>'+
3932
- '</select>'+
3933
4078
  '<button class="btn btn-sm" onclick="shareCurrentSkill()">'+(isShared?t('share.updateBtn'):t('share.shareBtn'))+'</button>'+
3934
4079
  (isShared?'<button class="btn btn-sm btn-ghost" onclick="unshareCurrentSkill()">'+t('share.unshareBtn')+'</button>':'');
3935
4080
  }
3936
4081
 
3937
4082
  async function shareCurrentSkill(){
3938
4083
  if(!currentSkillDetail) return;
3939
- const visibility=document.getElementById('skillShareScope').value||'public';
4084
+ const visibility='public';
3940
4085
  try{
3941
4086
  const r=await fetch('/api/sharing/skills/share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({skillId:currentSkillDetail.id,visibility:visibility})});
3942
4087
  const d=await r.json();
@@ -5188,6 +5333,7 @@ async function saveConfig(){
5188
5333
  cfg.sharing.hub={port:hubPort?Number(hubPort):18800};
5189
5334
  if(hubTeamName) cfg.sharing.hub.teamName=hubTeamName;
5190
5335
  if(hubTeamToken) cfg.sharing.hub.teamToken=hubTeamToken;
5336
+ cfg.sharing.client={hubAddress:'',userToken:'',teamToken:''};
5191
5337
  }
5192
5338
  if(sharingEnabled&&_sharingRole==='client'){
5193
5339
  var clientAddr=document.getElementById('cfgClientHubAddress').value.trim();
@@ -5197,6 +5343,17 @@ async function saveConfig(){
5197
5343
  if(clientAddr) cfg.sharing.client.hubAddress=clientAddr;
5198
5344
  if(clientTeamToken) cfg.sharing.client.teamToken=clientTeamToken;
5199
5345
  if(clientUserToken) cfg.sharing.client.userToken=clientUserToken;
5346
+ cfg.sharing.hub={port:18800,teamName:'',teamToken:''};
5347
+ if(clientAddr){
5348
+ try{
5349
+ var ips=await fetch('/api/local-ips').then(function(r){return r.json();});
5350
+ var localAddrs=['127.0.0.1','localhost','0.0.0.0'].concat(ips.ips||[]);
5351
+ var parsed=new URL(clientAddr.indexOf('://')>-1?clientAddr:'http://'+clientAddr);
5352
+ if(localAddrs.indexOf(parsed.hostname)>=0){
5353
+ done();toast(t('sharing.cannotJoinSelf'),'error');return;
5354
+ }
5355
+ }catch(e){}
5356
+ }
5200
5357
  }
5201
5358
 
5202
5359
  // 1) Embedding model is required
@@ -5374,7 +5531,7 @@ function fillDays(rows,days){
5374
5531
  const row=map.get(dateStr)||{};
5375
5532
  out.push({date:dateStr,count:row.count??0,list:row.list??0,search:row.search??0,total:(row.list??0)+(row.search??0)});
5376
5533
  }
5377
- if(days>21){
5534
+ if(days>60){
5378
5535
  const weeks=[];let i=0;
5379
5536
  while(i<out.length){
5380
5537
  const chunk=out.slice(i,i+7);
@@ -5392,19 +5549,23 @@ function fillDays(rows,days){
5392
5549
  }
5393
5550
 
5394
5551
  function renderBars(el,data,valueKey,H){
5395
- const vals=data.map(d=>d[valueKey]??0);
5396
- 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;}
5397
- const max=Math.max(1,...vals);
5398
- const nonZero=vals.filter(v=>v>0).length;
5399
- const barStyle=data.length<=7?'min-width:40px;max-width:120px':'';
5400
- el.innerHTML=data.map(r=>{
5401
- const v=r[valueKey]??0;
5402
- const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
5552
+ var vals=data.map(function(d){return d[valueKey]??0;});
5553
+ 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;}
5554
+ var max=Math.max(1,Math.max.apply(null,vals));
5555
+ var n=data.length;
5556
+ var labelStep=n<=7?1:(n<=14?2:5);
5557
+ el.innerHTML=data.map(function(r,idx){
5558
+ var v=r[valueKey]??0;
5559
+ var rawDate=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):'');
5560
+ var showLabel=(idx%labelStep===0)||(idx===n-1);
5561
+ var label=showLabel?rawDate:'';
5562
+ var tipDate=r.date.length>=10?r.date.slice(5,10):'';
5563
+ var tipText=tipDate?(tipDate+': '+v):(''+v);
5403
5564
  if(v===0){
5404
- 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>';
5565
+ 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>';
5405
5566
  }
5406
- const h=Math.max(8,Math.round((v/max)*H));
5407
- 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>';
5567
+ var h=Math.max(8,Math.round((v/max)*H));
5568
+ 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>';
5408
5569
  }).join('');
5409
5570
  }
5410
5571
 
@@ -5415,25 +5576,31 @@ function renderChartWrites(rows){
5415
5576
  }
5416
5577
 
5417
5578
  function renderChartCalls(rows){
5418
- const el=document.getElementById('chartCalls');
5419
- const filled=fillDays(rows?.map(r=>({date:r.date,list:r.list,search:r.search})),metricsDays);
5420
- const vals=filled.map(f=>f.total);
5421
- 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;}
5422
- const max=Math.max(1,...vals);
5423
- const H=160;
5424
- el.innerHTML=filled.map(r=>{
5425
- const label=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):r.date);
5579
+ var el=document.getElementById('chartCalls');
5580
+ var filled=fillDays(rows?.map(function(r){return {date:r.date,list:r.list,search:r.search};}),metricsDays);
5581
+ var vals=filled.map(function(f){return f.total;});
5582
+ 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;}
5583
+ var max=Math.max(1,Math.max.apply(null,vals));
5584
+ var H=160;
5585
+ var n=filled.length;
5586
+ var labelStep=n<=7?1:(n<=14?2:5);
5587
+ el.innerHTML=filled.map(function(r,idx){
5588
+ var rawDate=r.date.includes('~')?r.date:(r.date.length>5?r.date.slice(5):'');
5589
+ var showLabel=(idx%labelStep===0)||(idx===n-1);
5590
+ var label=showLabel?rawDate:'';
5591
+ var tipDate=r.date.length>=10?r.date.slice(5,10):'';
5426
5592
  if(r.total===0){
5427
- 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>';
5593
+ var tipZero=tipDate?(tipDate+': 0'):'0';
5594
+ 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>';
5428
5595
  }
5429
- const totalH=Math.max(8,Math.round((r.total/max)*H));
5430
- const listH=r.list?Math.max(3,Math.round((r.list/r.total)*totalH)):0;
5431
- const searchH=r.search?totalH-listH:0;
5432
- const tip='List: '+r.list+', Search: '+r.search;
5433
- let bars='';
5596
+ var totalH=Math.max(8,Math.round((r.total/max)*H));
5597
+ var listH=r.list?Math.max(4,Math.round((r.list/r.total)*totalH)):0;
5598
+ var searchH=r.search?totalH-listH:0;
5599
+ var tip=(tipDate?tipDate+' - ':'')+'List: '+r.list+', Search: '+r.search;
5600
+ var bars='';
5434
5601
  if(searchH>0) bars+='<div class="chart-bar violet" style="height:'+searchH+'px"></div>';
5435
5602
  if(listH>0) bars+='<div class="chart-bar" style="height:'+listH+'px"></div>';
5436
- 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>';
5603
+ 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>';
5437
5604
  }).join('');
5438
5605
  }
5439
5606
 
@@ -6011,38 +6178,50 @@ function scrollToMemory(targetId){
6011
6178
  }
6012
6179
  showMemoryModal(targetId);
6013
6180
  }
6181
+ function fmtModalDate(v){
6182
+ if(!v) return '-';
6183
+ var d=new Date(v);
6184
+ if(isNaN(d.getTime())) return '-';
6185
+ return d.toLocaleString(dateLoc());
6186
+ }
6014
6187
  async function showMemoryModal(chunkId){
6015
- const overlay=document.getElementById('memoryModal');
6016
- const body=document.getElementById('memoryModalBody');
6017
- body.innerHTML='<div style="text-align:center;padding:40px;color:var(--text-sec)">Loading...</div>';
6188
+ var overlay=document.getElementById('memoryModal');
6189
+ var body=document.getElementById('memoryModalBody');
6190
+ 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>';
6018
6191
  overlay.classList.add('show');
6019
6192
  try{
6020
- const res=await fetch('/api/memory/'+encodeURIComponent(chunkId));
6021
- if(!res.ok){body.innerHTML='<div style="text-align:center;padding:40px;color:#f87171">Memory not found</div>';return;}
6022
- const data=await res.json();
6023
- const m=data.memory;
6024
- const role=(m.role||'unknown').toUpperCase();
6025
- const roleCls=(m.role||'').toLowerCase();
6026
- const ds=m.dedup_status||'active';
6027
- const time=new Date(m.created_at).toLocaleString(dateLoc());
6028
- const updated=m.updated_at?new Date(m.updated_at).toLocaleString(dateLoc()):'';
6029
- let html='<div class="modal-memory-card">';
6030
- html+='<div class="modal-header-row"><span class="role-tag '+roleCls+'">'+role+'</span>';
6031
- if(ds!=='active') html+='<span class="dedup-badge '+(ds==='duplicate'?'duplicate':'merged')+'">'+ds+'</span>';
6032
- html+='</div>';
6033
- 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>';
6034
- 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>';
6035
- html+='<div class="modal-field"><div class="modal-field-label">Content</div><pre class="modal-field-content">'+esc(m.content||'')+'</pre></div>';
6036
- html+='<div class="modal-meta-row">';
6037
- html+='<span><strong>Session:</strong> '+esc(m.session_key||'')+'</span>';
6038
- html+='<span><strong>Created:</strong> '+time+'</span>';
6039
- if(updated) html+='<span><strong>Updated:</strong> '+updated+'</span>';
6040
- html+='</div>';
6041
- 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>';
6042
- 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>';
6043
- html+='</div>';
6044
- body.innerHTML=html;
6045
- }catch(e){body.innerHTML='<div style="text-align:center;padding:40px;color:#f87171">Error: '+esc(String(e))+'</div>';}
6193
+ var res=await fetch('/api/memory/'+encodeURIComponent(chunkId));
6194
+ 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;}
6195
+ var data=await res.json();
6196
+ var m=data.memory;
6197
+ var role=(m.role||'unknown').toUpperCase();
6198
+ var roleCls=(m.role||'').toLowerCase();
6199
+ var ds=m.dedup_status||'active';
6200
+ var h='<div class="mm-hero">';
6201
+ h+='<div class="mm-hero-row">';
6202
+ h+='<span class="mm-role-chip '+roleCls+'">'+role+'</span>';
6203
+ if(ds!=='active') h+='<span class="mm-dedup-chip '+(ds==='duplicate'?'duplicate':'merged')+'">'+(ds==='duplicate'?'\u274C':'\u{1F504}')+' '+ds+'</span>';
6204
+ 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>';
6205
+ h+='</div>';
6206
+ if(m.summary) h+='<div class="mm-summary">'+esc(m.summary)+'</div>';
6207
+ h+='</div>';
6208
+ if(m.content){
6209
+ h+='<div class="mm-section"><div class="mm-section-label">Content</div><pre class="mm-content">'+esc(m.content)+'</pre></div>';
6210
+ }
6211
+ h+='<div class="mm-meta">';
6212
+ if(m.session_key) h+='<div class="mm-meta-chip"><strong>Session</strong><span>'+esc(m.session_key.slice(0,12))+'</span></div>';
6213
+ h+='<div class="mm-meta-chip"><strong>'+t('memory.detail.created')+'</strong><span>'+fmtModalDate(m.created_at)+'</span></div>';
6214
+ if(m.updated_at) h+='<div class="mm-meta-chip"><strong>'+t('memory.detail.updated')+'</strong><span>'+fmtModalDate(m.updated_at)+'</span></div>';
6215
+ if(m.kind) h+='<div class="mm-meta-chip"><strong>Kind</strong><span>'+esc(m.kind)+'</span></div>';
6216
+ h+='</div>';
6217
+ if(m.dedup_reason){
6218
+ h+='<div class="mm-dedup"><div class="mm-dedup-box">'+esc(m.dedup_reason)+'</div></div>';
6219
+ }
6220
+ if(m.dedup_target&&m.dedup_target!==chunkId){
6221
+ 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>';
6222
+ }
6223
+ body.innerHTML=h;
6224
+ }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>';}
6046
6225
  }
6047
6226
  function closeMemoryModal(){document.getElementById('memoryModal').classList.remove('show');}
6048
6227
 
@@ -6840,8 +7019,8 @@ checkAuth();
6840
7019
  <div class="memory-modal-overlay" id="memoryModal" onclick="if(event.target===this)closeMemoryModal()">
6841
7020
  <div class="memory-modal">
6842
7021
  <div class="memory-modal-title">
6843
- <span>Memory Detail</span>
6844
- <button class="btn btn-sm btn-ghost" onclick="closeMemoryModal()" style="font-size:16px;padding:2px 8px">&times;</button>
7022
+ <div class="mm-tl"><div class="mm-tl-icon">\u{1F9E0}</div><span data-i18n="memory.detail.title">Memory Detail</span></div>
7023
+ <button class="mm-close" onclick="closeMemoryModal()" title="Close">&times;</button>
6845
7024
  </div>
6846
7025
  <div class="memory-modal-body" id="memoryModalBody"></div>
6847
7026
  </div>