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

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.
@@ -976,8 +976,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
976
976
  .team-guide-steps li::marker{color:var(--pri);font-weight:700;font-size:11px}
977
977
  .team-guide-opt .btn-guide{font-size:11px;padding:5px 14px;border-radius:6px;font-weight:600;border:1px solid rgba(99,102,241,.25);background:rgba(99,102,241,.08);color:var(--pri);cursor:pointer;transition:background .15s,border-color .15s}
978
978
  .team-guide-opt .btn-guide:hover{background:rgba(99,102,241,.14);border-color:var(--pri)}
979
- .team-guide-dismiss{position:absolute;top:10px;right:12px;background:none;border:none;color:var(--text-muted);font-size:15px;cursor:pointer;padding:4px;line-height:1;opacity:.5;transition:opacity .15s}
980
- .team-guide-dismiss:hover{opacity:1}
981
979
  [data-theme="light"] .team-guide{background:linear-gradient(135deg,rgba(6,182,212,.03),rgba(79,70,229,.02));border-color:rgba(6,182,212,.15)}
982
980
  [data-theme="light"] .team-guide-opt{box-shadow:0 1px 3px rgba(0,0,0,.03)}
983
981
  [data-theme="light"] .team-guide-opt:hover{box-shadow:0 4px 16px rgba(0,0,0,.04)}
@@ -1641,9 +1639,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1641
1639
  </div>
1642
1640
  </div>
1643
1641
  <div class="settings-card-body">
1644
- <!-- team setup guide (inside Hub card) -->
1642
+ <!-- team setup guide (inside Hub card) — always visible when sharing is not configured -->
1645
1643
  <div class="team-guide" id="teamSetupGuide">
1646
- <button class="team-guide-dismiss" onclick="dismissTeamGuide()" title="Dismiss">&times;</button>
1647
1644
  <div class="team-guide-title">\u{1F680} <span data-i18n="guide.title">Get Started with Team Collaboration</span></div>
1648
1645
  <div class="team-guide-subtitle" data-i18n="guide.subtitle">MemOS supports team memory sharing. Choose one of the following options to enable collaboration, or continue using local-only mode.</div>
1649
1646
  <div class="team-guide-options">
@@ -2126,6 +2123,8 @@ const I18N={
2126
2123
  'notif.userJoin':'New user requests to join the team',
2127
2124
  'notif.userOnline':'User came online',
2128
2125
  'notif.userOffline':'User went offline',
2126
+ 'notif.membershipApproved':'Your team join request has been approved',
2127
+ 'notif.membershipRejected':'Your team join request has been declined',
2129
2128
  'notif.clearAll':'Clear all',
2130
2129
  'notif.timeAgo.just':'just now',
2131
2130
  'notif.timeAgo.min':'{n}m ago',
@@ -2448,6 +2447,9 @@ const I18N={
2448
2447
  'sharing.pendingApproval.hint':'Your join request has been submitted. Please wait for the team admin to approve.',
2449
2448
  'sharing.rejected.hint':'Your join request was rejected by the team admin. Please contact the admin or retry.',
2450
2449
  'sharing.removed.hint':'You have been removed from the team by the admin. You can re-apply to join.',
2450
+ 'sharing.joinTeam':'Join Team',
2451
+ 'sharing.joinSent.pending':'Join request sent! Waiting for admin approval.',
2452
+ 'sharing.joinSent.active':'Successfully joined the team!',
2451
2453
  'sharing.retryJoin':'Retry Join',
2452
2454
  'sharing.retryJoin.hint':'Clears local data and re-submits the join request',
2453
2455
  'sharing.retryJoin.confirm':'This will clear your current connection and re-submit a join request. Continue?',
@@ -2840,6 +2842,8 @@ const I18N={
2840
2842
  'notif.userJoin':'有新用户申请加入团队',
2841
2843
  'notif.userOnline':'用户上线了',
2842
2844
  'notif.userOffline':'用户下线了',
2845
+ 'notif.membershipApproved':'你的团队加入申请已通过',
2846
+ 'notif.membershipRejected':'你的团队加入申请已被拒绝',
2843
2847
  'notif.clearAll':'清除全部',
2844
2848
  'notif.timeAgo.just':'刚刚',
2845
2849
  'notif.timeAgo.min':'{n}分钟前',
@@ -3162,6 +3166,9 @@ const I18N={
3162
3166
  'sharing.pendingApproval.hint':'加入申请已提交,请等待团队管理员审核通过。',
3163
3167
  'sharing.rejected.hint':'您的加入申请已被团队管理员拒绝,请联系管理员或重新申请。',
3164
3168
  'sharing.removed.hint':'您已被管理员从团队中移除,可以重新申请加入。',
3169
+ 'sharing.joinTeam':'加入团队',
3170
+ 'sharing.joinSent.pending':'加入申请已发送,等待管理员审批。',
3171
+ 'sharing.joinSent.active':'成功加入团队!',
3165
3172
  'sharing.retryJoin':'重新申请',
3166
3173
  'sharing.retryJoin.hint':'清除本地连接数据并重新提交加入申请',
3167
3174
  'sharing.retryJoin.confirm':'这将清除当前连接数据并重新提交加入申请,是否继续?',
@@ -3606,6 +3613,9 @@ function selectSharingRole(role){
3606
3613
  var tn=document.getElementById('cfgHubTeamName');
3607
3614
  if(!tn.value.trim()) tn.value='My Team';
3608
3615
  }
3616
+ var card=document.getElementById('settingsSharingConfig');
3617
+ var saveBtn=card&&card.querySelector('.settings-actions .btn-primary');
3618
+ if(saveBtn&&typeof _hubSaveBtnLabel==='function') saveBtn.textContent=_hubSaveBtnLabel();
3609
3619
  }
3610
3620
  var _cachedLocalIP='';
3611
3621
  function updateHubShareInfo(){
@@ -3696,9 +3706,15 @@ function switchView(view){
3696
3706
  else if(view==='skills') loadSkills();
3697
3707
  else if(view==='analytics') loadMetrics();
3698
3708
  else if(view==='logs') loadLogs();
3699
- else if(view==='settings'){loadConfig();loadModelHealth();}
3709
+ else if(view==='settings'){loadConfig().then(function(){
3710
+ var sharingOn=document.getElementById('cfgSharingEnabled');
3711
+ var sharingNotEnabled=!sharingOn||!sharingOn.checked;
3712
+ if(sharingNotEnabled){
3713
+ switchSettingsTab('hub',document.querySelector('.settings-tab-btn[data-tab="hub"]'));
3714
+ }
3715
+ });loadModelHealth();}
3700
3716
  else if(view==='import'){if(!window._migrateRunning) migrateScan(false);}
3701
- else if(view==='admin'){loadAdminData();}
3717
+ else if(view==='admin'){_lastAdminFingerprint='';loadAdminData();}
3702
3718
  }
3703
3719
 
3704
3720
  function onMemoryScopeChange(){
@@ -3736,6 +3752,13 @@ function onTaskScopeChange(){
3736
3752
 
3737
3753
  var _clientPendingPollTimer=null;
3738
3754
  var _lastSharingConnStatus='';
3755
+ function _updateScopeSelectorsVisibility(hubAvailable){
3756
+ var ids=['memorySearchScope','taskSearchScope','skillSearchScope'];
3757
+ for(var i=0;i<ids.length;i++){
3758
+ var el=document.getElementById(ids[i]);
3759
+ if(el) el.style.display=hubAvailable?'':'none';
3760
+ }
3761
+ }
3739
3762
  async function loadSharingStatus(forcePending){
3740
3763
  try{
3741
3764
  const r=await fetch('/api/sharing/status');
@@ -3748,19 +3771,26 @@ async function loadSharingStatus(forcePending){
3748
3771
  if(!d||!d.enabled){
3749
3772
  if(_clientPendingPollTimer){clearInterval(_clientPendingPollTimer);_clientPendingPollTimer=null;}
3750
3773
  _lastSharingConnStatus='';
3774
+ _updateScopeSelectorsVisibility(false);
3751
3775
  return;
3752
3776
  }
3753
3777
  var conn=d.connection||{};
3754
3778
  var curStatus=conn.rejected?'rejected':conn.pendingApproval?'pending':conn.connected?'connected':'none';
3779
+ var hubActive=d.role==='hub'||curStatus==='connected';
3780
+ _updateScopeSelectorsVisibility(hubActive);
3755
3781
  if(_lastSharingConnStatus==='pending'&&curStatus==='rejected'){
3756
3782
  toast(t('sharing.rejected.toast'),'error');
3757
3783
  }
3758
3784
  if(_lastSharingConnStatus==='pending'&&curStatus==='connected'){
3759
3785
  toast(t('sharing.approved.toast'),'success');
3786
+ loadMemories();loadTasks();loadSkills();
3787
+ if(_notifSSE){_notifSSE.close();_notifSSE=null;_notifSSEConnected=false;}
3788
+ connectNotifSSE();
3789
+ loadNotifications();
3760
3790
  }
3761
3791
  _lastSharingConnStatus=curStatus;
3762
3792
  if(curStatus==='pending'&&!_clientPendingPollTimer){
3763
- _clientPendingPollTimer=setInterval(function(){loadSharingStatus(false);},10000);
3793
+ _clientPendingPollTimer=setInterval(function(){loadSharingStatus(false);},5000);
3764
3794
  }
3765
3795
  if(curStatus!=='pending'&&_clientPendingPollTimer){
3766
3796
  clearInterval(_clientPendingPollTimer);
@@ -3770,6 +3800,7 @@ async function loadSharingStatus(forcePending){
3770
3800
  renderSharingSidebar(null);
3771
3801
  renderSharingSettings(null);
3772
3802
  updateTeamGuide(null);
3803
+ _updateScopeSelectorsVisibility(false);
3773
3804
  }
3774
3805
  }
3775
3806
 
@@ -4040,7 +4071,7 @@ async function approveSharingUser(userId,username){
4040
4071
  try{
4041
4072
  const r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4042
4073
  const d=await r.json();
4043
- if(d.ok){toast(t('toast.userApproved'),'success');loadSharingPendingUsers();loadSharingStatus(true);} else {toast(d.error||t('toast.approveFail'),'error');}
4074
+ if(d.ok){toast(t('toast.userApproved'),'success');loadSharingPendingUsers();loadSharingStatus(true);_lastAdminFingerprint='';loadAdminData();} else {toast(d.error||t('toast.approveFail'),'error');}
4044
4075
  }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
4045
4076
  }
4046
4077
 
@@ -4048,24 +4079,17 @@ async function rejectSharingUser(userId,username){
4048
4079
  try{
4049
4080
  const r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4050
4081
  const d=await r.json();
4051
- if(d.ok){toast(t('toast.userRejected'),'success');loadSharingPendingUsers();} else {toast(d.error||t('toast.rejectFail'),'error');}
4082
+ if(d.ok){toast(t('toast.userRejected'),'success');loadSharingPendingUsers();_lastAdminFingerprint='';loadAdminData();} else {toast(d.error||t('toast.rejectFail'),'error');}
4052
4083
  }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
4053
4084
  }
4054
4085
 
4055
4086
  /* ─── Team Setup Guide ─── */
4056
- var TEAM_GUIDE_DISMISSED_KEY='memos-team-guide-dismissed';
4057
4087
  function updateTeamGuide(sharingData){
4058
4088
  var el=document.getElementById('teamSetupGuide');
4059
4089
  if(!el) return;
4060
- if(localStorage.getItem(TEAM_GUIDE_DISMISSED_KEY)==='1'){el.style.display='none';return;}
4061
4090
  var isConfigured=sharingData&&sharingData.enabled;
4062
4091
  el.style.display=isConfigured?'none':'block';
4063
4092
  }
4064
- function dismissTeamGuide(){
4065
- localStorage.setItem(TEAM_GUIDE_DISMISSED_KEY,'1');
4066
- var el=document.getElementById('teamSetupGuide');
4067
- if(el) el.style.display='none';
4068
- }
4069
4093
  function guideGoToHub(role){
4070
4094
  switchSettingsTab('hub',document.querySelector('.settings-tab-btn[data-tab="hub"]'));
4071
4095
  var chk=document.getElementById('cfgSharingEnabled');
@@ -4181,7 +4205,7 @@ async function loadAdminData(){
4181
4205
  var _newMemories=Array.isArray(memoriesR.memories)?memoriesR.memories:[];
4182
4206
  var pending=isAdmin?(Array.isArray(pendingR.users)?pendingR.users:[]):[];
4183
4207
  var _fp=_newUsers.length+':'+_newTasks.length+':'+_newSkills.length+':'+_newMemories.length+':'+pending.length
4184
- +':'+_newUsers.map(function(u){return u.id+'|'+(u.isOnline?1:0)+'|'+(u.role||'')+'|'+(u.username||'')+'|'+(u.memoryCount||0)+'|'+(u.taskCount||0)+'|'+(u.skillCount||0)}).join(',')
4208
+ +':'+_newUsers.map(function(u){return u.id+'|'+(u.isOnline?1:0)+'|'+(u.role||'')+'|'+(u.status||'')+'|'+(u.username||'')+'|'+(u.memoryCount||0)+'|'+(u.taskCount||0)+'|'+(u.skillCount||0)}).join(',')
4185
4209
  +':'+_newMemories.map(function(m){return m.id}).join(',')
4186
4210
  +':'+_newTasks.map(function(t){return t.id+'|'+(t.status||'')}).join(',')
4187
4211
  +':'+_newSkills.map(function(s){return s.id+'|'+(s.status||'')}).join(',')
@@ -4377,7 +4401,7 @@ async function adminApproveUser(userId,username){
4377
4401
  try{
4378
4402
  var r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4379
4403
  var d=await r.json();
4380
- if(d.ok){toast(t('toast.userApproved'),'success');loadAdminData();}else{toast(d.error||t('toast.approveFail'),'error');}
4404
+ if(d.ok){toast(t('toast.userApproved'),'success');_lastAdminFingerprint='';loadAdminData();}else{toast(d.error||t('toast.approveFail'),'error');}
4381
4405
  }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
4382
4406
  }
4383
4407
  async function adminRejectUser(userId){
@@ -4385,7 +4409,7 @@ async function adminRejectUser(userId){
4385
4409
  try{
4386
4410
  var r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
4387
4411
  var d=await r.json();
4388
- if(d.ok){toast(t('toast.userRejected'),'success');loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
4412
+ if(d.ok){toast(t('toast.userRejected'),'success');_lastAdminFingerprint='';loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
4389
4413
  }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
4390
4414
  }
4391
4415
  async function adminToggleRole(userId,newRole){
@@ -4394,7 +4418,7 @@ async function adminToggleRole(userId,newRole){
4394
4418
  try{
4395
4419
  var r=await fetch('/api/sharing/change-role',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,role:newRole})});
4396
4420
  var d=await r.json();
4397
- if(d.ok){toast(t('toast.roleChanged'),'success');loadAdminData();}
4421
+ if(d.ok){toast(t('toast.roleChanged'),'success');_lastAdminFingerprint='';loadAdminData();}
4398
4422
  else if(d.error==='cannot_demote_owner'){toast(t('admin.ownerHint'),'error');}
4399
4423
  else{toast(d.error||t('toast.roleChangeFail'),'error');}
4400
4424
  }catch(e){toast(t('toast.roleChangeFail')+': '+e.message,'error');}
@@ -4446,7 +4470,7 @@ async function adminRemoveUser(userId,username){
4446
4470
  try{
4447
4471
  var r=await fetch('/api/sharing/remove-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,cleanResources:clean})});
4448
4472
  var d=await r.json();
4449
- if(d.ok){toast(t('toast.userRemoved'),'success');loadAdminData();}
4473
+ if(d.ok){toast(t('toast.userRemoved'),'success');_lastAdminFingerprint='';loadAdminData();}
4450
4474
  else if(d.error==='cannot_remove_owner'){toast(t('admin.ownerHint'),'error');}
4451
4475
  else{toast(d.error||t('toast.removeFail'),'error');}
4452
4476
  }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
@@ -6578,16 +6602,16 @@ async function doSaveConfig(cfg, btnEl, savedId){
6578
6602
  function done(){btnEl.disabled=false;btnEl.textContent=t('settings.save');}
6579
6603
  try{
6580
6604
  const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});
6581
- if(r.status===401){done();toast(t('settings.session.expired'),'error');return false;}
6605
+ if(r.status===401){done();toast(t('settings.session.expired'),'error');return null;}
6582
6606
  if(!r.ok) throw new Error(await r.text());
6607
+ var data=await r.json().catch(function(){return {ok:true};});
6583
6608
  flashSaved(savedId);
6584
- toast(t('settings.saved'),'success');
6585
6609
  done();
6586
- return true;
6610
+ return data;
6587
6611
  }catch(e){
6588
6612
  toast(t('settings.save.fail')+': '+e.message,'error');
6589
6613
  done();
6590
- return false;
6614
+ return null;
6591
6615
  }
6592
6616
  }
6593
6617
 
@@ -6682,11 +6706,19 @@ async function saveModelsConfig(){
6682
6706
  await doSaveConfig(cfg, saveBtn, 'modelsSaved');
6683
6707
  }
6684
6708
 
6709
+ function _hubSaveBtnLabel(){
6710
+ var on=document.getElementById('cfgSharingEnabled');
6711
+ if(on&&on.checked&&_sharingRole==='client'){
6712
+ var prevClient=sharingStatusCache&&sharingStatusCache.enabled&&sharingStatusCache.role==='client';
6713
+ return prevClient?t('settings.save'):t('sharing.joinTeam');
6714
+ }
6715
+ return t('settings.save');
6716
+ }
6685
6717
  async function saveHubConfig(){
6686
6718
  var card=document.getElementById('settingsSharingConfig');
6687
6719
  var saveBtn=card.querySelector('.settings-actions .btn-primary');
6688
6720
  saveBtn.disabled=true;saveBtn.textContent=t('settings.test.loading');
6689
- function done(){saveBtn.disabled=false;saveBtn.textContent=t('settings.save');}
6721
+ function done(){saveBtn.disabled=false;saveBtn.textContent=_hubSaveBtnLabel();}
6690
6722
 
6691
6723
  const cfg={};
6692
6724
  var sharingEnabled=document.getElementById('cfgSharingEnabled').checked;
@@ -6753,14 +6785,25 @@ async function saveHubConfig(){
6753
6785
  if(!(await confirmModal(switchMsg,{danger:true}))){done();return;}
6754
6786
  }
6755
6787
 
6756
- var ok=await doSaveConfig(cfg, saveBtn, 'hubSaved');
6757
- if(ok){
6788
+ var result=await doSaveConfig(cfg, saveBtn, 'hubSaved');
6789
+ if(result){
6758
6790
  if(sharingEnabled&&_sharingRole==='hub'){
6759
6791
  var adminNameEl=document.getElementById('cfgHubAdminName');
6760
6792
  if(adminNameEl&&adminNameEl.value.trim()){
6761
6793
  try{await fetch('/api/sharing/update-username',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:adminNameEl.value.trim()})});}catch(e){}
6762
6794
  }
6763
6795
  }
6796
+ if(sharingEnabled&&_sharingRole==='client'&&result.joinStatus){
6797
+ if(result.joinStatus==='pending'){
6798
+ toast(t('sharing.joinSent.pending'),'success');
6799
+ }else if(result.joinStatus==='active'){
6800
+ toast(t('sharing.joinSent.active'),'success');
6801
+ }else{
6802
+ toast(t('settings.saved'),'success');
6803
+ }
6804
+ }else{
6805
+ toast(t('settings.saved'),'success');
6806
+ }
6764
6807
  _lastSidebarFingerprint='';
6765
6808
  _lastSettingsFingerprint='';
6766
6809
  _lastSharingConnStatus='';
@@ -7335,6 +7378,12 @@ function notifTypeText(n){
7335
7378
  if(n.type==='user_offline'){
7336
7379
  return t('notif.userOffline');
7337
7380
  }
7381
+ if(n.type==='membership_approved'){
7382
+ return t('notif.membershipApproved');
7383
+ }
7384
+ if(n.type==='membership_rejected'){
7385
+ return t('notif.membershipRejected');
7386
+ }
7338
7387
  return n.message||n.type;
7339
7388
  }
7340
7389
 
@@ -1232,7 +1232,7 @@ export class ViewerServer {
1232
1232
  body: JSON.stringify({ memory: { sourceChunkId: refreshedChunk.id, role: refreshedChunk.role, content: refreshedChunk.content, summary: refreshedChunk.summary, kind: refreshedChunk.kind, groupId: null, visibility: "public" } }),
1233
1233
  });
1234
1234
  if (!isLocalShared) this.store.markMemorySharedLocally(chunkId);
1235
- if (hubClient.userId) {
1235
+ if (hubClient.userId && this.ctx?.config?.sharing?.role === "hub") {
1236
1236
  const existing = this.store.getHubMemoryBySource(hubClient.userId, chunkId);
1237
1237
  this.store.upsertHubMemory({
1238
1238
  id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
@@ -2080,14 +2080,13 @@ export class ViewerServer {
2080
2080
  },
2081
2081
  }),
2082
2082
  });
2083
- const hubUserId = hubClient.userId;
2084
- if (hubUserId) {
2083
+ if (hubClient.userId && this.ctx?.config?.sharing?.role === "hub") {
2085
2084
  const now = Date.now();
2086
- const existing = this.store.getHubMemoryBySource(hubUserId, chunk.id);
2085
+ const existing = this.store.getHubMemoryBySource(hubClient.userId, chunk.id);
2087
2086
  this.store.upsertHubMemory({
2088
2087
  id: (response as any)?.memoryId ?? existing?.id ?? crypto.randomUUID(),
2089
2088
  sourceChunkId: chunk.id,
2090
- sourceUserId: hubUserId,
2089
+ sourceUserId: hubClient.userId,
2091
2090
  role: chunk.role,
2092
2091
  content: chunk.content,
2093
2092
  summary: chunk.summary ?? "",
@@ -2441,6 +2440,7 @@ export class ViewerServer {
2441
2440
  res.write("data: {\"type\":\"connected\"}\n\n");
2442
2441
  this.notifSSEClients.push(res);
2443
2442
  if (!this.notifPollTimer) this.startNotifPoll();
2443
+ else this.notifPollImmediate();
2444
2444
  req.on("close", () => {
2445
2445
  this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
2446
2446
  if (this.notifSSEClients.length === 0) this.stopNotifPoll();
@@ -2476,6 +2476,20 @@ export class ViewerServer {
2476
2476
  if (this.notifPollTimer) { clearInterval(this.notifPollTimer); this.notifPollTimer = undefined; }
2477
2477
  }
2478
2478
 
2479
+ private notifPollImmediate(): void {
2480
+ const hub = this.resolveHubConnection();
2481
+ if (!hub) return;
2482
+ hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")
2483
+ .then((data: any) => {
2484
+ const count = data?.unreadCount ?? 0;
2485
+ if (count !== this.lastKnownNotifCount) {
2486
+ this.lastKnownNotifCount = count;
2487
+ this.broadcastNotifSSE({ type: "update", unreadCount: count });
2488
+ }
2489
+ })
2490
+ .catch(() => {});
2491
+ }
2492
+
2479
2493
  private startHubHeartbeat(): void {
2480
2494
  this.stopHubHeartbeat();
2481
2495
  const sendHeartbeat = async () => {
@@ -2661,11 +2675,16 @@ export class ViewerServer {
2661
2675
  const finalSharing = config.sharing as Record<string, unknown> | undefined;
2662
2676
  const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
2663
2677
  const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
2678
+ let joinStatus: string | undefined;
2664
2679
  if (nowClient && !previouslyClient) {
2665
- this.autoJoinOnSave(finalSharing).catch(e => this.log.warn(`Auto-join on save failed: ${e}`));
2680
+ try {
2681
+ joinStatus = await this.autoJoinOnSave(finalSharing);
2682
+ } catch (e) {
2683
+ this.log.warn(`Auto-join on save failed: ${e}`);
2684
+ }
2666
2685
  }
2667
2686
 
2668
- this.jsonResponse(res, { ok: true });
2687
+ this.jsonResponse(res, { ok: true, joinStatus });
2669
2688
  } catch (e) {
2670
2689
  this.log.warn(`handleSaveConfig error: ${e}`);
2671
2690
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -2674,11 +2693,11 @@ export class ViewerServer {
2674
2693
  });
2675
2694
  }
2676
2695
 
2677
- private async autoJoinOnSave(sharing: Record<string, unknown>): Promise<void> {
2696
+ private async autoJoinOnSave(sharing: Record<string, unknown>): Promise<string | undefined> {
2678
2697
  const clientCfg = sharing.client as Record<string, unknown> | undefined;
2679
2698
  const hubAddress = String(clientCfg?.hubAddress || "");
2680
2699
  const teamToken = String(clientCfg?.teamToken || "");
2681
- if (!hubAddress || !teamToken) return;
2700
+ if (!hubAddress || !teamToken) return undefined;
2682
2701
  const hubUrl = normalizeHubUrl(hubAddress);
2683
2702
  const os = await import("os");
2684
2703
  const nickname = String(clientCfg?.nickname || "");
@@ -2705,6 +2724,7 @@ export class ViewerServer {
2705
2724
  if (result.userToken) {
2706
2725
  this.startHubHeartbeat();
2707
2726
  }
2727
+ return result.status;
2708
2728
  }
2709
2729
 
2710
2730
  private async notifyHubLeave(): Promise<void> {