aimeat 1.16.2 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/dist/.env.example +6 -1
  2. package/dist/locales/en.json +166 -4
  3. package/dist/locales/fi.json +166 -4
  4. package/dist/public/components/Card.js +17 -3
  5. package/dist/public/components/Collapsible.js +39 -0
  6. package/dist/public/components/CopyButton.js +55 -24
  7. package/dist/public/components/DataTable.js +57 -0
  8. package/dist/public/components/EmptyState.js +28 -0
  9. package/dist/public/components/KeyValueRow.js +37 -0
  10. package/dist/public/components/Modal.js +22 -5
  11. package/dist/public/components/Pagination.js +70 -0
  12. package/dist/public/components/SearchBar.js +44 -0
  13. package/dist/public/components/StatusDot.js +29 -0
  14. package/dist/public/components/TagList.js +53 -0
  15. package/dist/public/components/ToggleSwitch.js +28 -0
  16. package/dist/public/components/index.js +41 -1
  17. package/dist/public/css/components/tags.css +24 -0
  18. package/dist/public/css/style-testbed.css +135 -0
  19. package/dist/public/css/theme.css +556 -34
  20. package/dist/public/css/views/admin-agent-integration.css +2 -33
  21. package/dist/public/css/views/admin.css +730 -686
  22. package/dist/public/css/views/agents-detail.css +402 -14
  23. package/dist/public/css/views/help.css +2 -1
  24. package/dist/public/css/views/hobbies.css +53 -33
  25. package/dist/public/css/views/marketplace.css +73 -60
  26. package/dist/public/css/views/openclaw.css +12 -6
  27. package/dist/public/css/views/portal-classic.css +34 -26
  28. package/dist/public/css/views/portal-dev.css +41 -32
  29. package/dist/public/css/views/portal.css +14 -18
  30. package/dist/public/css/views/profile.css +252 -164
  31. package/dist/public/css/views/public-knowledge-viewer.css +18 -6
  32. package/dist/public/css/views/scheduler.css +100 -0
  33. package/dist/public/js/format.js +41 -0
  34. package/dist/public/js/services/agent-tasks.js +14 -1
  35. package/dist/public/js/services/generator-spec-validate.js +5 -1
  36. package/dist/public/js/services/schedules.js +53 -0
  37. package/dist/public/js/style-testbed.js +170 -0
  38. package/dist/public/js/theme.js +114 -0
  39. package/dist/public/spa.html +74 -16
  40. package/dist/public/style-testbed.html +420 -0
  41. package/dist/public/views/admin/agent-integration-tab.js +23 -38
  42. package/dist/public/views/admin/agent-tasks-tab.js +14 -21
  43. package/dist/public/views/admin/chat-instances-tab.js +1 -1
  44. package/dist/public/views/admin/config-tab.js +7 -2
  45. package/dist/public/views/admin/cortex-tab.js +20 -14
  46. package/dist/public/views/admin/csm-tab.js +22 -18
  47. package/dist/public/views/admin/directory-tab.js +3 -3
  48. package/dist/public/views/admin/email-tab.js +23 -28
  49. package/dist/public/views/admin/federation-tab.js +15 -5
  50. package/dist/public/views/admin/generator-debug-tab.js +10 -8
  51. package/dist/public/views/admin/genesis-tab.js +4 -4
  52. package/dist/public/views/admin/ghii-tab.js +3 -3
  53. package/dist/public/views/admin/knowledge-tab.js +15 -5
  54. package/dist/public/views/admin/matching-tab.js +3 -3
  55. package/dist/public/views/admin/memory-tab.js +16 -20
  56. package/dist/public/views/admin/msm-tab.js +30 -17
  57. package/dist/public/views/admin/overview-tab.js +7 -7
  58. package/dist/public/views/admin/packages-tab.js +10 -9
  59. package/dist/public/views/admin/portal-tab.js +14 -4
  60. package/dist/public/views/admin/prompts-tab.js +5 -5
  61. package/dist/public/views/admin/push-tab.js +15 -4
  62. package/dist/public/views/admin/realtime-tab.js +3 -3
  63. package/dist/public/views/admin/services-tab.js +18 -27
  64. package/dist/public/views/admin/shared.js +48 -58
  65. package/dist/public/views/admin/sharing-groups-tab.js +5 -10
  66. package/dist/public/views/admin/stats-tab.js +3 -3
  67. package/dist/public/views/guides.js +9 -13
  68. package/dist/public/views/help.js +3 -24
  69. package/dist/public/views/hobbies.js +47 -41
  70. package/dist/public/views/marketplace.js +42 -34
  71. package/dist/public/views/openclaw.js +0 -1
  72. package/dist/public/views/portal-classic.js +4 -15
  73. package/dist/public/views/portal-dev.js +28 -30
  74. package/dist/public/views/portal.js +16 -21
  75. package/dist/public/views/portfolio.js +11 -12
  76. package/dist/public/views/profile/access-tab.js +250 -1
  77. package/dist/public/views/profile/agents/agent-card.js +75 -4
  78. package/dist/public/views/profile/agents/tab-activity.js +15 -13
  79. package/dist/public/views/profile/agents/tab-agent-config.js +60 -6
  80. package/dist/public/views/profile/agents/tab-integration.js +37 -60
  81. package/dist/public/views/profile/agents/tab-messages.js +10 -1
  82. package/dist/public/views/profile/agents/tab-quality.js +25 -19
  83. package/dist/public/views/profile/agents/tab-schedules.js +110 -0
  84. package/dist/public/views/profile/agents-activity-subtab.js +4 -4
  85. package/dist/public/views/profile/agents-services-subtab.js +5 -4
  86. package/dist/public/views/profile/agents-tab.js +141 -37
  87. package/dist/public/views/profile/agents-tasks-subtab.js +221 -26
  88. package/dist/public/views/profile/calibrator-batch.js +5 -11
  89. package/dist/public/views/profile/calibrator-tab.js +4 -9
  90. package/dist/public/views/profile/chat-sessions-tab.js +9 -5
  91. package/dist/public/views/profile/email-tab.js +2 -2
  92. package/dist/public/views/profile/extensions-tab.js +27 -22
  93. package/dist/public/views/profile/federation-tab.js +4 -1
  94. package/dist/public/views/profile/foundry-detail.js +22 -23
  95. package/dist/public/views/profile/foundry-tab.js +12 -13
  96. package/dist/public/views/profile/generator-dashboard/DebugPanel.js +4 -7
  97. package/dist/public/views/profile/generator-dashboard/use-dashboard-core.js +4 -1
  98. package/dist/public/views/profile/generator-dashboard/use-edit-mode.js +3 -2
  99. package/dist/public/views/profile/generator-detail.js +26 -30
  100. package/dist/public/views/profile/generator-tab.js +5 -5
  101. package/dist/public/views/profile/knowledge-tab.js +12 -12
  102. package/dist/public/views/profile/landing-page.js +100 -149
  103. package/dist/public/views/profile/memory-tab.js +15 -11
  104. package/dist/public/views/profile/node-stats-tab.js +3 -3
  105. package/dist/public/views/profile/nodes-tab.js +10 -3
  106. package/dist/public/views/profile/notifications-tab.js +3 -3
  107. package/dist/public/views/profile/packages-tab.js +14 -14
  108. package/dist/public/views/profile/scheduler-tab.js +340 -0
  109. package/dist/public/views/profile/services-tab.js +1 -1
  110. package/dist/public/views/profile/shared.js +16 -12
  111. package/dist/public/views/profile/wallet-tab.js +27 -50
  112. package/dist/public/views/profile/work-tab.js +29 -31
  113. package/dist/public/views/profile.js +24 -0
  114. package/dist/public/views/public-knowledge-viewer.js +9 -18
  115. package/dist/src/auth/middleware.d.ts +3 -2
  116. package/dist/src/auth/middleware.d.ts.map +1 -1
  117. package/dist/src/auth/middleware.js +78 -3
  118. package/dist/src/auth/middleware.js.map +1 -1
  119. package/dist/src/cli/init-wizard.d.ts.map +1 -1
  120. package/dist/src/cli/init-wizard.js +13 -1
  121. package/dist/src/cli/init-wizard.js.map +1 -1
  122. package/dist/src/config.d.ts +6 -0
  123. package/dist/src/config.d.ts.map +1 -1
  124. package/dist/src/config.js +12 -1
  125. package/dist/src/config.js.map +1 -1
  126. package/dist/src/generated/api-types.d.ts +708 -8
  127. package/dist/src/generated/api-types.d.ts.map +1 -1
  128. package/dist/src/mcp/agent-schedules.d.ts +23 -0
  129. package/dist/src/mcp/agent-schedules.d.ts.map +1 -0
  130. package/dist/src/mcp/agent-schedules.js +175 -0
  131. package/dist/src/mcp/agent-schedules.js.map +1 -0
  132. package/dist/src/mcp/annotations.d.ts.map +1 -1
  133. package/dist/src/mcp/annotations.js +6 -0
  134. package/dist/src/mcp/annotations.js.map +1 -1
  135. package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
  136. package/dist/src/mcp/catalog/definitions.js +55 -0
  137. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  138. package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
  139. package/dist/src/mcp/catalog/scopes.js +12 -0
  140. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  141. package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
  142. package/dist/src/mcp/catalog/surfaces.js +2 -0
  143. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  144. package/dist/src/mcp/index.d.ts.map +1 -1
  145. package/dist/src/mcp/index.js +2 -0
  146. package/dist/src/mcp/index.js.map +1 -1
  147. package/dist/src/models/agent-task-schemas.d.ts +6 -0
  148. package/dist/src/models/agent-task-schemas.d.ts.map +1 -1
  149. package/dist/src/models/agent-task-schemas.js +6 -0
  150. package/dist/src/models/agent-task-schemas.js.map +1 -1
  151. package/dist/src/models/schemas.d.ts +1 -0
  152. package/dist/src/models/schemas.d.ts.map +1 -1
  153. package/dist/src/models/schemas.js +4 -0
  154. package/dist/src/models/schemas.js.map +1 -1
  155. package/dist/src/routes/access-tokens.d.ts +17 -0
  156. package/dist/src/routes/access-tokens.d.ts.map +1 -0
  157. package/dist/src/routes/access-tokens.js +164 -0
  158. package/dist/src/routes/access-tokens.js.map +1 -0
  159. package/dist/src/routes/agent-tasks.d.ts +2 -0
  160. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  161. package/dist/src/routes/agent-tasks.js +118 -23
  162. package/dist/src/routes/agent-tasks.js.map +1 -1
  163. package/dist/src/routes/agents.d.ts.map +1 -1
  164. package/dist/src/routes/agents.js +46 -0
  165. package/dist/src/routes/agents.js.map +1 -1
  166. package/dist/src/routes/ai.d.ts +6 -9
  167. package/dist/src/routes/ai.d.ts.map +1 -1
  168. package/dist/src/routes/ai.js +26 -175
  169. package/dist/src/routes/ai.js.map +1 -1
  170. package/dist/src/routes/apps.d.ts.map +1 -1
  171. package/dist/src/routes/apps.js +104 -34
  172. package/dist/src/routes/apps.js.map +1 -1
  173. package/dist/src/routes/auth.d.ts.map +1 -1
  174. package/dist/src/routes/auth.js +77 -4
  175. package/dist/src/routes/auth.js.map +1 -1
  176. package/dist/src/routes/cortex.d.ts +2 -1
  177. package/dist/src/routes/cortex.d.ts.map +1 -1
  178. package/dist/src/routes/cortex.js +13 -8
  179. package/dist/src/routes/cortex.js.map +1 -1
  180. package/dist/src/routes/extensions.d.ts.map +1 -1
  181. package/dist/src/routes/extensions.js +85 -51
  182. package/dist/src/routes/extensions.js.map +1 -1
  183. package/dist/src/routes/generator.d.ts.map +1 -1
  184. package/dist/src/routes/generator.js +109 -43
  185. package/dist/src/routes/generator.js.map +1 -1
  186. package/dist/src/routes/ghii.d.ts.map +1 -1
  187. package/dist/src/routes/ghii.js +34 -13
  188. package/dist/src/routes/ghii.js.map +1 -1
  189. package/dist/src/routes/libs.d.ts +15 -0
  190. package/dist/src/routes/libs.d.ts.map +1 -1
  191. package/dist/src/routes/libs.js +178 -55
  192. package/dist/src/routes/libs.js.map +1 -1
  193. package/dist/src/routes/portal.d.ts.map +1 -1
  194. package/dist/src/routes/portal.js +1 -0
  195. package/dist/src/routes/portal.js.map +1 -1
  196. package/dist/src/routes/schedules.d.ts +33 -0
  197. package/dist/src/routes/schedules.d.ts.map +1 -0
  198. package/dist/src/routes/schedules.js +380 -0
  199. package/dist/src/routes/schedules.js.map +1 -0
  200. package/dist/src/server-bootstrap/config-init.js +2 -2
  201. package/dist/src/server-bootstrap/config-init.js.map +1 -1
  202. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  203. package/dist/src/server-bootstrap/routes-loader.js +7 -0
  204. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  205. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  206. package/dist/src/server-bootstrap/service-init.js +2 -1
  207. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  208. package/dist/src/services/access-token.d.ts +28 -0
  209. package/dist/src/services/access-token.d.ts.map +1 -0
  210. package/dist/src/services/access-token.js +37 -0
  211. package/dist/src/services/access-token.js.map +1 -0
  212. package/dist/src/services/ai-completion.d.ts +83 -0
  213. package/dist/src/services/ai-completion.d.ts.map +1 -0
  214. package/dist/src/services/ai-completion.js +187 -0
  215. package/dist/src/services/ai-completion.js.map +1 -0
  216. package/dist/src/services/config-schema.d.ts.map +1 -1
  217. package/dist/src/services/config-schema.js +3 -0
  218. package/dist/src/services/config-schema.js.map +1 -1
  219. package/dist/src/services/generator-prompt-seeds.d.ts.map +1 -1
  220. package/dist/src/services/generator-prompt-seeds.js +6 -3
  221. package/dist/src/services/generator-prompt-seeds.js.map +1 -1
  222. package/dist/src/services/generator-prompts/spec-validate.d.ts.map +1 -1
  223. package/dist/src/services/generator-prompts/spec-validate.js +6 -2
  224. package/dist/src/services/generator-prompts/spec-validate.js.map +1 -1
  225. package/dist/src/services/generator-registration.d.ts +12 -4
  226. package/dist/src/services/generator-registration.d.ts.map +1 -1
  227. package/dist/src/services/generator-registration.js +71 -0
  228. package/dist/src/services/generator-registration.js.map +1 -1
  229. package/dist/src/services/generator-validate.d.ts +10 -0
  230. package/dist/src/services/generator-validate.d.ts.map +1 -1
  231. package/dist/src/services/generator-validate.js +180 -36
  232. package/dist/src/services/generator-validate.js.map +1 -1
  233. package/dist/src/services/owner-session.d.ts +43 -0
  234. package/dist/src/services/owner-session.d.ts.map +1 -0
  235. package/dist/src/services/owner-session.js +175 -0
  236. package/dist/src/services/owner-session.js.map +1 -0
  237. package/dist/src/services/schedule-constraints.d.ts +65 -0
  238. package/dist/src/services/schedule-constraints.d.ts.map +1 -0
  239. package/dist/src/services/schedule-constraints.js +106 -0
  240. package/dist/src/services/schedule-constraints.js.map +1 -0
  241. package/dist/src/services/scheduler.d.ts +34 -0
  242. package/dist/src/services/scheduler.d.ts.map +1 -1
  243. package/dist/src/services/scheduler.js +266 -22
  244. package/dist/src/services/scheduler.js.map +1 -1
  245. package/dist/src/services/skill-bundle/generator.js +19 -0
  246. package/dist/src/services/skill-bundle/generator.js.map +1 -1
  247. package/dist/src/storage/interface.d.ts +48 -4
  248. package/dist/src/storage/interface.d.ts.map +1 -1
  249. package/dist/src/storage/interface.js.map +1 -1
  250. package/dist/src/storage/providers/mongodb/index.d.ts +31 -0
  251. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  252. package/dist/src/storage/providers/mongodb/index.js +187 -9
  253. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  254. package/dist/src/storage/providers/sqlite/index.d.ts +31 -0
  255. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  256. package/dist/src/storage/providers/sqlite/index.js +131 -8
  257. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  258. package/dist/src/storage/providers/sqlite/repos/agent-task.d.ts.map +1 -1
  259. package/dist/src/storage/providers/sqlite/repos/agent-task.js +6 -4
  260. package/dist/src/storage/providers/sqlite/repos/agent-task.js.map +1 -1
  261. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  262. package/dist/src/storage/providers/sqlite/schema.js +79 -8
  263. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  264. package/dist/src/storage/repositories/pat.repository.d.ts +40 -0
  265. package/dist/src/storage/repositories/pat.repository.d.ts.map +1 -0
  266. package/dist/src/storage/repositories/pat.repository.js +14 -0
  267. package/dist/src/storage/repositories/pat.repository.js.map +1 -0
  268. package/dist/src/storage/repositories/scheduler.repository.d.ts +2 -0
  269. package/dist/src/storage/repositories/scheduler.repository.d.ts.map +1 -1
  270. package/dist/src/storage/repositories/session.repository.d.ts +51 -0
  271. package/dist/src/storage/repositories/session.repository.d.ts.map +1 -1
  272. package/dist/src/storage/repositories/session.repository.js +12 -0
  273. package/dist/src/storage/repositories/session.repository.js.map +1 -1
  274. package/dist/src/utils/env-config.d.ts.map +1 -1
  275. package/dist/src/utils/env-config.js +25 -1
  276. package/dist/src/utils/env-config.js.map +1 -1
  277. package/dist/src/utils/env-validator.d.ts.map +1 -1
  278. package/dist/src/utils/env-validator.js +4 -0
  279. package/dist/src/utils/env-validator.js.map +1 -1
  280. package/package.json +1 -1
  281. package/prisma/schema.prisma +48 -0
@@ -24,7 +24,7 @@ import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
24
24
  import htm from 'htm';
25
25
  const html = htm.bind(h);
26
26
  import { t } from '/js/i18n.js';
27
- import { escHtml } from '/js/utils.js';
27
+ import { escHtml, copyToClipboard } from '/js/utils.js';
28
28
  import { apiGet, apiPost, apiDelete } from '/js/api.js';
29
29
  import { listConsents, grantConsent, revokeConsent } from '/js/services/consent.js';
30
30
  import { Spinner } from './shared.js';
@@ -63,9 +63,9 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
63
63
  const nodeUrl = window.location.origin;
64
64
  const nodeId = session?.nodeId || '';
65
65
 
66
- const loadPackages = useCallback(async () => {
66
+ const loadPackages = useCallback(async ({ showSpinner = true } = {}) => {
67
67
  try {
68
- setLoading(true);
68
+ if (showSpinner) setLoading(true);
69
69
  const list = await knowledgeService.listMyPackages();
70
70
  const hydrated = await Promise.all(list.map(async (pkg) => {
71
71
  if (pkg.value) return pkg;
@@ -76,7 +76,7 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
76
76
  }));
77
77
  setPackages(hydrated);
78
78
  onStats?.({ knowledge: hydrated.length });
79
- } catch { setPackages([]); }
79
+ } catch { if (showSpinner) setPackages([]); } // keep old list on a transient live-update refetch
80
80
  finally { setLoading(false); }
81
81
  }, [onStats]);
82
82
 
@@ -133,7 +133,7 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
133
133
  const loadRef = useRef(loadPackages);
134
134
  loadRef.current = loadPackages;
135
135
  useEffect(() => {
136
- const handler = () => { loadRef.current(); loadFedConsents(); };
136
+ const handler = () => { loadRef.current({ showSpinner: false }); loadFedConsents(); }; // silent
137
137
  window.addEventListener('aimeat-live-update', handler);
138
138
  return () => window.removeEventListener('aimeat-live-update', handler);
139
139
  }, []);
@@ -178,7 +178,7 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
178
178
  : await knowledgeService.getAgentPrompt();
179
179
  const text = resp?.data?.prompt;
180
180
  if (text) {
181
- await navigator.clipboard.writeText(text);
181
+ await copyToClipboard(text);
182
182
  showToast(t('knowledge.actionBar.copy' + (type === 'human' ? 'HumanPrompt' : 'AgentPrompt')) + ' \u2713');
183
183
  } else {
184
184
  showToast('Prompt template not available yet');
@@ -276,12 +276,12 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
276
276
  try {
277
277
  const resp = await knowledgeService.exportPackage(packageId);
278
278
  const json = JSON.stringify(resp?.data || manifest, null, 2);
279
- await navigator.clipboard.writeText(json);
279
+ await copyToClipboard(json);
280
280
  showToast(t('knowledge.myKnowledge.exportCopied') || 'Package JSON copied to clipboard');
281
281
  } catch {
282
282
  // Fallback: export the manifest we already have
283
283
  try {
284
- await navigator.clipboard.writeText(JSON.stringify(manifest, null, 2));
284
+ await copyToClipboard(JSON.stringify(manifest, null, 2));
285
285
  showToast(t('knowledge.myKnowledge.exportCopied') || 'Package JSON copied to clipboard');
286
286
  } catch {
287
287
  showToast('Failed to export package');
@@ -481,7 +481,7 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
481
481
  <strong>${escHtml(label)}</strong>
482
482
  ${entry.key && entry.key !== label ? html`<span class="kpkg-detail-key">${escHtml(entry.key)}</span>` : null}
483
483
  </div>
484
- ${loadingEntries === pkg.key && !rawData ? html`<p class="kpkg-detail-loading">Loading...</p>` : null}
484
+ ${loadingEntries === pkg.key && !rawData ? html`<p class="kpkg-detail-loading">${t('common.loading')}</p>` : null}
485
485
  ${val && html`<pre class="kpkg-detail-value">${val}</pre>`}
486
486
  ${renderEntryRefs(entry.references)}
487
487
  ${renderRelatedEntries(entry.related_entries, allEntries)}
@@ -616,7 +616,7 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
616
616
  <!-- MY KNOWLEDGE -->
617
617
  <div class="kpkg-section">
618
618
  <h3>${t('knowledge.myKnowledge.title')}</h3>
619
- ${loading && html`<${Spinner} text="Loading..." />`}
619
+ ${loading && html`<${Spinner} text=${t('common.loading')} />`}
620
620
  ${!loading && packages.length === 0 && html`
621
621
  <p class="kpkg-empty">${t('knowledge.myKnowledge.empty')}</p>
622
622
  `}
@@ -742,7 +742,7 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
742
742
  <!-- KNOWLEDGE ORGANISMS -->
743
743
  <div class="kpkg-section">
744
744
  <h3>${t('knowledge.organisms.title')}</h3>
745
- ${organismLoading && html`<${Spinner} text="Loading..." />`}
745
+ ${organismLoading && html`<${Spinner} text=${t('common.loading')} />`}
746
746
  ${!organismLoading && organismPackages.length === 0 && html`
747
747
  <p class="kpkg-empty">${t('knowledge.organisms.empty')}</p>
748
748
  `}
@@ -762,7 +762,7 @@ export default function KnowledgeTab({ session, showToast, onStats }) {
762
762
  <!-- DISCOVER -->
763
763
  <div class="kpkg-section">
764
764
  <h3>${t('knowledge.discover.title')}</h3>
765
- ${discoverLoading && html`<${Spinner} text="Loading..." />`}
765
+ ${discoverLoading && html`<${Spinner} text=${t('common.loading')} />`}
766
766
  ${!discoverLoading && discovered.filter(p => p.sharing?.allow_clone !== false).length === 0 && html`
767
767
  <p class="kpkg-empty">${t('knowledge.discover.empty')}</p>
768
768
  `}
@@ -1,16 +1,22 @@
1
1
  /**
2
2
  * @file landing-page.js
3
- * @description Adaptive profile landing page with three user tier variants
4
- * (new / active / experienced). Shows tier-specific dashboard content with
5
- * onboarding, app strip, menu sections, and inline preview panels.
3
+ * @description Profile landing page. Navigation is a PERSISTENT, grouped sidebar
4
+ * (every tab always visible — no activity-based hiding) plus a content column
5
+ * showing either the selected tab or the home dashboard (ProfileCard +
6
+ * tier-based onboarding/app-strip). On mobile the sidebar is an off-canvas
7
+ * drawer. The logged-in pill + Logout live in the global shell header
8
+ * (spa.html), not here. Replaced the old new/active/experienced tier-adaptive
9
+ * menu, which was unpredictable for humans and agentic developers.
6
10
  * @structure
7
- * - computeTier() — exported heuristic for user tier detection
11
+ * - computeTier() — exported heuristic; now gates only the home onboarding content
8
12
  * - tierLevel() — exported numeric tier comparison helper
9
- * - ProfileCard, HeroOnboarding, KnowledgeCallout, KnowledgeButton,
10
- * GhostTiles, CortexSection, AppStrip — section sub-components
11
- * - MenuSection, MenuItem — generic menu layout components
13
+ * - SIDEBAR_GROUPS — grouped, always-visible tab list
14
+ * - ProfileCard, HeroOnboarding, KnowledgeCallout, GhostTiles, CortexSection,
15
+ * AppStrip — home/section sub-components
12
16
  * - LandingPage — main orchestrator (default export)
13
17
  * @version-history
18
+ * v2.0.0 — 2026-06-03 — Replace tier-adaptive menu with a persistent grouped sidebar
19
+ * + content column + mobile drawer; computeTier now only gates home onboarding.
14
20
  * v1.2.0 — 2026-03-19 — Expandable AppStrip chips with launch button; remove Generator primary style
15
21
  * v1.1.0 — 2026-03-19 — Persist open tab to localStorage across page reloads
16
22
  * v1.0.0 — 2026-03-18 — Initial adaptive landing page implementation
@@ -503,6 +509,50 @@ function InlineView({ tabId, label, onClose, renderTab }) {
503
509
  `;
504
510
  }
505
511
 
512
+ /* ───── Persistent sidebar groups (replaces the tier-adaptive menu) ─────
513
+ * Every tab is always present and grouped into stable sections — no activity-based
514
+ * hiding, so humans and agentic developers can predict where each tab lives.
515
+ * Group titles reuse existing i18n keys; tab labels reuse profile.tabs.* / *.tabLabel. */
516
+ const SIDEBAR_GROUPS = [
517
+ { titleKey: 'profile.landing.menuDaily', items: [
518
+ { id: 'agents', icon: '\u{1F916}', labelKey: 'profile.tabs.agents', badgeStat: 'agents' },
519
+ { id: 'scheduler', icon: '⏰', labelKey: 'profile.tabs.scheduler' },
520
+ { id: 'memory', icon: '\u{1F9E0}', labelKey: 'profile.tabs.memory' },
521
+ { id: 'boards', icon: '\u{1F4CB}', labelKey: 'profile.tabs.boards' },
522
+ { id: 'knowledge', icon: '\u{1F4DA}', labelKey: 'knowledge.tabLabel' },
523
+ { id: 'notifications', icon: '\u{1F514}', labelKey: 'profile.tabs.notifications' },
524
+ { id: 'apps', icon: '⚙️', labelKey: 'profile.tabs.apps' },
525
+ ] },
526
+ { titleKey: 'profile.landing.menuBuildShare', items: [
527
+ { id: 'generator', icon: '\u{1F534}', labelKey: 'profile.generator.tabLabel' },
528
+ { id: 'foundry', icon: '\u{1F3ED}', labelKey: 'profile.foundry.tabLabel' },
529
+ { id: 'calibrator', icon: '\u{1F3AF}', labelKey: 'profile.calibrator.tabLabel' },
530
+ { id: 'extensions', icon: '\u{1F50C}', labelKey: 'profile.tabs.extensions' },
531
+ { id: 'capabilities', icon: '⚡', labelKey: 'capabilities.tabLabel' },
532
+ { id: 'portfolio', icon: '\u{1F3A8}', labelKey: 'portfolio.tabLabel' },
533
+ { id: 'packages', icon: '\u{1F4E6}', labelKey: 'profile.tabs.packages' },
534
+ ] },
535
+ { titleKey: 'profile.landing.menuTechnical', items: [
536
+ { id: 'actions', icon: '\u{1F6E0}️', labelKey: 'profile.tabs.services' },
537
+ { id: 'work', icon: '\u{1F4CB}', labelKey: 'profile.tabs.work' },
538
+ { id: 'mcp', icon: '\u{1F517}', labelKey: 'profile.tabs.mcp' },
539
+ { id: 'chatsessions', icon: '\u{1F4AC}', labelKey: 'profile.tabs.chatSessions' },
540
+ ] },
541
+ { titleKey: 'profile.landing.menuPersonal', items: [
542
+ { id: 'wallet', icon: '\u{1F48E}', labelKey: 'profile.tabs.wallet' },
543
+ { id: 'email', icon: '\u{1F4E7}', labelKey: 'profile.tabs.email' },
544
+ { id: 'access', icon: '\u{1F510}', labelKey: 'profile.tabs.access' },
545
+ { id: 'organisms', icon: '\u{1F3E2}', labelKey: 'profile.tabs.organisms' },
546
+ { id: 'dataWallet', icon: '\u{1F512}', labelKey: 'profile.tabs.dataWallet' },
547
+ ] },
548
+ { titleKey: 'profile.landing.menuInfra', items: [
549
+ { id: 'federation', icon: '\u{1F310}', labelKey: 'profile.tabs.federation' },
550
+ { id: 'nodes', icon: '\u{1F5A5}️', labelKey: 'profile.tabs.nodes' },
551
+ { id: 'nodeStats', icon: '\u{1F4CA}', labelKey: 'profile.tabs.nodeStats' },
552
+ { id: 'security', icon: '\u{1F6E1}️', labelKey: 'profile.tabs.security' },
553
+ ] },
554
+ ];
555
+
506
556
  /* ───── Main landing page ───── */
507
557
 
508
558
  export default function LandingPage({ tier, stats, session, navigate, showToast, locale, renderTab, getTabLabel }) {
@@ -520,6 +570,7 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
520
570
  return null;
521
571
  });
522
572
  const [expanded, setExpanded] = useState(false);
573
+ const [drawerOpen, setDrawerOpen] = useState(false);
523
574
 
524
575
  const owner = session.owner;
525
576
 
@@ -578,11 +629,7 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
578
629
  const isExperienced = tier === 'experienced';
579
630
 
580
631
  return html`
581
- <div class="pf-landing">
582
-
583
- <${ProfileCard} tier=${tier} stats=${stats} session=${session}
584
- onEditProfile=${() => setEditOpen(true)}
585
- onChangePassword=${() => setPwOpen(true)} />
632
+ <div class="pf-shell${drawerOpen ? ' pf-shell--open' : ''}">
586
633
 
587
634
  ${editOpen && html`<${EditProfileModal}
588
635
  session=${session}
@@ -595,150 +642,54 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
595
642
  onChanged=${() => { setPwOpen(false); showToast?.(t('profile.landing.passwordChanged')); }}
596
643
  />`}
597
644
 
598
- ${isNew && html`
599
- <${HeroOnboarding} switchTab=${(id) => open(id, 'hero')} />
600
- ${viewAt('hero')}
601
-
602
- <${KnowledgeCallout} switchTab=${() => open('knowledge', 'knowledge-new')} />
603
- ${viewAt('knowledge-new')}
645
+ <button class="pf-mnav-toggle" onClick=${() => setDrawerOpen(o => !o)}>☰ ${t('profile.landing.menu')}</button>
646
+ <div class="pf-scrim" onClick=${() => setDrawerOpen(false)}></div>
604
647
 
605
- <${GhostTiles} switchTab=${() => open('packages', 'ghost')} />
606
- ${viewAt('ghost')}
607
-
608
- <${CortexSection} switchTab=${() => open('extensions', 'cortex')} />
609
- ${viewAt('cortex')}
610
-
611
- <${MenuSection} title=${t('profile.landing.menuManagement')}>
612
- <div class="pf-menu-grid">
613
- <${MenuItem} icon="\u{1F9E0}" label=${t('profile.tabs.memory')} active=${isOpen('memory')} onClick=${() => open('memory', 'manage-new')} />
614
- <${MenuItem} icon="\u{1F48E}" label=${t('profile.tabs.wallet')} active=${isOpen('wallet')} onClick=${() => open('wallet', 'manage-new')} />
615
- <${MenuItem} icon="\u{1F510}" label=${t('profile.tabs.access')} active=${isOpen('access')} onClick=${() => open('access', 'manage-new')} />
616
- <${MenuItem} icon="\u{1F4E7}" label=${t('profile.tabs.email')} active=${isOpen('email')} onClick=${() => open('email', 'manage-new')} />
617
- </div>
618
- <div class="pf-expand-trigger" onClick=${() => setExpanded(!expanded)}>
619
- ${expanded ? '\u25BE' : '\u25B8'} ${t('profile.landing.expandAll')}
620
- </div>
621
- ${expanded && html`
622
- <div class="pf-menu-grid pf-menu-grid-expanded">
623
- <${MenuItem} icon="\u{1F916}" label=${t('profile.tabs.agents')} active=${isOpen('agents')} onClick=${() => open('agents', 'manage-new')} />
624
- <${MenuItem} icon="\u{1F4AC}" label=${t('profile.tabs.chatSessions')} active=${isOpen('chatsessions')} onClick=${() => open('chatsessions', 'manage-new')} />
625
- <${MenuItem} icon="\u{1F50C}" label=${t('profile.tabs.extensions')} active=${isOpen('extensions')} onClick=${() => open('extensions', 'manage-new')} />
626
- <${MenuItem} icon="\u{1F534}" label=${t('profile.generator.tabLabel')} active=${isOpen('generator')} onClick=${() => open('generator', 'manage-new')} />
627
- <${MenuItem} icon="\u{1F3ED}" label=${t('profile.foundry.tabLabel')} active=${isOpen('foundry')} onClick=${() => open('foundry', 'manage-new')} />
628
- <${MenuItem} icon="\u{1F3AF}" label=${t('profile.calibrator.tabLabel')} active=${isOpen('calibrator')} onClick=${() => open('calibrator', 'manage-new')} />
629
- <${MenuItem} icon="\u{1F4E6}" label=${t('profile.tabs.packages')} active=${isOpen('packages')} onClick=${() => open('packages', 'manage-new')} />
630
- <${MenuItem} icon="\u{1F4DA}" label=${t('knowledge.tabLabel')} active=${isOpen('knowledge')} onClick=${() => open('knowledge', 'manage-new')} />
631
- <${MenuItem} icon="\u{1F4CB}" label=${t('profile.tabs.boards')} active=${isOpen('boards')} onClick=${() => open('boards', 'manage-new')} />
632
- <${MenuItem} icon="\u{1F3A8}" label=${t('portfolio.tabLabel')} active=${isOpen('portfolio')} onClick=${() => open('portfolio', 'manage-new')} />
633
- <${MenuItem} icon="\u{1F6E0}\uFE0F" label=${t('profile.tabs.services')} active=${isOpen('actions')} onClick=${() => open('actions', 'manage-new')} />
634
- <${MenuItem} icon="\u{1F4CB}" label=${t('profile.tabs.work')} active=${isOpen('work')} onClick=${() => open('work', 'manage-new')} />
635
- <${MenuItem} icon="\u{1F3E2}" label=${t('profile.tabs.organisms')} active=${isOpen('organisms')} onClick=${() => open('organisms', 'manage-new')} />
636
- <${MenuItem} icon="\u{1F512}" label=${t('profile.tabs.dataWallet')} active=${isOpen('dataWallet')} onClick=${() => open('dataWallet', 'manage-new')} />
637
- <${MenuItem} icon="\u{1F514}" label=${t('profile.tabs.notifications')} active=${isOpen('notifications')} onClick=${() => open('notifications', 'manage-new')} />
638
- <${MenuItem} icon="\u{1F512}" label=${t('profile.tabs.security')} active=${isOpen('security')} onClick=${() => open('security', 'manage-new')} />
639
- <${MenuItem} icon="⚡" label=${t('capabilities.tabLabel')} active=${isOpen('capabilities')} onClick=${() => open('capabilities', 'manage-new')} />
640
- </div>
641
- `}
642
- <//>
643
- ${viewAt('manage-new')}
644
- `}
648
+ <aside class="pf-sidebar">
649
+ <div class="pf-side-identity">
650
+ <div class="pf-side-avatar"></div>
651
+ <div class="pf-side-id-name">${session.displayName || owner}</div>
652
+ </div>
645
653
 
646
- ${(isActive || isExperienced) && html`
647
- <${AppStrip} apps=${apps} switchTab=${(id) => open(id, 'apps')} />
648
- ${viewAt('apps')}
649
-
650
- <${MenuSection} title=${t('profile.landing.menuDaily')}>
651
- <div class="pf-menu-grid">
652
- <${MenuItem} icon="\u{1F4E6}" label=${t('profile.landing.packagesExt')} active=${isOpen('packages')} onClick=${() => open('packages', 'daily')} />
653
- <${MenuItem} icon="\u{1F514}" label=${t('profile.landing.notificationsBadge')} active=${isOpen('notifications')} onClick=${() => open('notifications', 'daily')} />
654
- <${MenuItem} icon="\u{1F916}" label=${t('profile.tabs.agents')}
655
- badge=${typeof stats.agents === 'number' ? stats.agents : 0} badgeMuted
656
- active=${isOpen('agents')} onClick=${() => open('agents', 'daily')} />
657
- <${MenuItem} icon="\u{1F9E0}" label=${t('profile.tabs.memory')} active=${isOpen('memory')} onClick=${() => open('memory', 'daily')} />
658
- <${MenuItem} icon="\u{1F4CB}" label=${t('profile.tabs.boards')} active=${isOpen('boards')} onClick=${() => open('boards', 'daily')} />
659
- <${MenuItem} icon="\u{1F4DA}" label=${t('knowledge.tabLabel')} active=${isOpen('knowledge')} onClick=${() => open('knowledge', 'daily')} />
654
+ <button class="pf-side-item${!openView ? ' pf-side-item--active' : ''}"
655
+ onClick=${() => { close(); setDrawerOpen(false); }}>
656
+ <span class="pf-side-ico">\u{1F3E0}</span><span class="pf-side-label">${t('profile.landing.home')}</span>
657
+ </button>
658
+
659
+ ${SIDEBAR_GROUPS.map(g => html`
660
+ <div class="pf-side-group">
661
+ <div class="pf-side-group-title">${t(g.titleKey)}</div>
662
+ ${g.items.map(it => html`
663
+ <button class="pf-side-item${isOpen(it.id) ? ' pf-side-item--active' : ''}"
664
+ onClick=${() => { open(it.id, 'main'); setDrawerOpen(false); }}>
665
+ <span class="pf-side-ico">${it.icon}</span>
666
+ <span class="pf-side-label">${t(it.labelKey)}</span>
667
+ ${it.badgeStat && typeof stats?.[it.badgeStat] === 'number' && stats[it.badgeStat] > 0
668
+ ? html`<span class="pf-side-badge">${stats[it.badgeStat]}</span>` : null}
669
+ </button>
670
+ `)}
660
671
  </div>
661
- <//>
662
- ${viewAt('daily')}
663
-
664
- ${isActive && html`
665
- <${KnowledgeButton} switchTab=${() => open('knowledge', 'knowledge-btn')} />
666
- ${viewAt('knowledge-btn')}
667
- `}
672
+ `)}
673
+ </aside>
668
674
 
669
- <${MenuSection} title=${t('profile.landing.menuBuildShare')}>
670
- <div class="pf-menu-grid">
671
- <${MenuItem} icon="\u{1F534}" label=${t('profile.generator.tabLabel')} active=${isOpen('generator')} onClick=${() => open('generator', 'build')} />
672
- <${MenuItem} icon="\u{1F3ED}" label=${t('profile.foundry.tabLabel')} active=${isOpen('foundry')} onClick=${() => open('foundry', 'build')} />
673
- <${MenuItem} icon="\u{1F3AF}" label=${t('profile.calibrator.tabLabel')} active=${isOpen('calibrator')} onClick=${() => open('calibrator', 'build')} />
674
- <${MenuItem} icon="\u{1F50C}" label=${t('profile.tabs.extensions')} active=${isOpen('extensions')} onClick=${() => open('extensions', 'build')} />
675
- <${MenuItem} icon="⚡" label=${t('capabilities.tabLabel')} active=${isOpen('capabilities')} onClick=${() => open('capabilities', 'build')} />
676
- <${MenuItem} icon="\u{1F3A8}" label=${t('portfolio.tabLabel')} active=${isOpen('portfolio')} onClick=${() => open('portfolio', 'build')} />
677
- ${isExperienced && html`
678
- <${MenuItem} icon="\u{1F4E4}" label=${t('profile.landing.ownPackages')} active=${isOpen('packages')} onClick=${() => open('packages', 'build')} />
679
- `}
675
+ <main class="pf-content">
676
+ ${openView ? html`
677
+ <div class="pf-content-head">
678
+ <button class="pf-back-btn" onClick=${close}>← ${t('profile.landing.home')}</button>
679
+ <span class="pf-back-current">${getTabLabel(openView.tabId)}</span>
680
680
  </div>
681
- <//>
682
- ${viewAt('build')}
683
-
684
- ${isExperienced ? html`
685
- <${MenuSection} title=${t('profile.landing.menuManagement')}>
686
- <div class="pf-menu-subgroup">
687
- <div class="pf-menu-subgroup-title">${t('profile.landing.menuTechnical')}</div>
688
- <div class="pf-menu-grid">
689
- <${MenuItem} icon="\u2699\uFE0F" label=${t('profile.tabs.apps')} active=${isOpen('apps')} onClick=${() => open('apps', 'manage')} />
690
- <${MenuItem} icon="\u{1F517}" label=${t('profile.tabs.mcp')} active=${isOpen('mcp')} onClick=${() => open('mcp', 'manage')} />
691
- <${MenuItem} icon="\u{1F6E0}\uFE0F" label=${t('profile.tabs.services')} active=${isOpen('actions')} onClick=${() => open('actions', 'manage')} />
692
- <${MenuItem} icon="\u{1F4CB}" label=${t('profile.tabs.work')} active=${isOpen('work')} onClick=${() => open('work', 'manage')} />
693
- <${MenuItem} icon="\u{1F4AC}" label=${t('profile.tabs.chatSessions')} active=${isOpen('chatsessions')} onClick=${() => open('chatsessions', 'manage')} />
694
- </div>
695
- </div>
696
- <div class="pf-menu-subgroup">
697
- <div class="pf-menu-subgroup-title">${t('profile.landing.menuPersonal')}</div>
698
- <div class="pf-menu-grid">
699
- <${MenuItem} icon="\u{1F48E}" label=${t('profile.tabs.wallet')} active=${isOpen('wallet')} onClick=${() => open('wallet', 'manage')} />
700
- <${MenuItem} icon="\u{1F4E7}" label=${t('profile.tabs.email')} active=${isOpen('email')} onClick=${() => open('email', 'manage')} />
701
- <${MenuItem} icon="\u{1F510}" label=${t('profile.tabs.access')} active=${isOpen('access')} onClick=${() => open('access', 'manage')} />
702
- <${MenuItem} icon="\u{1F3E2}" label=${t('profile.tabs.organisms')} active=${isOpen('organisms')} onClick=${() => open('organisms', 'manage')} />
703
- </div>
704
- </div>
705
- <//>
706
- ${viewAt('manage')}
707
-
708
- <${MenuSection} title=${t('profile.landing.menuInfra')} annotation=${t('profile.landing.menuInfraAnnotation')}>
709
- <div class="pf-menu-grid">
710
- <${MenuItem} icon="\u{1F310}" label=${t('profile.tabs.federation')} indigo active=${isOpen('federation')} onClick=${() => open('federation', 'infra')} />
711
- <${MenuItem} icon="\u{1F5A5}\uFE0F" label=${t('profile.tabs.nodes')} active=${isOpen('nodes')} onClick=${() => open('nodes', 'infra')} />
712
- <${MenuItem} icon="\u{1F4CA}" label=${t('profile.tabs.nodeStats')} active=${isOpen('nodeStats')} onClick=${() => open('nodeStats', 'infra')} />
713
- <${MenuItem} icon="\u{1F512}" label=${t('profile.tabs.security')} active=${isOpen('security')} onClick=${() => open('security', 'infra')} />
714
- </div>
715
- <//>
716
- ${viewAt('infra')}
681
+ <div class="pf-content-body">${renderTab(openView.tabId)}</div>
717
682
  ` : html`
718
- <${MenuSection} title=${t('profile.landing.menuManagement')}>
719
- <div class="pf-menu-grid">
720
- <${MenuItem} icon="\u2699\uFE0F" label=${t('profile.tabs.apps')} active=${isOpen('apps')} onClick=${() => open('apps', 'manage')} />
721
- <${MenuItem} icon="\u{1F517}" label=${t('profile.tabs.mcp')} active=${isOpen('mcp')} onClick=${() => open('mcp', 'manage')} />
722
- <${MenuItem} icon="\u{1F48E}" label=${t('profile.tabs.wallet')} active=${isOpen('wallet')} onClick=${() => open('wallet', 'manage')} />
723
- <${MenuItem} icon="\u{1F4E7}" label=${t('profile.tabs.email')} active=${isOpen('email')} onClick=${() => open('email', 'manage')} />
724
- <${MenuItem} icon="\u{1F510}" label=${t('profile.tabs.access')} active=${isOpen('access')} onClick=${() => open('access', 'manage')} />
725
- </div>
726
- <div class="pf-expand-trigger" onClick=${() => setExpanded(!expanded)}>
727
- ${expanded ? '\u25BE' : '\u25B8'} ${t('profile.landing.expandMore')}
728
- </div>
729
- ${expanded && html`
730
- <div class="pf-menu-grid pf-menu-grid-expanded">
731
- <${MenuItem} icon="\u{1F6E0}\uFE0F" label=${t('profile.tabs.services')} active=${isOpen('actions')} onClick=${() => open('actions', 'manage')} />
732
- <${MenuItem} icon="\u{1F4CB}" label=${t('profile.tabs.work')} active=${isOpen('work')} onClick=${() => open('work', 'manage')} />
733
- <${MenuItem} icon="\u{1F4AC}" label=${t('profile.tabs.chatSessions')} active=${isOpen('chatsessions')} onClick=${() => open('chatsessions', 'manage')} />
734
- <${MenuItem} icon="\u{1F512}" label=${t('profile.tabs.dataWallet')} active=${isOpen('dataWallet')} onClick=${() => open('dataWallet', 'manage')} />
735
- <${MenuItem} icon="\u{1F3E2}" label=${t('profile.tabs.organisms')} active=${isOpen('organisms')} onClick=${() => open('organisms', 'manage')} />
736
- </div>
737
- `}
738
- <//>
739
- ${viewAt('manage')}
683
+ <${ProfileCard} tier=${tier} stats=${stats} session=${session}
684
+ onEditProfile=${() => setEditOpen(true)}
685
+ onChangePassword=${() => setPwOpen(true)} />
686
+ ${isNew ? html`<${HeroOnboarding} switchTab=${(id) => open(id, 'main')} />` : null}
687
+ ${(isNew || isActive) ? html`<${KnowledgeCallout} switchTab=${() => open('knowledge', 'main')} />` : null}
688
+ ${isNew ? html`<${GhostTiles} switchTab=${() => open('packages', 'main')} />` : null}
689
+ <${CortexSection} switchTab=${() => open('extensions', 'main')} />
690
+ <${AppStrip} apps=${apps} switchTab=${(id) => open(id, 'main')} />
740
691
  `}
741
- `}
692
+ </main>
742
693
  </div>
743
694
  `;
744
695
  }
@@ -7,6 +7,10 @@
7
7
  * v1.1.0 — 2026-03-18 — Fix: use AuthImage for thumbnails and authenticated download to avoid 401
8
8
  * v1.2.0 — 2026-05-22 — Add Browse Home / Browse Remote panels for federation memory sync
9
9
  * v1.3.0 — 2026-05-22 — Add timestamps, sort controls, fix group picker rotation bug
10
+ * v1.4.0 — 2026-06-02 — Component unification (#2): edit modal uses the canonical
11
+ * <Modal> component (Escape/backdrop close + header ✕).
12
+ * v1.5.0 — 2026-06-02 — Component unification (#1): "Copy URLs" button uses canonical
13
+ * <CopyButton> (toast preserved via onCopied).
10
14
  */
11
15
  import { h } from 'preact';
12
16
  import { useState, useEffect, useRef } from 'preact/hooks';
@@ -24,7 +28,8 @@ import { getNodeUrl } from '/js/services/auth.js';
24
28
  import { listGroups } from '/js/services/sharing-groups.js';
25
29
  import TagCloud from '/js/components/tag-cloud.js';
26
30
  import TagEditor from '/js/components/tag-editor.js';
27
- import { useConfirm } from '/components/Modal.js';
31
+ import { Modal, useConfirm } from '/components/Modal.js';
32
+ import { CopyButton } from '/components/CopyButton.js';
28
33
 
29
34
  export default function MemoryTab({ session, showToast, onStats }) {
30
35
  const { confirm, ConfirmUI } = useConfirm();
@@ -490,15 +495,17 @@ export default function MemoryTab({ session, showToast, onStats }) {
490
495
  });
491
496
  };
492
497
 
493
- const copyFileUrls = () => {
494
- const urls = filtered.map(f => `${NODE_URL}/v1/memory/files/${encodeURIComponent(f.key || f.name)}`).join('\n');
495
- navigator.clipboard.writeText(urls).then(() => showToast(t('profile.files.urlsCopied') || `${filtered.length} URLs copied`));
496
- };
498
+ const fileUrls = filtered.map(f => `${NODE_URL}/v1/memory/files/${encodeURIComponent(f.key || f.name)}`).join('\n');
497
499
 
498
500
  return html`
499
501
  <div class="action-bar">
500
502
  <button class="btn-primary" onClick=${() => setShowFileForm(!showFileForm)}>${t('profile.files.uploadBtn')}</button>
501
- ${filtered.length > 0 && html`<button class="btn-outline btn-sm" onClick=${copyFileUrls} title="Copy file URLs">\u{1F4CB} ${t('profile.files.copyUrls') || 'Copy URLs'} (${filtered.length})</button>`}
503
+ ${filtered.length > 0 && html`<${CopyButton}
504
+ text=${fileUrls}
505
+ label=${`\u{1F4CB} ${t('profile.files.copyUrls') || 'Copy URLs'} (${filtered.length})`}
506
+ title="Copy file URLs"
507
+ className="btn-outline btn-sm"
508
+ onCopied=${() => showToast(t('profile.files.urlsCopied') || `${filtered.length} URLs copied`)} />`}
502
509
  <span class="text-meta-sm">${t('profile.files.sizeLimit')}</span>
503
510
  </div>
504
511
  <${TagCloud} tags=${[...allTags]} selected=${fileTagFilter} onToggle=${toggleTag} onClear=${() => setFileTagFilter(new Set())} />
@@ -1081,9 +1088,7 @@ function EditMemoryModal({ memKey, initialValue, initialVisibility, initialVersi
1081
1088
  }
1082
1089
  };
1083
1090
  return html`
1084
- <div class="modal-overlay" onClick=${e => { if (e.target.className.includes('modal-overlay')) onCancel(); }}>
1085
- <div class="modal">
1086
- <h3>${t('profile.memory.editTitle')}: ${escHtml(memKey)}</h3>
1091
+ <${Modal} open=${true} onClose=${onCancel} title=${`${t('profile.memory.editTitle')}: ${memKey}`}>
1087
1092
  <div class="form-row flex-row mb-half">
1088
1093
  <label class="pf-label-inline">${t('profile.memory.visLabel')}</label>
1089
1094
  <button class="kpkg-vis-pill"
@@ -1111,6 +1116,5 @@ function EditMemoryModal({ memKey, initialValue, initialVisibility, initialVersi
1111
1116
  <button class="btn-primary" onClick=${() => onSave(value, vis, initialVersion, vis === 'group' ? groupId : undefined)}>${t('profile.save')}</button>
1112
1117
  <button class="btn-outline" onClick=${onCancel}>${t('profile.cancel')}</button>
1113
1118
  </div>
1114
- </div>
1115
- </div>`;
1119
+ <//>`;
1116
1120
  }
@@ -30,9 +30,9 @@ function fmtBytes(b) {
30
30
  }
31
31
 
32
32
  function StatCard({ label, value, color }) {
33
- return html`<div class="card stat-card">
34
- <div class="stat-value" style=${`color:${color}`}>${value}</div>
35
- <div class="stat-label">${label}</div>
33
+ return html`<div class="stat-card">
34
+ <div class="stat-card-value" style=${`color:${color}`}>${value}</div>
35
+ <div class="stat-card-label">${label}</div>
36
36
  </div>`;
37
37
  }
38
38
 
@@ -5,14 +5,21 @@
5
5
  * @version-history
6
6
  * v1.0.0 — 2026-03-16 — Initial nodes tab
7
7
  * v1.1.0 — 2026-03-17 — Replace inline styles with CSS classes
8
+ * v1.2.0 — 2026-06-02 — Component unification (#1): tunnel-URL copy button uses
9
+ * canonical <CopyButton> (toast preserved via onCopied).
10
+ * v1.3.0 — 2026-06-02 — Component unification (#11): node status dot uses
11
+ * canonical <StatusDot> (online/offline/degraded/detached) instead of the
12
+ * bespoke 10px .pn-status-dot (now 8px, with title tooltip added).
8
13
  */
9
14
  import { h } from 'preact';
10
15
  import { useState, useEffect, useRef } from 'preact/hooks';
11
16
  import htm from 'htm';
12
17
  const html = htm.bind(h);
13
18
  import { t } from '/js/i18n.js';
14
- import { escHtml, timeAgo, copyToClipboard } from '/js/utils.js';
19
+ import { escHtml, timeAgo } from '/js/utils.js';
15
20
  import { Spinner } from './shared.js';
21
+ import { CopyButton } from '/components/CopyButton.js';
22
+ import { StatusDot } from '/components/StatusDot.js';
16
23
  import { useConfirm } from '/components/Modal.js';
17
24
  import * as nodesService from '/js/services/nodes.js';
18
25
  import { getNodeUrl } from '/js/services/auth.js';
@@ -99,7 +106,7 @@ export default function NodesTab({ session, showToast, onStats }) {
99
106
  setExpandedNodes(s);
100
107
  }}>
101
108
  <div class="pn-header-left">
102
- <div class="pn-status-dot ${statusClass}"></div>
109
+ <${StatusDot} status=${statusClass} title=${statusLabel} />
103
110
  <span class="pn-name">${escHtml(node.node_id)}</span>
104
111
  </div>
105
112
  <div class="pn-badges">
@@ -117,7 +124,7 @@ export default function NodesTab({ session, showToast, onStats }) {
117
124
  <span class="pn-detail-label">${t('profile.nodes.tunnelUrl')}</span>
118
125
  <span class="pn-detail-value flex-row">
119
126
  <code class="text-meta-sm">${tunnelUrl}</code>
120
- <button class="pn-copy-btn" onClick=${() => { copyToClipboard(tunnelUrl).then(() => showToast(t('profile.nodes.copied'))); }}>${t('profile.nodes.copyUrl')}</button>
127
+ <${CopyButton} text=${tunnelUrl} label=${t('profile.nodes.copyUrl')} className="pn-copy-btn" onCopied=${() => showToast(t('profile.nodes.copied'))} />
121
128
  </span>
122
129
  </div>
123
130
  <div class="pn-padding-half">
@@ -27,13 +27,13 @@ export default function NotificationsTab({ session, showToast }) {
27
27
 
28
28
  // Re-check email verification when other tabs update data (e.g. email confirmed)
29
29
  useEffect(() => {
30
- const handler = () => { if (session) checkSubscription(); };
30
+ const handler = () => { if (session) checkSubscription({ showSpinner: false }); }; // silent
31
31
  window.addEventListener('aimeat-live-update', handler);
32
32
  return () => window.removeEventListener('aimeat-live-update', handler);
33
33
  }, [session]);
34
34
 
35
- async function checkSubscription() {
36
- setLoading(true);
35
+ async function checkSubscription({ showSpinner = true } = {}) {
36
+ if (showSpinner) setLoading(true);
37
37
  try {
38
38
  // Get VAPID key
39
39
  const vapidRes = await apiGet('/v1/push/vapid-key');