aimeat 1.25.1 → 1.26.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 (229) hide show
  1. package/README.md +8 -1
  2. package/dist/locales/en.json +130 -0
  3. package/dist/locales/fi.json +130 -0
  4. package/dist/public/components/ImageDeliverable.js +2 -1
  5. package/dist/public/components/Modal.js +3 -2
  6. package/dist/public/components/NotificationBell.js +2 -1
  7. package/dist/public/components/PresenceDot.js +53 -0
  8. package/dist/public/css/components/markdown.css +67 -0
  9. package/dist/public/css/theme.css +30 -0
  10. package/dist/public/css/views/notebook.css +195 -0
  11. package/dist/public/css/views/profile.css +15 -0
  12. package/dist/public/js/api.js +5 -3
  13. package/dist/public/js/components/auth-image.js +3 -2
  14. package/dist/public/js/components/tag-cloud.js +1 -1
  15. package/dist/public/js/components/tag-editor.js +1 -1
  16. package/dist/public/js/hooks.js +1 -2
  17. package/dist/public/js/presence-store.js +97 -0
  18. package/dist/public/js/services/admin.js +6 -0
  19. package/dist/public/js/services/boards.js +1 -1
  20. package/dist/public/js/services/ecosystem.js +1 -1
  21. package/dist/public/js/services/foundry-prompts-base.js +2 -2
  22. package/dist/public/js/services/foundry-smoke.js +2 -1
  23. package/dist/public/js/services/foundry-validate.js +6 -8
  24. package/dist/public/js/services/foundry.js +4 -3
  25. package/dist/public/js/services/generator-prompts-base.js +2 -2
  26. package/dist/public/js/services/generator-smoke.js +2 -1
  27. package/dist/public/js/services/generator-validate.js +5 -7
  28. package/dist/public/js/services/generator.js +2 -1
  29. package/dist/public/js/services/memory.js +13 -0
  30. package/dist/public/js/services/messages.js +2 -1
  31. package/dist/public/js/services/notebook.js +124 -0
  32. package/dist/public/js/services/packages.js +3 -2
  33. package/dist/public/js/services/presence.js +35 -0
  34. package/dist/public/js/style-testbed.js +6 -5
  35. package/dist/public/lib/minidenticons.min.js +1 -0
  36. package/dist/public/lib/yaml.mjs +1 -0
  37. package/dist/public/spa.html +68 -8
  38. package/dist/public/types/aimeat-globals.d.ts +52 -0
  39. package/dist/public/types/vendored-libs.d.ts +56 -0
  40. package/dist/public/views/_template.js +3 -1
  41. package/dist/public/views/admin/agent-integration-tab.js +4 -3
  42. package/dist/public/views/admin/agents-tab.js +3 -2
  43. package/dist/public/views/admin/config-tab.js +4 -3
  44. package/dist/public/views/admin/economy-tab.js +4 -3
  45. package/dist/public/views/admin/federation-tab.js +138 -1
  46. package/dist/public/views/admin/maintenance-tab.js +1 -1
  47. package/dist/public/views/admin/overview-tab.js +4 -3
  48. package/dist/public/views/admin/portal-tab.js +90 -2
  49. package/dist/public/views/admin/push-tab.js +8 -6
  50. package/dist/public/views/admin/security-tab.js +7 -7
  51. package/dist/public/views/admin.js +2 -1
  52. package/dist/public/views/doc-solo.js +3 -1
  53. package/dist/public/views/guides.js +8 -4
  54. package/dist/public/views/hobbies.js +4 -2
  55. package/dist/public/views/marketplace.js +10 -9
  56. package/dist/public/views/openclaw.js +1 -1
  57. package/dist/public/views/portal-dev.js +5 -2
  58. package/dist/public/views/portal.js +3 -2
  59. package/dist/public/views/profile/agents/shared-board.js +4 -2
  60. package/dist/public/views/profile/agents/tab-messages.js +13 -11
  61. package/dist/public/views/profile/agents-messages-subtab.js +2 -1
  62. package/dist/public/views/profile/apps-tab.js +2 -1
  63. package/dist/public/views/profile/boards-tab.js +3 -2
  64. package/dist/public/views/profile/calibrator-batch.js +3 -2
  65. package/dist/public/views/profile/data-wallet-tab.js +2 -1
  66. package/dist/public/views/profile/extensions-tab.js +4 -3
  67. package/dist/public/views/profile/federation-tab.js +4 -0
  68. package/dist/public/views/profile/foundry-dashboard/use-autopilot.js +3 -2
  69. package/dist/public/views/profile/foundry-detail.js +1 -1
  70. package/dist/public/views/profile/generator-detail.js +1 -1
  71. package/dist/public/views/profile/inbox-tab.js +7 -3
  72. package/dist/public/views/profile/inline-panels.js +3 -1
  73. package/dist/public/views/profile/knowledge-tab.js +2 -1
  74. package/dist/public/views/profile/landing-page.js +88 -0
  75. package/dist/public/views/profile/memory-tab.js +6 -5
  76. package/dist/public/views/profile/notebook-tab.js +500 -0
  77. package/dist/public/views/profile/organisms/activity-panel.js +135 -0
  78. package/dist/public/views/profile/organisms/agents.js +141 -0
  79. package/dist/public/views/profile/organisms/document.js +274 -0
  80. package/dist/public/views/profile/organisms/helpers.js +61 -0
  81. package/dist/public/views/profile/organisms/home.js +315 -0
  82. package/dist/public/views/profile/organisms/members.js +220 -0
  83. package/dist/public/views/profile/organisms/panels.js +173 -0
  84. package/dist/public/views/profile/organisms/participants-panel.js +160 -0
  85. package/dist/public/views/profile/organisms/schema-form.js +127 -0
  86. package/dist/public/views/profile/organisms/sources-panel.js +164 -0
  87. package/dist/public/views/profile/organisms/widgets.js +58 -0
  88. package/dist/public/views/profile/organisms/workspace-comments.js +99 -0
  89. package/dist/public/views/profile/organisms/workspace-list.js +268 -0
  90. package/dist/public/views/profile/organisms/workspace.js +1272 -0
  91. package/dist/public/views/profile/organisms-tab.js +456 -3819
  92. package/dist/public/views/profile/shared.js +64 -2
  93. package/dist/public/views/profile/workflows-form.js +1 -1
  94. package/dist/public/views/profile.js +16 -14
  95. package/dist/public/views/public-knowledge-viewer.js +3 -2
  96. package/dist/public/views/public-workspace-viewer.js +2 -1
  97. package/dist/scripts/check-importmap.d.ts +2 -0
  98. package/dist/scripts/check-importmap.d.ts.map +1 -0
  99. package/dist/scripts/check-importmap.js +102 -0
  100. package/dist/scripts/check-importmap.js.map +1 -0
  101. package/dist/src/config.d.ts +10 -0
  102. package/dist/src/config.d.ts.map +1 -1
  103. package/dist/src/config.js +5 -0
  104. package/dist/src/config.js.map +1 -1
  105. package/dist/src/generated/api-types.d.ts +852 -11
  106. package/dist/src/generated/api-types.d.ts.map +1 -1
  107. package/dist/src/generated/prisma-postgres/edge.js +18 -5
  108. package/dist/src/generated/prisma-postgres/index-browser.js +15 -2
  109. package/dist/src/generated/prisma-postgres/index.d.ts +422 -7
  110. package/dist/src/generated/prisma-postgres/index.js +18 -5
  111. package/dist/src/generated/prisma-postgres/package.json +1 -1
  112. package/dist/src/generated/prisma-postgres/schema.prisma +30 -12
  113. package/dist/src/generated/prisma-postgres/wasm.js +18 -5
  114. package/dist/src/routes/federation-peer.d.ts.map +1 -1
  115. package/dist/src/routes/federation-peer.js +327 -43
  116. package/dist/src/routes/federation-peer.js.map +1 -1
  117. package/dist/src/routes/federation-sync.d.ts +2 -0
  118. package/dist/src/routes/federation-sync.d.ts.map +1 -1
  119. package/dist/src/routes/federation-sync.js +20 -1
  120. package/dist/src/routes/federation-sync.js.map +1 -1
  121. package/dist/src/routes/librarian.d.ts +18 -0
  122. package/dist/src/routes/librarian.d.ts.map +1 -0
  123. package/dist/src/routes/librarian.js +88 -0
  124. package/dist/src/routes/librarian.js.map +1 -0
  125. package/dist/src/routes/portal.d.ts.map +1 -1
  126. package/dist/src/routes/portal.js +9 -1
  127. package/dist/src/routes/portal.js.map +1 -1
  128. package/dist/src/routes/presence.d.ts +23 -0
  129. package/dist/src/routes/presence.d.ts.map +1 -0
  130. package/dist/src/routes/presence.js +88 -0
  131. package/dist/src/routes/presence.js.map +1 -0
  132. package/dist/src/routes/site.d.ts +2 -0
  133. package/dist/src/routes/site.d.ts.map +1 -1
  134. package/dist/src/routes/site.js +27 -0
  135. package/dist/src/routes/site.js.map +1 -1
  136. package/dist/src/routes/sse.d.ts +2 -0
  137. package/dist/src/routes/sse.d.ts.map +1 -1
  138. package/dist/src/routes/sse.js +9 -0
  139. package/dist/src/routes/sse.js.map +1 -1
  140. package/dist/src/routes/wellknown.d.ts.map +1 -1
  141. package/dist/src/routes/wellknown.js +5 -0
  142. package/dist/src/routes/wellknown.js.map +1 -1
  143. package/dist/src/routes/work.d.ts.map +1 -1
  144. package/dist/src/routes/work.js +6 -0
  145. package/dist/src/routes/work.js.map +1 -1
  146. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  147. package/dist/src/server-bootstrap/routes-loader.js +13 -0
  148. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  149. package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
  150. package/dist/src/server-bootstrap/service-init.js +34 -0
  151. package/dist/src/server-bootstrap/service-init.js.map +1 -1
  152. package/dist/src/services/config-schema.d.ts.map +1 -1
  153. package/dist/src/services/config-schema.js +2 -0
  154. package/dist/src/services/config-schema.js.map +1 -1
  155. package/dist/src/services/federation-availability.d.ts +28 -0
  156. package/dist/src/services/federation-availability.d.ts.map +1 -0
  157. package/dist/src/services/federation-availability.js +58 -0
  158. package/dist/src/services/federation-availability.js.map +1 -0
  159. package/dist/src/services/federation-book.d.ts +70 -0
  160. package/dist/src/services/federation-book.d.ts.map +1 -0
  161. package/dist/src/services/federation-book.js +198 -0
  162. package/dist/src/services/federation-book.js.map +1 -0
  163. package/dist/src/services/federation-tiers.d.ts +45 -0
  164. package/dist/src/services/federation-tiers.d.ts.map +1 -0
  165. package/dist/src/services/federation-tiers.js +58 -0
  166. package/dist/src/services/federation-tiers.js.map +1 -0
  167. package/dist/src/services/federation.d.ts +15 -0
  168. package/dist/src/services/federation.d.ts.map +1 -1
  169. package/dist/src/services/federation.js +12 -0
  170. package/dist/src/services/federation.js.map +1 -1
  171. package/dist/src/services/librarian.d.ts +62 -0
  172. package/dist/src/services/librarian.d.ts.map +1 -0
  173. package/dist/src/services/librarian.js +135 -0
  174. package/dist/src/services/librarian.js.map +1 -0
  175. package/dist/src/services/network-policy.d.ts +88 -0
  176. package/dist/src/services/network-policy.d.ts.map +1 -0
  177. package/dist/src/services/network-policy.js +154 -0
  178. package/dist/src/services/network-policy.js.map +1 -0
  179. package/dist/src/services/notebook-classify-prompt.d.ts +15 -0
  180. package/dist/src/services/notebook-classify-prompt.d.ts.map +1 -0
  181. package/dist/src/services/notebook-classify-prompt.js +39 -0
  182. package/dist/src/services/notebook-classify-prompt.js.map +1 -0
  183. package/dist/src/services/notebook-classify.d.ts +80 -0
  184. package/dist/src/services/notebook-classify.d.ts.map +1 -0
  185. package/dist/src/services/notebook-classify.js +150 -0
  186. package/dist/src/services/notebook-classify.js.map +1 -0
  187. package/dist/src/services/presence.d.ts +108 -0
  188. package/dist/src/services/presence.d.ts.map +1 -0
  189. package/dist/src/services/presence.js +304 -0
  190. package/dist/src/services/presence.js.map +1 -0
  191. package/dist/src/services/prompt-defaults.d.ts.map +1 -1
  192. package/dist/src/services/prompt-defaults.js +13 -0
  193. package/dist/src/services/prompt-defaults.js.map +1 -1
  194. package/dist/src/services/site.d.ts +20 -0
  195. package/dist/src/services/site.d.ts.map +1 -1
  196. package/dist/src/services/site.js +70 -0
  197. package/dist/src/services/site.js.map +1 -1
  198. package/dist/src/storage/interface.d.ts +18 -0
  199. package/dist/src/storage/interface.d.ts.map +1 -1
  200. package/dist/src/storage/interface.js.map +1 -1
  201. package/dist/src/storage/providers/mongodb/index.d.ts +5 -0
  202. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  203. package/dist/src/storage/providers/mongodb/index.js +65 -2
  204. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  205. package/dist/src/storage/providers/sqlite/index.d.ts +2 -0
  206. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  207. package/dist/src/storage/providers/sqlite/index.js +15 -2
  208. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  209. package/dist/src/storage/providers/sqlite/repos/memory.d.ts +2 -0
  210. package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
  211. package/dist/src/storage/providers/sqlite/repos/memory.js +55 -0
  212. package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
  213. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  214. package/dist/src/storage/providers/sqlite/schema.js +46 -0
  215. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  216. package/dist/src/storage/repositories/memory.repository.d.ts +25 -0
  217. package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
  218. package/dist/src/utils/node-descriptor.d.ts +34 -0
  219. package/dist/src/utils/node-descriptor.d.ts.map +1 -0
  220. package/dist/src/utils/node-descriptor.js +42 -0
  221. package/dist/src/utils/node-descriptor.js.map +1 -0
  222. package/dist/src/utils/version.d.ts +3 -0
  223. package/dist/src/utils/version.d.ts.map +1 -0
  224. package/dist/src/utils/version.js +40 -0
  225. package/dist/src/utils/version.js.map +1 -0
  226. package/dist/static/app-catalog.html +643 -4
  227. package/package.json +5 -2
  228. package/prisma/schema.postgres.prisma +37 -19
  229. package/prisma/schema.prisma +14 -0
@@ -8,13 +8,14 @@ import { num, EconRow, StatsGrid } from './shared.js';
8
8
  import { mintMorsels } from '/js/services/admin.js';
9
9
 
10
10
  export default function EconomyTab({ data, reload }) {
11
- const e = data.dash?.economy;
12
- if (!e) return html`<div class="empty">${t('dashboard.loading')}</div>`;
13
-
11
+ // Hooks must run unconditionally before any early return (Rules of Hooks).
14
12
  const [mintGaii, setMintGaii] = useState('');
15
13
  const [mintAmount, setMintAmount] = useState('');
16
14
  const [mintResult, setMintResult] = useState(null);
17
15
 
16
+ const e = data.dash?.economy;
17
+ if (!e) return html`<div class="empty">${t('dashboard.loading')}</div>`;
18
+
18
19
  async function doMint() {
19
20
  const amount = parseInt(mintAmount, 10);
20
21
  if (!mintGaii || !amount || amount < 1 || amount > 1_000_000) {
@@ -5,6 +5,13 @@
5
5
  * @structure FederationTab (default)
6
6
  * @usage Mounted by the admin dashboard tab router.
7
7
  * @version-history
8
+ * v1.4.0 — 2026-06-19 — Federation book: a Version column + "behind" badge in the live-peers
9
+ * table, and a Federation Book section (operators + resources + version + settings per node,
10
+ * with Rebuild on the primary / Mirror-from-genesis on leaves).
11
+ * v1.3.0 — 2026-06-19 — Phase B: Promote button is eligibility-aware (uses promotion_eligible /
12
+ * promotion_failing from the API; forced override prompts when not yet eligible).
13
+ * v1.2.0 — 2026-06-19 — Visiting-node tier: Tier + Availability badges in the live-peers
14
+ * table, an "open join" toggle, and a Promote (vouch) button for visiting peers.
8
15
  * v1.1.0 — 2026-06-02 — Admin design unification: inline button color style
9
16
  * on the approve action → adm-btn-success.
10
17
  */
@@ -18,7 +25,8 @@ import { dt, num, Badge, EconRow, StatsGrid, Empty, ExpandableHelp, ErrorBox } f
18
25
  import {
19
26
  getFederationPeers, getFederationDirectory, approvePeeringRequest, rejectPeeringRequest,
20
27
  deletePeeringRequest, activatePeer, addPeerDirect, removePeer, removePeerEmergency,
21
- testFederationNode, joinGenesisNetwork, updatePeerPolicy, getNetworkDirectory,
28
+ testFederationNode, joinGenesisNetwork, updatePeerPolicy, promotePeer, getNetworkDirectory,
29
+ getFederationBook, rebuildFederationBook, pullFederationBook,
22
30
  getConfig, saveConfig,
23
31
  } from '/js/services/admin.js';
24
32
  import { useConfirm } from '/components/Modal.js';
@@ -45,6 +53,7 @@ export default function FederationTab({ data, reload }) {
45
53
  const [authPolicy, setAuthPolicy] = useState('disabled');
46
54
  const [defaultScopes, setDefaultScopes] = useState([]);
47
55
  const [authPolicyLoading, setAuthPolicyLoading] = useState(true);
56
+ const [openJoin, setOpenJoin] = useState(false);
48
57
 
49
58
  useEffect(() => {
50
59
  getConfig().then(res => {
@@ -53,11 +62,34 @@ export default function FederationTab({ data, reload }) {
53
62
  setAuthPolicy(s['federation.auth_policy']?.value ?? 'disabled');
54
63
  const scopes = s['federation.default_scopes']?.value ?? 'memory:read,catalogue:read';
55
64
  setDefaultScopes(typeof scopes === 'string' ? scopes.split(',').filter(Boolean) : (scopes || []));
65
+ const oj = s['federation.open_join']?.value;
66
+ setOpenJoin(oj === true || oj === 'true');
56
67
  }
57
68
  setAuthPolicyLoading(false);
58
69
  });
59
70
  }, []);
60
71
 
72
+ const doSaveOpenJoin = async (v) => {
73
+ setOpenJoin(v);
74
+ const res = await saveConfig([{ path: 'federation.open_join', value: v }]);
75
+ if (res.ok) { setActionSuccess(t('dashboard.fedOpenJoinUpdated')); setTimeout(() => setActionSuccess(null), 3000); }
76
+ else setActionError(res.error?.message || 'Failed to save');
77
+ };
78
+
79
+ // ── Federation Book state ──
80
+ const [book, setBook] = useState(null);
81
+ const [bookPrimary, setBookPrimary] = useState(false);
82
+ const loadBook = useCallback(async () => {
83
+ try { const r = await getFederationBook(); setBook(r.data?.book || null); setBookPrimary(!!r.data?.is_primary); }
84
+ catch { setBook(null); }
85
+ }, []);
86
+ useEffect(() => { loadBook(); }, [loadBook]);
87
+ useEffect(() => {
88
+ const h = () => loadBook();
89
+ window.addEventListener('aimeat-live-update', h);
90
+ return () => window.removeEventListener('aimeat-live-update', h);
91
+ }, [loadBook]);
92
+
61
93
  const doSaveAuthPolicy = async (policy, scopes) => {
62
94
  const changes = [
63
95
  { path: 'federation.auth_policy', value: policy },
@@ -176,6 +208,29 @@ export default function FederationTab({ data, reload }) {
176
208
  finally { setTesting(false); }
177
209
  }, [testUrl]);
178
210
 
211
+ const doPromote = useCallback((p) => {
212
+ // Eligible → vouch directly. Not eligible → confirm a forced (audited) override.
213
+ const run = async (force) => {
214
+ try { await promotePeer(p.node_id, force ? { force: true } : {}); flash(t('dashboard.fedPromoted')); reload(); }
215
+ catch (e) {
216
+ if (e.code === 'NOT_ELIGIBLE') {
217
+ confirm((t('dashboard.fedPromoteForceConfirm') || 'Not eligible yet. Promote anyway (vouch)?') + (p.promotion_failing?.length ? '\n' + p.promotion_failing.join(', ') : ''), () => run(true));
218
+ } else { flashErr(e.message); }
219
+ }
220
+ };
221
+ if (p.promotion_eligible) confirm(t('dashboard.fedPromoteConfirm'), () => run(false));
222
+ else run(false); // will 409 → prompts for forced override
223
+ }, [reload]);
224
+
225
+ const doRebuildBook = useCallback(async () => {
226
+ try { await rebuildFederationBook(); flash(t('dashboard.fedBookRebuilt') || 'Book rebuilt'); loadBook(); }
227
+ catch (e) { flashErr(e.message); }
228
+ }, [loadBook]);
229
+ const doPullBook = useCallback(async () => {
230
+ try { const r = await pullFederationBook(); flash(r.data?.applied ? (t('dashboard.fedBookPulled') || 'Book mirrored') : (t('dashboard.fedBookUpToDate') || 'Already up to date')); loadBook(); }
231
+ catch (e) { flashErr(e.message); }
232
+ }, [loadBook]);
233
+
179
234
  const doAddPeer = useCallback(async () => {
180
235
  if (!addNodeId || !addUrl) { flashErr(t('dashboard.fedAddPeerMissing')); return; }
181
236
  try { new URL(addUrl); } catch { flashErr(t('dashboard.fedInvalidUrl') || 'Invalid URL format'); return; }
@@ -203,6 +258,37 @@ export default function FederationTab({ data, reload }) {
203
258
  return html`<${Badge} type=${map[status] || 'watch'} label=${status || 'unknown'} />`;
204
259
  }
205
260
 
261
+ function tierBadge(tier) {
262
+ const tt = tier || 'member';
263
+ const map = { genesis: 'info', member: 'healthy', visiting: 'watch' };
264
+ const label = t(`dashboard.fedTier_${tt}`) || tt;
265
+ return html`<${Badge} type=${map[tt] || 'neutral'} label=${label} />`;
266
+ }
267
+
268
+ // Compare semver-ish version strings; returns 1/0/-1. Non-numeric/"unknown" sort lowest.
269
+ function cmpVersion(a, b) {
270
+ const pa = String(a || '').split('.').map(n => parseInt(n, 10));
271
+ const pb = String(b || '').split('.').map(n => parseInt(n, 10));
272
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
273
+ const x = Number.isFinite(pa[i]) ? pa[i] : -1;
274
+ const y = Number.isFinite(pb[i]) ? pb[i] : -1;
275
+ if (x !== y) return x < y ? -1 : 1;
276
+ }
277
+ return 0;
278
+ }
279
+ // Highest version seen across peers — peers below it are flagged "behind" (a feature-gap hint).
280
+ const maxPeerVersion = livePeers.reduce((m, p) => (p.software_version && cmpVersion(p.software_version, m) > 0 ? p.software_version : m), '');
281
+ function versionCell(v) {
282
+ if (!v) return html`<span class="adm-text-dim">—</span>`;
283
+ const behind = maxPeerVersion && cmpVersion(v, maxPeerVersion) < 0;
284
+ return html`<span>${v}</span>${behind ? html` <${Badge} type="watch" label=${t('dashboard.fedBehind') || 'behind'} />` : null}`;
285
+ }
286
+
287
+ function availabilityBadge(av) {
288
+ if (!av || av === 'unknown') return null;
289
+ return html`<${Badge} type=${av === 'permanent' ? 'healthy' : 'watch'} label=${t(`dashboard.fedAvail_${av}`) || av} />`;
290
+ }
291
+
206
292
  return html`
207
293
  <p class="adm-text-dim adm-mb-md" style="margin:0">${t('dashboard.federationExplain')}</p>
208
294
 
@@ -283,9 +369,53 @@ export default function FederationTab({ data, reload }) {
283
369
  </div>
284
370
  </div>
285
371
  `}
372
+ <div class="adm-mt-md" style="border-top:1px solid var(--border);padding-top:12px">
373
+ <label class="adm-text-sm" style="display:flex;gap:8px;align-items:center">
374
+ <input type="checkbox" checked=${openJoin} onChange=${(e) => doSaveOpenJoin(e.target.checked)} />
375
+ <span><b>${t('dashboard.fedOpenJoinLabel')}</b> — ${t('dashboard.fedOpenJoinDesc')}</span>
376
+ </label>
377
+ </div>
286
378
  `}
287
379
  </div>
288
380
 
381
+ <!-- ═══ Federation Book ═══ -->
382
+ <div class="adm-card adm-mt-lg">
383
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
384
+ <div>
385
+ <h4 class="adm-mb-sm" style="margin:0">${t('dashboard.fedBookTitle')}</h4>
386
+ <p class="adm-text-dim adm-text-base" style="margin:0">${t('dashboard.fedBookDesc')}</p>
387
+ </div>
388
+ ${bookPrimary
389
+ ? html`<button class="adm-btn-sm" onClick=${doRebuildBook}>↻ ${t('dashboard.fedBookRebuild')}</button>`
390
+ : html`<button class="adm-btn-sm" onClick=${doPullBook}>⬇ ${t('dashboard.fedBookPull')}</button>`}
391
+ </div>
392
+ ${!book || !book.nodes?.length
393
+ ? html`<${Empty} text=${t('dashboard.fedBookEmpty')} />`
394
+ : html`<div class="scrollable adm-mt-md"><table>
395
+ <thead><tr>
396
+ <th>${t('dashboard.nodeId')}</th>
397
+ <th>${t('dashboard.fedBookOperators')}</th>
398
+ <th>${t('dashboard.fedVersionLabel')}</th>
399
+ <th>${t('dashboard.fedBookResources')}</th>
400
+ <th>${t('dashboard.fedBookSettings')}</th>
401
+ </tr></thead>
402
+ <tbody>
403
+ ${book.nodes.map(n => html`<tr key=${n.node_id}>
404
+ <td class="mono adm-text-sm">${escHtml(n.node_id)}<div class="adm-text-dim" style="font-size:.7rem">${escHtml(n.node_type || '')}</div></td>
405
+ <td class="adm-text-sm">${(n.operators || []).map(o => html`<div title=${escHtml(o.ghii)}>${o.avatar ? o.avatar + ' ' : ''}${escHtml(o.display_name || o.ghii)}</div>`)}${!(n.operators || []).length ? html`<span class="adm-text-dim">—</span>` : null}</td>
406
+ <td class="adm-text-sm">${versionCell(n.software_version)}</td>
407
+ <td class="adm-text-sm">${n.resources ? html`<span title=${(n.resources.highlights || []).join(', ')}>${n.resources.actions}a · ${n.resources.agents}g · ${n.resources.boards}b · ${n.resources.csms}c</span>` : '—'}</td>
408
+ <td style="display:flex;gap:4px;flex-wrap:wrap">
409
+ ${n.settings?.open_join ? html`<${Badge} type="watch" label=${t('dashboard.fedBookOpenJoin')} />` : null}
410
+ ${n.settings?.auth_policy && n.settings.auth_policy !== 'disabled' ? html`<${Badge} type="info" label=${'auth:' + n.settings.auth_policy} />` : null}
411
+ ${n.settings?.cross_federation ? html`<${Badge} type="neutral" label=${t('dashboard.fedBookCrossFed')} />` : null}
412
+ </td>
413
+ </tr>`)}
414
+ </tbody>
415
+ </table></div>
416
+ <p class="adm-text-dim" style="font-size:.7rem;margin-top:6px">${t('dashboard.fedBookVersion')}: ${book.book_version} · ${escHtml(book.issued_by || '')}</p>`}
417
+ </div>
418
+
289
419
  <!-- ═══ Network Directory ═══ -->
290
420
  <div class="adm-card adm-mt-lg">
291
421
  <h4 class="adm-mb-sm" style="margin:0">${t('dashboard.fedNetworkDirectoryTitle')}</h4>
@@ -364,6 +494,8 @@ export default function FederationTab({ data, reload }) {
364
494
  <th>${t('dashboard.nodeId')}</th>
365
495
  <th>URL</th>
366
496
  <th>${t('dashboard.statusLabel')}</th>
497
+ <th>${t('dashboard.fedTierLabel')}</th>
498
+ <th>${t('dashboard.fedVersionLabel')}</th>
367
499
  <th>${t('dashboard.fedAddedAt')}</th>
368
500
  <th>${t('dashboard.lastSeen')}</th>
369
501
  <th>${t('dashboard.fedPublicKey')}</th>
@@ -375,6 +507,8 @@ export default function FederationTab({ data, reload }) {
375
507
  <td class="mono adm-text-sm">${escHtml(p.node_id)}</td>
376
508
  <td class="mono adm-text-sm">${escHtml(p.url || '\u2014')}</td>
377
509
  <td>${statusBadge(p.status)}</td>
510
+ <td style="display:flex;gap:4px;flex-wrap:wrap;align-items:center">${tierBadge(p.tier)}${availabilityBadge(p.availability)}</td>
511
+ <td class="adm-text-sm">${versionCell(p.software_version)}</td>
378
512
  <td class="adm-text-dim">${dt(p.added_at)}</td>
379
513
  <td class="adm-text-dim">${dt(p.last_seen)}</td>
380
514
  <td class="mono" style="font-size:.7rem;max-width:100px;overflow:hidden;text-overflow:ellipsis" title=${p.public_key || ''}>${p.public_key ? p.public_key.substring(0, 16) + '...' : '\u2014'}</td>
@@ -392,6 +526,9 @@ export default function FederationTab({ data, reload }) {
392
526
  ${p.status === 'approved' && html`
393
527
  <button class="adm-btn-sm" onClick=${() => doActivate(p.node_id)}>\u25B6 ${t('dashboard.fedActivate')}</button>
394
528
  `}
529
+ ${p.tier === 'visiting' && html`
530
+ <button class="adm-btn-sm" title=${p.promotion_eligible ? t('dashboard.fedPromoteHint') : ((t('dashboard.fedPromoteNotEligible') || 'Not yet eligible') + (p.promotion_failing?.length ? ': ' + p.promotion_failing.join(', ') : ''))} onClick=${() => doPromote(p)}>${p.promotion_eligible ? '\u2B06' : '\u26A0'} ${t('dashboard.fedPromote')}</button>
531
+ `}
395
532
  ${(p.status === 'active' || p.status === 'degraded') && html`
396
533
  <button class="adm-btn-sm" onClick=${() => doRemove(p.node_id)}>${t('dashboard.fedDepeer')}</button>
397
534
  `}
@@ -45,7 +45,7 @@ export default function MaintenanceTab({ data, reload }) {
45
45
  const reader = new FileReader();
46
46
  reader.onload = async () => {
47
47
  try {
48
- const parsed = JSON.parse(reader.result);
48
+ const parsed = JSON.parse(/** @type {string} */ (reader.result));
49
49
  await apiRestore(parsed);
50
50
  setBackupResult({ ok: true, msg: t('dashboard.dataRestored') });
51
51
  reload();
@@ -8,9 +8,7 @@ import { num, fmtUp, Badge, StatsGrid, EconRow, HealthRow } from './shared.js';
8
8
  import { apiGet } from '/js/api.js';
9
9
 
10
10
  export default function OverviewTab({ data }) {
11
- const d = data.dash;
12
- if (!d) return html`<div class="empty">${t('dashboard.loading')}</div>`;
13
-
11
+ // Hooks must run unconditionally before any early return (Rules of Hooks).
14
12
  const [activeTaskCount, setActiveTaskCount] = useState(null);
15
13
  const [sharingGroupCount, setSharingGroupCount] = useState(null);
16
14
 
@@ -32,6 +30,9 @@ export default function OverviewTab({ data }) {
32
30
  .catch(() => setSharingGroupCount(0));
33
31
  }, []);
34
32
 
33
+ const d = data.dash;
34
+ if (!d) return html`<div class="empty">${t('dashboard.loading')}</div>`;
35
+
35
36
  const h_ = d.health;
36
37
  const c = d.counts;
37
38
  const e = d.economy;
@@ -13,6 +13,8 @@
13
13
  * (now /v1/site/memory → __site__ so {{memory:portal/*}} resolves); add AI
14
14
  * bundle import (/v1/site/import) so the AI-Assisted result no longer 422s in
15
15
  * the template box; reload the preview iframe after every change + Clear Cache.
16
+ * v1.4.0 — 2026-06-19 — Add Header Navigation section: operator show/hide + reorder
17
+ * of the public header links (persisted via /v1/site/header-nav).
16
18
  */
17
19
  import { h } from 'preact';
18
20
  import { useState, useEffect } from 'preact/hooks';
@@ -24,10 +26,20 @@ import { dt, Badge, ExpandableHelp, useToast, Toast } from './shared.js';
24
26
  import {
25
27
  saveSiteTemplate, deleteSiteTemplate, clearSiteCache,
26
28
  getSiteMemoryKeys, getSitePrompt, setSiteMemory, deleteSiteMemory,
27
- importSiteBundle, triggerLbSync,
29
+ importSiteBundle, triggerLbSync, getHeaderNav, saveHeaderNav,
28
30
  } from '/js/services/admin.js';
29
31
  import { useConfirm } from '/components/Modal.js';
30
32
 
33
+ // Public header link ids → their nav i18n label key (mirror of PUBLIC_NAV_LINKS in spa.html
34
+ // and PUBLIC_NAV_LINK_IDS in src/services/site.ts). Gated links are not configurable.
35
+ const HEADER_LINK_LABELS = {
36
+ try: 'nav.try',
37
+ howItWorks: 'nav.howItWorks',
38
+ business: 'nav.business',
39
+ devView: 'nav.devView',
40
+ help: 'nav.help',
41
+ };
42
+
31
43
  export default function PortalTab({ data, reload }) {
32
44
  const [toast, showErr, showOk, clearToast] = useToast();
33
45
  const p = data.portal || {};
@@ -43,11 +55,14 @@ export default function PortalTab({ data, reload }) {
43
55
  const [newKey, setNewKey] = useState('');
44
56
  const [newVal, setNewVal] = useState('');
45
57
  const [aiBundle, setAiBundle] = useState('');
58
+ // Header nav: ordered list of { id, visible }. null while loading.
59
+ const [navLinks, setNavLinks] = useState(null);
60
+ const [navSaving, setNavSaving] = useState(false);
46
61
  // Bumped after any change so the preview iframe re-fetches `/` (busts browser cache).
47
62
  const [previewNonce, setPreviewNonce] = useState(0);
48
63
  const bumpPreview = () => setPreviewNonce(n => n + 1);
49
64
 
50
- useEffect(() => { loadMemKeys(); }, []);
65
+ useEffect(() => { loadMemKeys(); loadNav(); }, []);
51
66
 
52
67
  async function loadMemKeys() {
53
68
  try {
@@ -56,6 +71,44 @@ export default function PortalTab({ data, reload }) {
56
71
  } catch { setMemKeys([]); }
57
72
  }
58
73
 
74
+ async function loadNav() {
75
+ try {
76
+ const res = await getHeaderNav();
77
+ const order = res.data?.order || Object.keys(HEADER_LINK_LABELS);
78
+ const hidden = new Set(res.data?.hidden || []);
79
+ setNavLinks(order
80
+ .filter(id => HEADER_LINK_LABELS[id])
81
+ .map(id => ({ id, visible: !hidden.has(id) })));
82
+ } catch { setNavLinks([]); }
83
+ }
84
+
85
+ function toggleNav(id) {
86
+ setNavLinks(links => links.map(l => l.id === id ? { ...l, visible: !l.visible } : l));
87
+ }
88
+
89
+ function moveNav(idx, dir) {
90
+ setNavLinks(links => {
91
+ const next = links.slice();
92
+ const j = idx + dir;
93
+ if (j < 0 || j >= next.length) return next;
94
+ [next[idx], next[j]] = [next[j], next[idx]];
95
+ return next;
96
+ });
97
+ }
98
+
99
+ async function saveNav() {
100
+ if (!navLinks) return;
101
+ setNavSaving(true);
102
+ try {
103
+ const order = navLinks.map(l => l.id);
104
+ const hidden = navLinks.filter(l => !l.visible).map(l => l.id);
105
+ await saveHeaderNav(order, hidden);
106
+ showOk(t('dashboard.portalNavSaved'));
107
+ bumpPreview();
108
+ } catch (e) { showErr(e.message); }
109
+ finally { setNavSaving(false); }
110
+ }
111
+
59
112
  async function saveTemplate() {
60
113
  // The AI-Assisted prompt produces a JSON import bundle, not raw HTML. If the
61
114
  // user pasted that here, the template route would 422 — point them to Import.
@@ -188,6 +241,41 @@ export default function PortalTab({ data, reload }) {
188
241
  </div>
189
242
  </div>
190
243
 
244
+ <!-- Header navigation -->
245
+ <div class="adm-card">
246
+ <h3>${t('dashboard.portalNavTitle')}</h3>
247
+ <p class="adm-text-dim adm-text-base adm-mb-sm">${t('dashboard.portalNavExplain')}</p>
248
+ ${navLinks === null
249
+ ? html`<div class="adm-text-dim">${t('dashboard.loading')}...</div>`
250
+ : html`
251
+ <table>
252
+ <thead><tr>
253
+ <th>${t('dashboard.portalNavLink')}</th>
254
+ <th>${t('dashboard.portalNavVisible')}</th>
255
+ <th>${t('dashboard.portalNavOrder')}</th>
256
+ </tr></thead>
257
+ <tbody>
258
+ ${navLinks.map((l, idx) => html`<tr key=${l.id}>
259
+ <td>${t(HEADER_LINK_LABELS[l.id]) || l.id}</td>
260
+ <td>
261
+ <label class="adm-flex-center">
262
+ <input type="checkbox" checked=${l.visible} onChange=${() => toggleNav(l.id)} />
263
+ <span class="adm-text-dim">${l.visible ? t('dashboard.portalNavShown') : t('dashboard.portalNavHidden')}</span>
264
+ </label>
265
+ </td>
266
+ <td>
267
+ <button class="adm-btn-sm" disabled=${idx === 0} onClick=${() => moveNav(idx, -1)} title=${t('dashboard.portalNavMoveUp')}>↑</button>
268
+ <button class="adm-btn-sm" disabled=${idx === navLinks.length - 1} onClick=${() => moveNav(idx, 1)} title=${t('dashboard.portalNavMoveDown')}>↓</button>
269
+ </td>
270
+ </tr>`)}
271
+ </tbody>
272
+ </table>
273
+ <div class="adm-flex adm-mt-sm">
274
+ <button class="adm-btn-action" disabled=${navSaving} onClick=${saveNav}>\u{1F4BE} ${t('dashboard.portalNavSave')}</button>
275
+ </div>
276
+ `}
277
+ </div>
278
+
191
279
  <!-- Template editor -->
192
280
  <div class="adm-card">
193
281
  <h3>${t('dashboard.portalTemplate')}</h3>
@@ -21,13 +21,9 @@ import { useConfirm } from '/components/Modal.js';
21
21
 
22
22
  export default function PushTab({ data, reload }) {
23
23
  const push = data.push;
24
- if (!push) return html`<${Empty} text=${t('dashboard.pushNotConfigured')} />`;
25
-
26
- const subs = push.subscriptions || [];
27
- const templates = push.templates || [];
28
- const locales = push.locales || ['en'];
29
24
 
30
- const [tplLocale, setTplLocale] = useState(locales[0] || 'en');
25
+ // Hooks must run unconditionally before any early return (Rules of Hooks).
26
+ const [tplLocale, setTplLocale] = useState((push?.locales || ['en'])[0] || 'en');
31
27
  const [saving, setSaving] = useState(null);
32
28
  const [testStatus, setTestStatus] = useState(null);
33
29
  const [resetStatus, setResetStatus] = useState(null);
@@ -36,6 +32,12 @@ export default function PushTab({ data, reload }) {
36
32
  const { confirm, ConfirmUI } = useConfirm();
37
33
  const [toast, showErr, showOk, clearToast] = useToast();
38
34
 
35
+ if (!push) return html`<${Empty} text=${t('dashboard.pushNotConfigured')} />`;
36
+
37
+ const subs = push.subscriptions || [];
38
+ const templates = push.templates || [];
39
+ const locales = push.locales || ['en'];
40
+
39
41
  const localeTpls = templates.filter(tpl => tpl.locale === tplLocale);
40
42
 
41
43
  const handleSave = async (tpl) => {
@@ -19,22 +19,22 @@ const html = htm.bind(h);
19
19
 
20
20
  export default function SecurityTab() {
21
21
  const [state, setState] = useState({ incidents: [], open: 0, total: 0, loading: true });
22
- const { toast, showToast } = useToast();
22
+ const [toast, showErr, showOk, clearToast] = useToast();
23
23
  const { confirm, ConfirmUI } = useConfirm();
24
24
 
25
25
  const load = async () => {
26
26
  try { const r = await getSecurityIncidents(); setState({ incidents: r?.data?.incidents || [], open: r?.data?.open || 0, total: r?.data?.total || 0, loading: false }); }
27
- catch (e) { setState({ incidents: [], open: 0, total: 0, loading: false }); showToast((e && e.message) || 'Failed to load incidents'); }
27
+ catch (e) { setState({ incidents: [], open: 0, total: 0, loading: false }); showErr((e && e.message) || 'Failed to load incidents'); }
28
28
  };
29
29
  useEffect(() => { load(); }, []);
30
30
 
31
31
  const resolve = async (id) => {
32
- try { await resolveSecurityIncident(id); showToast(t('admin.security.resolved') || 'Incident resolved'); load(); }
33
- catch (e) { showToast((e && e.message) || 'Failed'); }
32
+ try { await resolveSecurityIncident(id); showOk(t('admin.security.resolved') || 'Incident resolved'); load(); }
33
+ catch (e) { showErr((e && e.message) || 'Failed'); }
34
34
  };
35
35
  const remove = (id) => confirm(
36
36
  t('admin.security.deleteConfirm') || 'Delete this incident and its quarantined file?',
37
- async () => { try { await deleteSecurityIncident(id); showToast(t('admin.security.deleted') || 'Deleted'); load(); } catch (e) { showToast((e && e.message) || 'Failed'); } },
37
+ async () => { try { await deleteSecurityIncident(id); showOk(t('admin.security.deleted') || 'Deleted'); load(); } catch (e) { showErr((e && e.message) || 'Failed'); } },
38
38
  { danger: true, title: t('admin.security.title') || 'Security' },
39
39
  );
40
40
  const downloadQuarantine = async (id) => {
@@ -46,12 +46,12 @@ export default function SecurityTab() {
46
46
  const url = URL.createObjectURL(blob);
47
47
  const a = document.createElement('a'); a.href = url; a.download = `quarantine-${id}.zip`;
48
48
  document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
49
- } catch (e) { showToast((e && e.message) || 'No quarantined file'); }
49
+ } catch (e) { showErr((e && e.message) || 'No quarantined file'); }
50
50
  };
51
51
 
52
52
  return html`
53
53
  <div class="adm-section">
54
- <${ConfirmUI} /><${Toast} toast=${toast} />
54
+ <${ConfirmUI} />${toast && html`<${Toast} ...${toast} onDismiss=${clearToast} />`}
55
55
  <div class="adm-card">
56
56
  <h2 class="adm-sec-title">${'🛡️ '}${t('admin.security.title') || 'Security'}</h2>
57
57
  <p class="adm-sec-desc">
@@ -5,6 +5,7 @@
5
5
  * live-updates trigger a debounced, silent background refresh.
6
6
  * @version-history
7
7
  * v1.1.0 — 2026-06-18 — Debounce + silence SSE-driven refresh so busy nodes don't flicker "Loading…".
8
+ * v1.1.1 — 2026-06-19 — JSDoc type annotations for frontend type-checking
8
9
  */
9
10
  import { h } from 'preact';
10
11
  import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
@@ -341,7 +342,7 @@ export default function Admin({ navigate, locale }) {
341
342
  function switchPage(id) {
342
343
  setActivePage(id);
343
344
  // Update URL without reload
344
- const url = new URL(window.location);
345
+ const url = new URL(window.location.href);
345
346
  url.searchParams.set('tab', id);
346
347
  history.replaceState(null, '', url);
347
348
  }
@@ -9,6 +9,8 @@
9
9
  * @structure DocSolo (default export)
10
10
  * @version-history
11
11
  * v1.0.0 — 2026-06-09 — Initial: pop-out document window.
12
+ * v1.0.1 — 2026-06-19 — Import DocumentView/DocumentEditor from their new home
13
+ * (./profile/organisms/document.js) after the organisms-tab module split.
12
14
  */
13
15
  import { h } from 'preact';
14
16
  import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
@@ -18,7 +20,7 @@ import { getSession } from '/js/services/auth.js';
18
20
  import { connect, disconnect, onUpdate, offUpdate } from '/lib/live-updates.js';
19
21
  import { useViewCSS } from '/components/useViewCSS.js';
20
22
  import * as orgService from '/js/services/organisms.js';
21
- import { DocumentView, DocumentEditor } from './profile/organisms-tab.js';
23
+ import { DocumentView, DocumentEditor } from './profile/organisms/document.js';
22
24
 
23
25
  const html = htm.bind(h);
24
26
 
@@ -300,11 +300,12 @@ function GuideIndex({ locale, onSelectGuide, navigate }) {
300
300
  function GuideDetail({ slug, locale, onBack }) {
301
301
  const labels = LABELS[locale] || LABELS.en;
302
302
  const guide = GUIDES[slug];
303
- if (!guide) return html`<div class="gd-container"><p>Guide not found.</p></div>`;
304
- const data = guide[locale] || guide.en;
303
+ const data = guide ? (guide[locale] || guide.en) : null;
305
304
 
306
- // Build markdown for clipboard
305
+ // Hooks must run unconditionally before any early return (Rules of Hooks) — guard their
306
+ // bodies against a missing guide instead.
307
307
  const buildMarkdown = useCallback(() => {
308
+ if (!data) return '';
308
309
  let md = '# ' + data.title + '\n\nNode URL: ' + NODE_URL + '\n\nIMPORTANT: Make it a single downloadable HTML file.\n\n';
309
310
  data.sections.forEach(s => {
310
311
  md += '## ' + s.heading + '\n\n' + s.body + '\n\n';
@@ -316,9 +317,12 @@ function GuideDetail({ slug, locale, onBack }) {
316
317
  }, [data]);
317
318
 
318
319
  useEffect(() => {
320
+ if (!data) return;
319
321
  document.title = data.title + ' | AIME AT';
320
322
  window.scrollTo(0, 0);
321
- }, [slug, data.title]);
323
+ }, [slug, data]);
324
+
325
+ if (!guide) return html`<div class="gd-container"><p>Guide not found.</p></div>`;
322
326
 
323
327
  return html`
324
328
  <div class="gd-container">
@@ -2,6 +2,7 @@
2
2
  * @file hobbies.js
3
3
  * @description Hobbies view module — multi-view hobby directory (home, search, profile, join, me, matches) rendered with Preact + HTM.
4
4
  * @version-history
5
+ * v1.3.0 — 2026-06-19 — Show a presence dot next to people in the directory results + profile header (shared <PresenceDot>)
5
6
  * v1.1.0 — 2026-06-02 — Phase 5: tokenize for dark mode (replaced light-only rgba(255,255,255,...) divider with var(--border))
6
7
  * v1.2.0 — 2026-06-02 — Component unification (#4 buttons): hb-btn* → canonical
7
8
  * theme.css btn-primary / btn-outline / btn-danger / btn-sm.
@@ -15,6 +16,7 @@ import { escHtml, escAttr } from '/js/utils.js';
15
16
  import { t, getLocale, switchLocale } from '/js/i18n.js';
16
17
  import { apiGet, apiPost, apiPut, apiDelete } from '/js/api.js';
17
18
  import { useViewCSS } from '/components/useViewCSS.js';
19
+ import { PresenceDot } from '/components/PresenceDot.js';
18
20
 
19
21
  const html = htm.bind(h);
20
22
 
@@ -207,7 +209,7 @@ function SearchView({ goTo, initData }) {
207
209
  <div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
208
210
  <span style="font-size:1.5rem;">${entry.avatar || '\uD83D\uDE0A'}</span>
209
211
  <div>
210
- <div class="hb-card-title">${entry.displayName}</div>
212
+ <div class="hb-card-title">${entry.displayName} <${PresenceDot} ghii=${entry.ghii} /></div>
211
213
  ${(entry.city || entry.area) && html`<div class="hb-card-meta">${[entry.city, entry.area].filter(Boolean).join(', ')}</div>`}
212
214
  </div>
213
215
  </div>
@@ -282,7 +284,7 @@ function ProfileView({ goTo, ghii }) {
282
284
  <div class="hb-profile-header">
283
285
  <span class="hb-profile-avatar">${entry.avatar || '\uD83D\uDE0A'}</span>
284
286
  <div>
285
- <div class="hb-profile-name">${entry.displayName}</div>
287
+ <div class="hb-profile-name">${entry.displayName} <${PresenceDot} ghii=${ghii} label=${true} /></div>
286
288
  <div class="hb-profile-ghii">${ghii}</div>
287
289
  </div>
288
290
  </div>
@@ -5,6 +5,7 @@
5
5
  * @version-history
6
6
  * v1.1.0 — 2026-06-02 — Component unification (#18 category cards): category tiles
7
7
  * use canonical theme.css .category-card / -icon / -name / -count (was .mk-category-*).
8
+ * v1.1.1 — 2026-06-19 — JSDoc type annotations for frontend type-checking
8
9
  */
9
10
  import { h } from 'preact';
10
11
  import { useState, useEffect, useCallback } from 'preact/hooks';
@@ -379,19 +380,19 @@ function CreateListingView({ extAction, instanceConfig, onNav, tl }) {
379
380
  const submit = () => {
380
381
  setMsg(''); setErr(''); setSending(true);
381
382
  const data = {
382
- title: document.getElementById('mk-sell-title').value,
383
- description: document.getElementById('mk-sell-desc').value,
384
- category: document.getElementById('mk-sell-category').value,
385
- price: parseInt(document.getElementById('mk-sell-price').value, 10),
383
+ title: /** @type {HTMLInputElement} */ (document.getElementById('mk-sell-title')).value,
384
+ description: /** @type {HTMLInputElement} */ (document.getElementById('mk-sell-desc')).value,
385
+ category: /** @type {HTMLInputElement} */ (document.getElementById('mk-sell-category')).value,
386
+ price: parseInt(/** @type {HTMLInputElement} */ (document.getElementById('mk-sell-price')).value, 10),
386
387
  };
387
- const condition = document.getElementById('mk-sell-condition').value;
388
+ const condition = /** @type {HTMLInputElement} */ (document.getElementById('mk-sell-condition')).value;
388
389
  if (condition) data.condition = condition;
389
- const availability = document.getElementById('mk-sell-avail').value;
390
+ const availability = /** @type {HTMLInputElement} */ (document.getElementById('mk-sell-avail')).value;
390
391
  if (availability) data.availability = availability;
391
- const city = document.getElementById('mk-sell-city').value;
392
- const area = document.getElementById('mk-sell-area').value;
392
+ const city = /** @type {HTMLInputElement} */ (document.getElementById('mk-sell-city')).value;
393
+ const area = /** @type {HTMLInputElement} */ (document.getElementById('mk-sell-area')).value;
393
394
  if (city || area) { data.location = {}; if (city) data.location.city = city; if (area) data.location.area = area; }
394
- const tags = document.getElementById('mk-sell-tags').value;
395
+ const tags = /** @type {HTMLInputElement} */ (document.getElementById('mk-sell-tags')).value;
395
396
  if (tags) data.tags = tags.split(',').map(s => s.trim()).filter(Boolean);
396
397
 
397
398
  extAction('create-listing', data)
@@ -176,7 +176,7 @@ function OpenClawView() {
176
176
  ].map(([title, steps]) => html`
177
177
  <div class="oc-scenario">
178
178
  <h4>${title}</h4>
179
- <div class="flow"><ol>${steps.map(s => html`<li>${s}</li>`)}</ol></div>
179
+ <div class="flow"><ol>${(/** @type {any[]} */ (steps)).map(s => html`<li>${s}</li>`)}</ol></div>
180
180
  </div>
181
181
  `)}
182
182
  </div>