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.
- package/README.md +8 -1
- package/dist/locales/en.json +130 -0
- package/dist/locales/fi.json +130 -0
- package/dist/public/components/ImageDeliverable.js +2 -1
- package/dist/public/components/Modal.js +3 -2
- package/dist/public/components/NotificationBell.js +2 -1
- package/dist/public/components/PresenceDot.js +53 -0
- package/dist/public/css/components/markdown.css +67 -0
- package/dist/public/css/theme.css +30 -0
- package/dist/public/css/views/notebook.css +195 -0
- package/dist/public/css/views/profile.css +15 -0
- package/dist/public/js/api.js +5 -3
- package/dist/public/js/components/auth-image.js +3 -2
- package/dist/public/js/components/tag-cloud.js +1 -1
- package/dist/public/js/components/tag-editor.js +1 -1
- package/dist/public/js/hooks.js +1 -2
- package/dist/public/js/presence-store.js +97 -0
- package/dist/public/js/services/admin.js +6 -0
- package/dist/public/js/services/boards.js +1 -1
- package/dist/public/js/services/ecosystem.js +1 -1
- package/dist/public/js/services/foundry-prompts-base.js +2 -2
- package/dist/public/js/services/foundry-smoke.js +2 -1
- package/dist/public/js/services/foundry-validate.js +6 -8
- package/dist/public/js/services/foundry.js +4 -3
- package/dist/public/js/services/generator-prompts-base.js +2 -2
- package/dist/public/js/services/generator-smoke.js +2 -1
- package/dist/public/js/services/generator-validate.js +5 -7
- package/dist/public/js/services/generator.js +2 -1
- package/dist/public/js/services/memory.js +13 -0
- package/dist/public/js/services/messages.js +2 -1
- package/dist/public/js/services/notebook.js +124 -0
- package/dist/public/js/services/packages.js +3 -2
- package/dist/public/js/services/presence.js +35 -0
- package/dist/public/js/style-testbed.js +6 -5
- package/dist/public/lib/minidenticons.min.js +1 -0
- package/dist/public/lib/yaml.mjs +1 -0
- package/dist/public/spa.html +68 -8
- package/dist/public/types/aimeat-globals.d.ts +52 -0
- package/dist/public/types/vendored-libs.d.ts +56 -0
- package/dist/public/views/_template.js +3 -1
- package/dist/public/views/admin/agent-integration-tab.js +4 -3
- package/dist/public/views/admin/agents-tab.js +3 -2
- package/dist/public/views/admin/config-tab.js +4 -3
- package/dist/public/views/admin/economy-tab.js +4 -3
- package/dist/public/views/admin/federation-tab.js +138 -1
- package/dist/public/views/admin/maintenance-tab.js +1 -1
- package/dist/public/views/admin/overview-tab.js +4 -3
- package/dist/public/views/admin/portal-tab.js +90 -2
- package/dist/public/views/admin/push-tab.js +8 -6
- package/dist/public/views/admin/security-tab.js +7 -7
- package/dist/public/views/admin.js +2 -1
- package/dist/public/views/doc-solo.js +3 -1
- package/dist/public/views/guides.js +8 -4
- package/dist/public/views/hobbies.js +4 -2
- package/dist/public/views/marketplace.js +10 -9
- package/dist/public/views/openclaw.js +1 -1
- package/dist/public/views/portal-dev.js +5 -2
- package/dist/public/views/portal.js +3 -2
- package/dist/public/views/profile/agents/shared-board.js +4 -2
- package/dist/public/views/profile/agents/tab-messages.js +13 -11
- package/dist/public/views/profile/agents-messages-subtab.js +2 -1
- package/dist/public/views/profile/apps-tab.js +2 -1
- package/dist/public/views/profile/boards-tab.js +3 -2
- package/dist/public/views/profile/calibrator-batch.js +3 -2
- package/dist/public/views/profile/data-wallet-tab.js +2 -1
- package/dist/public/views/profile/extensions-tab.js +4 -3
- package/dist/public/views/profile/federation-tab.js +4 -0
- package/dist/public/views/profile/foundry-dashboard/use-autopilot.js +3 -2
- package/dist/public/views/profile/foundry-detail.js +1 -1
- package/dist/public/views/profile/generator-detail.js +1 -1
- package/dist/public/views/profile/inbox-tab.js +7 -3
- package/dist/public/views/profile/inline-panels.js +3 -1
- package/dist/public/views/profile/knowledge-tab.js +2 -1
- package/dist/public/views/profile/landing-page.js +88 -0
- package/dist/public/views/profile/memory-tab.js +6 -5
- package/dist/public/views/profile/notebook-tab.js +500 -0
- package/dist/public/views/profile/organisms/activity-panel.js +135 -0
- package/dist/public/views/profile/organisms/agents.js +141 -0
- package/dist/public/views/profile/organisms/document.js +274 -0
- package/dist/public/views/profile/organisms/helpers.js +61 -0
- package/dist/public/views/profile/organisms/home.js +315 -0
- package/dist/public/views/profile/organisms/members.js +220 -0
- package/dist/public/views/profile/organisms/panels.js +173 -0
- package/dist/public/views/profile/organisms/participants-panel.js +160 -0
- package/dist/public/views/profile/organisms/schema-form.js +127 -0
- package/dist/public/views/profile/organisms/sources-panel.js +164 -0
- package/dist/public/views/profile/organisms/widgets.js +58 -0
- package/dist/public/views/profile/organisms/workspace-comments.js +99 -0
- package/dist/public/views/profile/organisms/workspace-list.js +268 -0
- package/dist/public/views/profile/organisms/workspace.js +1272 -0
- package/dist/public/views/profile/organisms-tab.js +456 -3819
- package/dist/public/views/profile/shared.js +64 -2
- package/dist/public/views/profile/workflows-form.js +1 -1
- package/dist/public/views/profile.js +16 -14
- package/dist/public/views/public-knowledge-viewer.js +3 -2
- package/dist/public/views/public-workspace-viewer.js +2 -1
- package/dist/scripts/check-importmap.d.ts +2 -0
- package/dist/scripts/check-importmap.d.ts.map +1 -0
- package/dist/scripts/check-importmap.js +102 -0
- package/dist/scripts/check-importmap.js.map +1 -0
- package/dist/src/config.d.ts +10 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +5 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +852 -11
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +18 -5
- package/dist/src/generated/prisma-postgres/index-browser.js +15 -2
- package/dist/src/generated/prisma-postgres/index.d.ts +422 -7
- package/dist/src/generated/prisma-postgres/index.js +18 -5
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +30 -12
- package/dist/src/generated/prisma-postgres/wasm.js +18 -5
- package/dist/src/routes/federation-peer.d.ts.map +1 -1
- package/dist/src/routes/federation-peer.js +327 -43
- package/dist/src/routes/federation-peer.js.map +1 -1
- package/dist/src/routes/federation-sync.d.ts +2 -0
- package/dist/src/routes/federation-sync.d.ts.map +1 -1
- package/dist/src/routes/federation-sync.js +20 -1
- package/dist/src/routes/federation-sync.js.map +1 -1
- package/dist/src/routes/librarian.d.ts +18 -0
- package/dist/src/routes/librarian.d.ts.map +1 -0
- package/dist/src/routes/librarian.js +88 -0
- package/dist/src/routes/librarian.js.map +1 -0
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +9 -1
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/presence.d.ts +23 -0
- package/dist/src/routes/presence.d.ts.map +1 -0
- package/dist/src/routes/presence.js +88 -0
- package/dist/src/routes/presence.js.map +1 -0
- package/dist/src/routes/site.d.ts +2 -0
- package/dist/src/routes/site.d.ts.map +1 -1
- package/dist/src/routes/site.js +27 -0
- package/dist/src/routes/site.js.map +1 -1
- package/dist/src/routes/sse.d.ts +2 -0
- package/dist/src/routes/sse.d.ts.map +1 -1
- package/dist/src/routes/sse.js +9 -0
- package/dist/src/routes/sse.js.map +1 -1
- package/dist/src/routes/wellknown.d.ts.map +1 -1
- package/dist/src/routes/wellknown.js +5 -0
- package/dist/src/routes/wellknown.js.map +1 -1
- package/dist/src/routes/work.d.ts.map +1 -1
- package/dist/src/routes/work.js +6 -0
- package/dist/src/routes/work.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +13 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +34 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +2 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/federation-availability.d.ts +28 -0
- package/dist/src/services/federation-availability.d.ts.map +1 -0
- package/dist/src/services/federation-availability.js +58 -0
- package/dist/src/services/federation-availability.js.map +1 -0
- package/dist/src/services/federation-book.d.ts +70 -0
- package/dist/src/services/federation-book.d.ts.map +1 -0
- package/dist/src/services/federation-book.js +198 -0
- package/dist/src/services/federation-book.js.map +1 -0
- package/dist/src/services/federation-tiers.d.ts +45 -0
- package/dist/src/services/federation-tiers.d.ts.map +1 -0
- package/dist/src/services/federation-tiers.js +58 -0
- package/dist/src/services/federation-tiers.js.map +1 -0
- package/dist/src/services/federation.d.ts +15 -0
- package/dist/src/services/federation.d.ts.map +1 -1
- package/dist/src/services/federation.js +12 -0
- package/dist/src/services/federation.js.map +1 -1
- package/dist/src/services/librarian.d.ts +62 -0
- package/dist/src/services/librarian.d.ts.map +1 -0
- package/dist/src/services/librarian.js +135 -0
- package/dist/src/services/librarian.js.map +1 -0
- package/dist/src/services/network-policy.d.ts +88 -0
- package/dist/src/services/network-policy.d.ts.map +1 -0
- package/dist/src/services/network-policy.js +154 -0
- package/dist/src/services/network-policy.js.map +1 -0
- package/dist/src/services/notebook-classify-prompt.d.ts +15 -0
- package/dist/src/services/notebook-classify-prompt.d.ts.map +1 -0
- package/dist/src/services/notebook-classify-prompt.js +39 -0
- package/dist/src/services/notebook-classify-prompt.js.map +1 -0
- package/dist/src/services/notebook-classify.d.ts +80 -0
- package/dist/src/services/notebook-classify.d.ts.map +1 -0
- package/dist/src/services/notebook-classify.js +150 -0
- package/dist/src/services/notebook-classify.js.map +1 -0
- package/dist/src/services/presence.d.ts +108 -0
- package/dist/src/services/presence.d.ts.map +1 -0
- package/dist/src/services/presence.js +304 -0
- package/dist/src/services/presence.js.map +1 -0
- package/dist/src/services/prompt-defaults.d.ts.map +1 -1
- package/dist/src/services/prompt-defaults.js +13 -0
- package/dist/src/services/prompt-defaults.js.map +1 -1
- package/dist/src/services/site.d.ts +20 -0
- package/dist/src/services/site.d.ts.map +1 -1
- package/dist/src/services/site.js +70 -0
- package/dist/src/services/site.js.map +1 -1
- package/dist/src/storage/interface.d.ts +18 -0
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/interface.js.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +5 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +65 -2
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +2 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +15 -2
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts +2 -0
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.js +55 -0
- package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +46 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/memory.repository.d.ts +25 -0
- package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
- package/dist/src/utils/node-descriptor.d.ts +34 -0
- package/dist/src/utils/node-descriptor.d.ts.map +1 -0
- package/dist/src/utils/node-descriptor.js +42 -0
- package/dist/src/utils/node-descriptor.js.map +1 -0
- package/dist/src/utils/version.d.ts +3 -0
- package/dist/src/utils/version.d.ts.map +1 -0
- package/dist/src/utils/version.js +40 -0
- package/dist/src/utils/version.js.map +1 -0
- package/dist/static/app-catalog.html +643 -4
- package/package.json +5 -2
- package/prisma/schema.postgres.prisma +37 -19
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 });
|
|
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);
|
|
33
|
-
catch (e) {
|
|
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);
|
|
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) {
|
|
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}
|
|
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
|
|
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
|
-
|
|
304
|
-
const data = guide[locale] || guide.en;
|
|
303
|
+
const data = guide ? (guide[locale] || guide.en) : null;
|
|
305
304
|
|
|
306
|
-
//
|
|
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
|
|
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}
|
|
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}
|
|
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>
|