total-recall-brain 3.24.0 → 3.24.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.
Binary file
Binary file
@@ -1,6 +1,3 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
2
- <rect width="64" height="64" rx="14" fill="#0b1220"/>
3
- <path d="M18 42V28a14 14 0 0 1 28 0v14" stroke="#60a5fa" stroke-width="5" stroke-linecap="round"/>
4
- <path d="M24 42V28a8 8 0 0 1 16 0v14" stroke="#1e3a8a" stroke-width="4" stroke-linecap="round"/>
5
- <rect x="20" y="40" width="24" height="6" rx="2" fill="#93c5fd"/>
6
- </svg>
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <image href="data:image/png;base64,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" width="64" height="64" />
3
+ </svg>
Binary file
@@ -17,7 +17,7 @@
17
17
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
18
18
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
19
19
  <title>Total Recall — Portable Memory</title>
20
- <script type="module" crossorigin src="/assets/index-CtCDaZYZ.js"></script>
20
+ <script type="module" crossorigin src="/assets/index-BQL-opnK.js"></script>
21
21
  <link rel="stylesheet" crossorigin href="/assets/index-C7_Xpk27.css">
22
22
  </head>
23
23
  <body>
@@ -99,6 +99,7 @@ import {
99
99
  triggerRecompile,
100
100
  triggerDream,
101
101
  runAgentDiagnostics,
102
+ restartDaemon,
102
103
  fetchBrains,
103
104
  listTasks,
104
105
  createTask,
@@ -189,7 +190,7 @@ describe('API barrel exports — all functions exist', () => {
189
190
  // research
190
191
  listResearch, createResearch, patchResearch, deleteResearch,
191
192
  // system
192
- fetchHealth, fetchUsageStats, fetchLogs, triggerRecompile, triggerDream, runAgentDiagnostics,
193
+ fetchHealth, fetchUsageStats, fetchLogs, triggerRecompile, triggerDream, runAgentDiagnostics, restartDaemon,
193
194
  fetchBrains, listTasks, createTask, listFiles, listScripts, readScript, saveScript, runScript,
194
195
  shareToApi, runOkfLint, triggerOkfExport, postDecision,
195
196
  // update
@@ -71,6 +71,12 @@ export async function runAgentDiagnostics(): Promise<{ success: boolean; output:
71
71
  return res.json()
72
72
  }
73
73
 
74
+ export async function restartDaemon(): Promise<{ success: boolean; message: string; pid?: number }> {
75
+ const res = await apiFetch(`${API_BASE}/api/daemon/restart`, { method: 'POST' })
76
+ if (!res.ok) throw new Error(`Restart daemon API error: ${res.status}`)
77
+ return res.json()
78
+ }
79
+
74
80
  export async function fetchBrains(): Promise<Record<string, unknown>[]> {
75
81
  const res = await apiFetch(`${API_BASE}/api/brains`)
76
82
  const data = await res.json()
@@ -15,13 +15,10 @@ export async function checkUpdate(): Promise<UpdateCheckResult> {
15
15
  // Normalize snake_case API fields for the Settings UI
16
16
  const currentVersion = data.currentVersion ?? data.current ?? ''
17
17
  const latestVersion = data.latestVersion ?? data.latest ?? ''
18
- // Prefer host package comparison; fall back to API flag (includes consumers behind).
19
- const hostBehind =
20
- Boolean(latestVersion && currentVersion && latestVersion !== currentVersion)
21
18
  return {
22
19
  currentVersion,
23
20
  latestVersion,
24
- updateAvailable: hostBehind || Boolean(data.updateAvailable ?? data.update_available),
21
+ updateAvailable: Boolean(data.updateAvailable ?? data.update_available),
25
22
  }
26
23
  }
27
24
 
@@ -202,43 +202,50 @@ export function MobilePairing() {
202
202
  </div>
203
203
 
204
204
  {selected && (
205
- <p style={{ fontSize: 12, color: 'var(--text-secondary)', margin: '0 0 8px' }}>
206
- {selected.reachable_hint}
207
- </p>
205
+ <div style={{ fontSize: 12, color: 'var(--text-secondary)', margin: '0 0 10px', lineHeight: 1.5, background: 'rgba(255,255,255,0.03)', padding: '8px 12px', borderRadius: 6 }}>
206
+ <strong>Connection Route:</strong> {selected.reachable_hint}
207
+ </div>
208
208
  )}
209
209
 
210
210
  {isLoopback && (
211
- <p
211
+ <div
212
212
  data-testid="pairing-loopback-warning"
213
- style={{ fontSize: 12, color: '#f59e0b', margin: 0 }}
213
+ style={{ fontSize: 12, color: '#f59e0b', margin: '0 0 8px', background: 'rgba(245, 158, 11, 0.1)', padding: '8px 12px', borderRadius: 6, border: '1px solid rgba(245, 158, 11, 0.2)' }}
214
214
  >
215
- This address only works on this computer. Choose LAN or Tailscale for your phone.
216
- </p>
215
+ ⚠️ <strong>Loopback Only:</strong> <code>127.0.0.1</code> only works on this computer. To open on your phone, select your Mesh IP (<code>100.64.x.x</code>) or enable <strong>Allow Public Bind</strong> below to use local Wi-Fi.
216
+ </div>
217
217
  )}
218
218
  </div>
219
219
  </div>
220
220
  )}
221
221
 
222
222
  {info?.warnings && info.warnings.length > 0 && (
223
- <ul
223
+ <div
224
224
  data-testid="pairing-warnings"
225
225
  style={{
226
226
  margin: '16px 0 0',
227
- paddingLeft: 18,
227
+ padding: '10px 14px',
228
228
  fontSize: 12,
229
- color: '#f59e0b',
229
+ color: '#fbbf24',
230
+ background: 'rgba(251, 191, 36, 0.08)',
231
+ borderRadius: 8,
232
+ border: '1px solid rgba(251, 191, 36, 0.2)',
230
233
  }}
231
234
  >
232
- {info.warnings.map((w) => (
233
- <li key={w}>{w}</li>
234
- ))}
235
- </ul>
235
+ <div style={{ fontWeight: 600, marginBottom: 4 }}>Network Notice:</div>
236
+ <ul style={{ margin: 0, paddingLeft: 18 }}>
237
+ {info.warnings.map((w) => (
238
+ <li key={w}>{w}</li>
239
+ ))}
240
+ </ul>
241
+ </div>
236
242
  )}
237
243
 
238
- <p style={{ marginTop: 16, fontSize: 11, color: 'var(--text-tertiary)' }}>
239
- After scanning, log in on the phone (same password/PAT as this dashboard). Listening:{' '}
240
- {(info?.listen_hosts || []).join(', ') || 'unknown'}
241
- </p>
244
+ <div style={{ marginTop: 16, padding: '12px 14px', background: 'rgba(59, 130, 246, 0.06)', borderRadius: 8, border: '1px solid rgba(59, 130, 246, 0.15)', fontSize: 12, color: 'var(--text-secondary)', lineHeight: 1.5 }}>
245
+ <div style={{ color: 'var(--text-primary)', fontWeight: 600, marginBottom: 4 }}>📱 How to connect your phone:</div>
246
+ <div><strong>Option A (WireGuard / Mesh — Recommended):</strong> Install the standard <em>Tailscale</em> app on your phone (iOS App Store / Google Play). Log into your tailnet/headscale server. Scan the <code>100.64.x.x</code> QR code above.</div>
247
+ <div style={{ marginTop: 4 }}><strong>Option B (Local Home Wi-Fi):</strong> Check <strong>Allow Public Bind</strong> in <em>Network & Binding</em> below, restart the server, and scan your local Wi-Fi LAN QR code (e.g. <code>192.168.x.x</code>).</div>
248
+ </div>
242
249
  </div>
243
250
  );
244
251
  }
@@ -46,6 +46,9 @@ export default function ChatPage({ activeBrainId, onBrainChange }: { activeBrain
46
46
  const [extensionBannerDismissed, setExtensionBannerDismissed] = useState(
47
47
  () => localStorage.getItem('tr-ext-banner-dismissed') === 'true'
48
48
  )
49
+ const [dismissedUpdateVersion, setDismissedUpdateVersion] = useState<string | null>(
50
+ () => localStorage.getItem('tr-update-banner-dismissed-version')
51
+ )
49
52
 
50
53
  // Threads listing & session control state
51
54
  const [threads, setThreads] = useState<ChatThread[]>([])
@@ -671,7 +674,7 @@ export default function ChatPage({ activeBrainId, onBrainChange }: { activeBrain
671
674
  </header>
672
675
 
673
676
  {/* Update Available Banner */}
674
- {updateInfo?.updateAvailable && (
677
+ {updateInfo?.updateAvailable && dismissedUpdateVersion !== updateInfo.latestVersion && (
675
678
  <div className="animate-fade-in" style={{
676
679
  margin: '0 16px 12px',
677
680
  padding: '12px 16px',
@@ -710,6 +713,27 @@ export default function ChatPage({ activeBrainId, onBrainChange }: { activeBrain
710
713
  >
711
714
  Go to Update
712
715
  </button>
716
+ <button
717
+ onClick={() => {
718
+ if (updateInfo?.latestVersion) {
719
+ localStorage.setItem('tr-update-banner-dismissed-version', updateInfo.latestVersion)
720
+ setDismissedUpdateVersion(updateInfo.latestVersion)
721
+ }
722
+ }}
723
+ title="Dismiss banner"
724
+ aria-label="Dismiss update banner"
725
+ style={{
726
+ background: 'transparent',
727
+ border: 'none',
728
+ color: 'var(--text-muted)',
729
+ fontSize: 16,
730
+ cursor: 'pointer',
731
+ padding: '4px 6px',
732
+ lineHeight: 1,
733
+ }}
734
+ >
735
+ ✕
736
+ </button>
713
737
  </div>
714
738
  )}
715
739
 
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect } from 'react'
2
- import { fetchHealth, checkUpdate, runUpdate } from '../api'
2
+ import { fetchHealth, checkUpdate, runUpdate, restartDaemon } from '../api'
3
3
  import type { HealthData, UpdateCheckResult } from '../types'
4
4
 
5
5
  function formatUptime(seconds: number): string {
@@ -23,6 +23,24 @@ export default function HealthPage() {
23
23
  const [updateError, setUpdateError] = useState('')
24
24
  const [isCheckingUpdate, setIsCheckingUpdate] = useState(false)
25
25
  const [checkMessage, setCheckMessage] = useState('')
26
+ const [isRestartingDaemon, setIsRestartingDaemon] = useState(false)
27
+ const [daemonMessage, setDaemonMessage] = useState('')
28
+
29
+ const handleRestartDaemon = async () => {
30
+ setIsRestartingDaemon(true)
31
+ setDaemonMessage('')
32
+ try {
33
+ const res = await restartDaemon()
34
+ setDaemonMessage(res.message || 'Daemon restarted successfully')
35
+ setTimeout(() => setDaemonMessage(''), 4000)
36
+ const data = await fetchHealth()
37
+ setHealth(data)
38
+ } catch (e) {
39
+ setDaemonMessage(`Failed to restart daemon: ${(e as Error).message}`)
40
+ } finally {
41
+ setIsRestartingDaemon(false)
42
+ }
43
+ }
26
44
 
27
45
  const handleManualCheckUpdate = async () => {
28
46
  setIsCheckingUpdate(true)
@@ -96,8 +114,14 @@ export default function HealthPage() {
96
114
  setIsUpdating(false)
97
115
  setCheckMessage(
98
116
  result.message ||
99
- 'Registered projects updated. Restart local daemon/server if you need new code in this process.',
117
+ 'Registered projects updated. Restart daemon if you need new code running.',
100
118
  )
119
+
120
+ // Refresh version check immediately so banner / badge clears
121
+ const refreshed = await checkUpdate().catch(() => null)
122
+ if (refreshed) {
123
+ setUpdateInfo(refreshed)
124
+ }
101
125
  } catch (e) {
102
126
  setUpdateError((e as Error).message)
103
127
  setIsUpdating(false)
@@ -278,7 +302,32 @@ export default function HealthPage() {
278
302
  <tr><th>Version</th><td>{health.version}</td></tr>
279
303
  <tr><th>Uptime</th><td>{formatUptime(health.uptime_seconds)}</td></tr>
280
304
  <tr><th>Timestamp</th><td>{health.timestamp}</td></tr>
281
- <tr><th>Daemon Status</th><td>{health.daemon || 'unknown'}</td></tr>
305
+ <tr>
306
+ <th>Daemon Status</th>
307
+ <td>
308
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
309
+ <span style={{ fontWeight: 600 }}>{health.daemon || 'unknown'}</span>
310
+ <button
311
+ className="btn btn-secondary"
312
+ onClick={handleRestartDaemon}
313
+ disabled={isRestartingDaemon}
314
+ style={{
315
+ fontSize: 12,
316
+ padding: '4px 12px',
317
+ borderRadius: 6,
318
+ cursor: 'pointer'
319
+ }}
320
+ >
321
+ {isRestartingDaemon ? 'Restarting…' : 'Restart Daemon'}
322
+ </button>
323
+ </div>
324
+ {daemonMessage && (
325
+ <div style={{ fontSize: 12, color: daemonMessage.includes('Failed') ? '#ef4444' : '#3fb950', marginTop: 4 }}>
326
+ {daemonMessage}
327
+ </div>
328
+ )}
329
+ </td>
330
+ </tr>
282
331
  {health.cli_agents && (
283
332
  <tr><th>CLI Agents</th><td>{health.cli_agents.join(', ')}</td></tr>
284
333
  )}
@@ -6,6 +6,7 @@ import {
6
6
  runAgentDiagnostics,
7
7
  checkUpdate,
8
8
  runUpdate,
9
+ restartDaemon,
9
10
  triggerRecompile,
10
11
  apiFetch,
11
12
  getApiBase,
@@ -50,6 +51,22 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
50
51
  } | null>(null);
51
52
  const [updating, setUpdating] = useState(false);
52
53
  const [updateMessage, setUpdateMessage] = useState<string | null>(null);
54
+ const [restartingDaemon, setRestartingDaemon] = useState(false);
55
+
56
+ const handleRestartDaemon = async () => {
57
+ setRestartingDaemon(true);
58
+ setUpdateMessage(null);
59
+ try {
60
+ const res = await restartDaemon();
61
+ setUpdateMessage(res.message || 'Daemon restarted successfully.');
62
+ const h = await fetchHealth().catch(() => null);
63
+ if (h) setHealth(h);
64
+ } catch (err: unknown) {
65
+ setUpdateMessage('Restart error: ' + (err as Error).message);
66
+ } finally {
67
+ setRestartingDaemon(false);
68
+ }
69
+ };
53
70
  const [rebuilding, setRebuilding] = useState(false);
54
71
 
55
72
  const loadConfig = async () => {
@@ -984,14 +1001,24 @@ export default function SettingsPage({ activeBrainId }: { activeBrainId?: string
984
1001
  </div>
985
1002
  )}
986
1003
  </div>
987
- <button
988
- className="btn-primary"
989
- onClick={handleRunUpdate}
990
- disabled={updating}
991
- style={{ background: updateInfo?.updateAvailable ? '#3fb950' : undefined, border: 'none' }}
992
- >
993
- {updating ? 'Updating…' : updateInfo?.updateAvailable ? 'Apply Updates' : 'Check & Sync Projects'}
994
- </button>
1004
+ <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
1005
+ <button
1006
+ className="btn-secondary"
1007
+ onClick={handleRestartDaemon}
1008
+ disabled={restartingDaemon}
1009
+ style={{ fontSize: 13, padding: '8px 14px', borderRadius: 8, cursor: 'pointer' }}
1010
+ >
1011
+ {restartingDaemon ? 'Restarting…' : 'Restart Daemon'}
1012
+ </button>
1013
+ <button
1014
+ className="btn-primary"
1015
+ onClick={handleRunUpdate}
1016
+ disabled={updating}
1017
+ style={{ background: updateInfo?.updateAvailable ? '#3fb950' : undefined, border: 'none' }}
1018
+ >
1019
+ {updating ? 'Updating…' : updateInfo?.updateAvailable ? 'Apply Updates' : 'Check & Sync Projects'}
1020
+ </button>
1021
+ </div>
995
1022
  </div>
996
1023
  </div>
997
1024
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "total-recall-brain",
3
- "version": "3.24.0",
3
+ "version": "3.24.1",
4
4
  "description": "Personal AI system — database-free, SSSS-powered intelligence engine deployable to any POSIX host.",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -169,20 +169,24 @@ export function inspectProjectPackage(projectRoot) {
169
169
  } catch {
170
170
  return { declared: null, installed: null, isSourceTree: false, packageName: null };
171
171
  }
172
+ const isSourceTree =
173
+ pkg.name === PACKAGE_NAME &&
174
+ fs.existsSync(path.join(projectRoot, 'src', 'server', 'index.mjs'));
172
175
  const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
173
176
  const declared = deps[PACKAGE_NAME] || null;
174
177
  let installed = null;
175
- const nm = path.join(projectRoot, 'node_modules', PACKAGE_NAME, 'package.json');
176
- if (fs.existsSync(nm)) {
177
- try {
178
- installed = JSON.parse(fs.readFileSync(nm, 'utf8')).version || null;
179
- } catch {
180
- installed = null;
178
+ if (isSourceTree) {
179
+ installed = pkg.version || null;
180
+ } else {
181
+ const nm = path.join(projectRoot, 'node_modules', PACKAGE_NAME, 'package.json');
182
+ if (fs.existsSync(nm)) {
183
+ try {
184
+ installed = JSON.parse(fs.readFileSync(nm, 'utf8')).version || null;
185
+ } catch {
186
+ installed = null;
187
+ }
181
188
  }
182
189
  }
183
- const isSourceTree =
184
- pkg.name === PACKAGE_NAME &&
185
- fs.existsSync(path.join(projectRoot, 'src', 'server', 'index.mjs'));
186
190
  return { declared, installed, isSourceTree, packageName: pkg.name || null };
187
191
  }
188
192
 
@@ -321,38 +321,67 @@ export async function buildRulesBlock(skillsDir, nodes = [], { consumer = 'ide',
321
321
  // Note: Only invariants, preferences, and anti-patterns are included in instructions,
322
322
  // enforcing Category Partitioning (concepts, decisions, facts are search-only).
323
323
  const isImportant = (n) => n.importance === undefined || n.importance >= 3;
324
- const invariants = nodes.filter(n => n.category === 'invariants' && n.status === 'active' && !isExpired(n) && isImportant(n));
325
- const preferences = nodes.filter(n => n.category === 'preferences' && n.status === 'active' && !isExpired(n) && isImportant(n));
326
- const corrections = nodes.filter(n => n.category === 'anti-patterns' && n.status === 'active' && !isExpired(n) && isImportant(n));
324
+
325
+ // Determine current project/repo context to prevent cross-repo pollution
326
+ const currentRepoName = path.basename(process.cwd()).toLowerCase();
327
+
328
+ const isRelevantToRepo = (n) => {
329
+ // If explicitly marked global or no repo restriction, it applies everywhere
330
+ if (n.scope === 'global' || n._layer === 'global') {
331
+ // If the node text explicitly binds to specific other repos, skip it
332
+ if (Array.isArray(n.repos) && n.repos.length > 0) {
333
+ return n.repos.some(r => r.toLowerCase() === currentRepoName);
334
+ }
335
+ return true;
336
+ }
337
+ // If tagged with specific repos or project name
338
+ if (Array.isArray(n.repos) && n.repos.length > 0) {
339
+ return n.repos.some(r => r.toLowerCase() === currentRepoName);
340
+ }
341
+ if (n.project && typeof n.project === 'string') {
342
+ return n.project.toLowerCase() === currentRepoName;
343
+ }
344
+ return true;
345
+ };
327
346
 
328
- // Deduplicate nodes by compacted content hash (OKF non-destructive merge pattern).
329
- // When two nodes produce identical compacted text, keep the one with higher importance.
330
- const deduplicateNodes = (list) => {
347
+ const rawInvariants = nodes.filter(n => n.category === 'invariants' && n.status === 'active' && !isExpired(n) && isImportant(n) && isRelevantToRepo(n));
348
+ const rawPreferences = nodes.filter(n => n.category === 'preferences' && n.status === 'active' && !isExpired(n) && isImportant(n) && isRelevantToRepo(n));
349
+ const rawCorrections = nodes.filter(n => n.category === 'anti-patterns' && n.status === 'active' && !isExpired(n) && isImportant(n) && isRelevantToRepo(n));
350
+
351
+ // Deduplicate nodes by normalized content key and rank by priority/importance
352
+ const deduplicateAndRankNodes = (list, maxCount = 15) => {
331
353
  const seen = new Map();
332
354
  const deduped = [];
333
- for (const n of list) {
334
- const key = (n.body || n.content || '').trim().toLowerCase().substring(0, 200);
335
- if (seen.has(key)) {
336
- const existing = seen.get(key);
337
- if ((n.importance || 3) > (existing.importance || 3)) {
338
- // Replace with higher-importance node
339
- const idx = deduped.indexOf(existing);
340
- if (idx !== -1) deduped[idx] = n;
341
- seen.set(key, n);
342
- }
343
- // else skip the lower-importance duplicate
344
- } else {
345
- seen.set(key, n);
355
+
356
+ // Sort by importance (descending) and priority (absolute first)
357
+ const sorted = [...list].sort((a, b) => {
358
+ const aAbs = a.priority === 'absolute' ? 10 : 0;
359
+ const bAbs = b.priority === 'absolute' ? 10 : 0;
360
+ const aScore = (a.importance || 3) + aAbs;
361
+ const bScore = (b.importance || 3) + bAbs;
362
+ return bScore - aScore;
363
+ });
364
+
365
+ for (const n of sorted) {
366
+ // Normalize content to catch paraphrased/duplicate directives
367
+ const rawText = (n.body || n.content || n.title || '').trim().toLowerCase();
368
+ const normKey = rawText.replace(/[^a-z0-9]/g, '').substring(0, 120);
369
+
370
+ if (!seen.has(normKey)) {
371
+ seen.set(normKey, n);
346
372
  deduped.push(n);
347
373
  }
348
374
  }
349
- return deduped;
375
+ return deduped.slice(0, maxCount);
350
376
  };
351
377
 
378
+ const invariants = deduplicateAndRankNodes(rawInvariants, 15);
379
+ const preferences = deduplicateAndRankNodes(rawPreferences, 10);
380
+ const corrections = deduplicateAndRankNodes(rawCorrections, 15);
381
+
352
382
  const formatNodes = async (list) => {
353
- const unique = deduplicateNodes(list);
354
383
  const formatted = [];
355
- for (const n of unique) {
384
+ for (const n of list) {
356
385
  const snippet = await compactNode(n, derivedDir, force);
357
386
  formatted.push(snippet.startsWith('-') ? snippet : `- ${snippet}`);
358
387
  }
@@ -202,5 +202,20 @@ router.get('/api/usage/providers', requireAuth, async (req, res) => {
202
202
  } catch (err) { serverError(res, err); }
203
203
  });
204
204
 
205
+ /**
206
+ * POST /api/daemon/restart
207
+ * Restarts the Active Intelligence background daemon process.
208
+ */
209
+ router.post('/api/daemon/restart', requireAuth, requireScope('config:write'), async (_req, res) => {
210
+ try {
211
+ const { stopDaemon, startDaemon } = await import('../../core/daemon-control.mjs');
212
+ stopDaemon();
213
+ const pid = startDaemon();
214
+ res.json({ success: true, message: `Daemon restarted successfully (PID ${pid})`, pid });
215
+ } catch (err) {
216
+ serverError(res, err);
217
+ }
218
+ });
219
+
205
220
  export default router;
206
221
  export { router as systemRouter };
@@ -45,7 +45,7 @@ router.get('/api/update/check', requireAuth, async (_req, res) => {
45
45
 
46
46
  const consumersBehind = projects.filter((p) => p.update_available).length;
47
47
  const updateAvailable =
48
- Boolean(latest && current && current !== latest) || consumersBehind > 0;
48
+ Boolean(latest && current && needsUpdate(current, latest)) || consumersBehind > 0;
49
49
 
50
50
  res.json({
51
51
  package: PACKAGE_NAME,