aimeat 1.23.2 → 1.23.3
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/dist/locales/en.json +536 -21
- package/dist/locales/fi.json +548 -33
- package/dist/public/components/ContactCard.js +28 -0
- package/dist/public/css/views/admin.css +12 -0
- package/dist/public/css/views/agents-detail.css +62 -10
- package/dist/public/css/views/landing.css +87 -0
- package/dist/public/css/views/offers.css +96 -0
- package/dist/public/css/views/portfolio.css +18 -0
- package/dist/public/css/views/profile.css +153 -8
- package/dist/public/css/views/start.css +96 -0
- package/dist/public/img/business-hero.png +0 -0
- package/dist/public/img/toimitus.png +0 -0
- package/dist/public/js/components/tag-cloud.js +46 -31
- package/dist/public/js/components/tag-editor.js +2 -2
- package/dist/public/js/recents.js +40 -0
- package/dist/public/js/services/admin.js +6 -0
- package/dist/public/js/services/agents.js +34 -0
- package/dist/public/js/services/offers.js +152 -0
- package/dist/public/js/services/packages.js +1 -0
- package/dist/public/spa.html +32 -2
- package/dist/public/views/admin/subdomains-tab.js +182 -0
- package/dist/public/views/admin.js +2 -0
- package/dist/public/views/business.js +119 -0
- package/dist/public/views/how-it-works.js +159 -0
- package/dist/public/views/landing.js +235 -0
- package/dist/public/views/portfolio.js +120 -23
- package/dist/public/views/pricing.js +75 -0
- package/dist/public/views/profile/access-tab.js +124 -51
- package/dist/public/views/profile/agents/agent-card.js +68 -37
- package/dist/public/views/profile/agents/shared-board.js +56 -22
- package/dist/public/views/profile/agents/tab-activity.js +24 -12
- package/dist/public/views/profile/agents/tab-agent-config.js +31 -1
- package/dist/public/views/profile/agents/tab-data-access.js +9 -13
- package/dist/public/views/profile/agents/tab-integration.js +27 -1
- package/dist/public/views/profile/agents/tab-messages.js +6 -11
- package/dist/public/views/profile/agents/tab-quality.js +45 -5
- package/dist/public/views/profile/agents-tab.js +41 -6
- package/dist/public/views/profile/apps-tab.js +7 -1
- package/dist/public/views/profile/knowledge-tab.js +4 -1
- package/dist/public/views/profile/landing-page.js +427 -123
- package/dist/public/views/profile/memory-tab.js +409 -211
- package/dist/public/views/profile/nodes-tab.js +18 -3
- package/dist/public/views/profile/offers-tab.js +310 -0
- package/dist/public/views/profile/organisms-tab.js +289 -28
- package/dist/public/views/profile/packages-tab.js +152 -16
- package/dist/public/views/profile/portfolio-tab.js +98 -31
- package/dist/public/views/profile.js +2 -0
- package/dist/public/views/start-flows.js +281 -0
- package/dist/public/views/start.js +258 -0
- package/dist/src/generated/api-types.d.ts +856 -17
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +15 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +12 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +1476 -160
- package/dist/src/generated/prisma-postgres/index.js +15 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +11 -0
- package/dist/src/generated/prisma-postgres/wasm.js +15 -3
- package/dist/src/middleware/subdomain.d.ts +28 -0
- package/dist/src/middleware/subdomain.d.ts.map +1 -0
- package/dist/src/middleware/subdomain.js +32 -0
- package/dist/src/middleware/subdomain.js.map +1 -0
- package/dist/src/models/offer-schemas.d.ts +229 -0
- package/dist/src/models/offer-schemas.d.ts.map +1 -0
- package/dist/src/models/offer-schemas.js +100 -0
- package/dist/src/models/offer-schemas.js.map +1 -0
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +33 -0
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/agents.d.ts.map +1 -1
- package/dist/src/routes/agents.js +146 -1
- package/dist/src/routes/agents.js.map +1 -1
- package/dist/src/routes/apps-backup.d.ts +20 -0
- package/dist/src/routes/apps-backup.d.ts.map +1 -0
- package/dist/src/routes/apps-backup.js +165 -0
- package/dist/src/routes/apps-backup.js.map +1 -0
- package/dist/src/routes/ghii.d.ts.map +1 -1
- package/dist/src/routes/ghii.js +4 -0
- package/dist/src/routes/ghii.js.map +1 -1
- package/dist/src/routes/instances.d.ts +2 -1
- package/dist/src/routes/instances.d.ts.map +1 -1
- package/dist/src/routes/instances.js +59 -1
- package/dist/src/routes/instances.js.map +1 -1
- package/dist/src/routes/packages.d.ts.map +1 -1
- package/dist/src/routes/packages.js +32 -0
- package/dist/src/routes/packages.js.map +1 -1
- package/dist/src/routes/permissions.d.ts +12 -0
- package/dist/src/routes/permissions.d.ts.map +1 -1
- package/dist/src/routes/permissions.js +35 -3
- package/dist/src/routes/permissions.js.map +1 -1
- package/dist/src/routes/portal.d.ts +3 -0
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +12 -0
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +15 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -0
- package/dist/src/routes/public-stats.js +90 -0
- package/dist/src/routes/public-stats.js.map +1 -0
- package/dist/src/routes/sse.d.ts +7 -2
- package/dist/src/routes/sse.d.ts.map +1 -1
- package/dist/src/routes/sse.js +35 -5
- package/dist/src/routes/sse.js.map +1 -1
- package/dist/src/routes/subdomains.d.ts +33 -0
- package/dist/src/routes/subdomains.d.ts.map +1 -0
- package/dist/src/routes/subdomains.js +208 -0
- package/dist/src/routes/subdomains.js.map +1 -0
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +12 -1
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +4 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/apps-backup-export.d.ts +22 -0
- package/dist/src/services/apps-backup-export.d.ts.map +1 -0
- package/dist/src/services/apps-backup-export.js +136 -0
- package/dist/src/services/apps-backup-export.js.map +1 -0
- package/dist/src/services/apps-backup-import.d.ts +120 -0
- package/dist/src/services/apps-backup-import.d.ts.map +1 -0
- package/dist/src/services/apps-backup-import.js +259 -0
- package/dist/src/services/apps-backup-import.js.map +1 -0
- package/dist/src/services/component-registrar.d.ts +17 -0
- package/dist/src/services/component-registrar.d.ts.map +1 -1
- package/dist/src/services/component-registrar.js +63 -5
- package/dist/src/services/component-registrar.js.map +1 -1
- package/dist/src/services/handbooks/appdev.d.ts +1 -1
- package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
- package/dist/src/services/handbooks/appdev.js +5 -2
- package/dist/src/services/handbooks/appdev.js.map +1 -1
- package/dist/src/storage/interface.d.ts +11 -1
- 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 +10 -1
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +76 -0
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +8 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +61 -0
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +11 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/index.d.ts +1 -0
- package/dist/src/storage/repositories/index.d.ts.map +1 -1
- package/dist/src/storage/repositories/package.repository.d.ts +2 -0
- package/dist/src/storage/repositories/package.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/subdomain-site.repository.d.ts +19 -0
- package/dist/src/storage/repositories/subdomain-site.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/subdomain-site.repository.js +2 -0
- package/dist/src/storage/repositories/subdomain-site.repository.js.map +1 -0
- package/dist/static/app-catalog.html +1139 -28
- package/package.json +1 -1
- package/prisma/schema.postgres.prisma +11 -0
- package/prisma/schema.prisma +11 -0
|
@@ -15,6 +15,28 @@
|
|
|
15
15
|
* AppStrip — home/section sub-components
|
|
16
16
|
* - LandingPage — main orchestrator (default export)
|
|
17
17
|
* @version-history
|
|
18
|
+
* v3.2.0 — 2026-06-10 — Sidebar reorg: groups follow the information-refinement pipeline
|
|
19
|
+
* (Information / Automation / Activity / Build & Share / Account / Infrastructure) —
|
|
20
|
+
* Daily/Personal/Technical are gone. Pinned section under Home (pin-on-hover 📌, max 5,
|
|
21
|
+
* defaults organisms/agents/memory/scheduler, persisted in user memory `sidebar.pins`);
|
|
22
|
+
* groups collapse/expand with localStorage memory; Infrastructure renders AND opens only
|
|
23
|
+
* for operators (open() refuses infra ids; APIs operator-gated server-side); Node Stats
|
|
24
|
+
* left the menu (now a sub-tab on the Nodes page); "work" parked at Build & Share bottom
|
|
25
|
+
* pending placement decision.
|
|
26
|
+
* v3.1.0 — 2026-06-10 — Sidebar identity shows the generated identicon (same seed as the profile
|
|
27
|
+
* card) instead of a plain accent ball; home-card list rows lightened (weight 500, dim color) so
|
|
28
|
+
* card titles read as headers again. Agents-card rows deep-link to the agent (primes
|
|
29
|
+
* aimeat.agents.open → agents tab expands + scrolls to it); the next-run row opens the Scheduler.
|
|
30
|
+
* v3.0.0 — 2026-06-10 — Home is a DASHBOARD ("what happened, what waits for me"): new
|
|
31
|
+
* WaitingForYou box (pending publish approvals per org/workspace + join requests + incoming
|
|
32
|
+
* invitations, with Review/View buttons that prime the organisms tab), Continue card (cross-type
|
|
33
|
+
* recents from /js/recents.js with real display names — replaces the AppStrip file listing) and
|
|
34
|
+
* Agents card (active today / last seen + next scheduled run). Extensions promo shows only while
|
|
35
|
+
* apps < 3 and is dismissable (localStorage). ProfileCard stats are navigation (click → section);
|
|
36
|
+
* profile editing moved behind a Profile button / avatar click. Edit-profile modal: avatar live
|
|
37
|
+
* preview, locale hint, real "Change in the Email tab" link, Change-password link in the footer.
|
|
38
|
+
* Change-password modal: live requirement checklist, inline mismatch, neutral Show/Hide toggles,
|
|
39
|
+
* button verb "Change password". Sidebar agents count badge removed (badges = action only).
|
|
18
40
|
* v2.3.0 — 2026-06-10 — Drop the "← Home" button from the content header (visual noise,
|
|
19
41
|
* duplicated the sidebar's 🏠 Home item); the header keeps the current-tab label.
|
|
20
42
|
* v2.2.0 — 2026-06-10 — Listen for the `aimeat-open-tab` CustomEvent so tab components can
|
|
@@ -33,13 +55,69 @@ import { h } from 'preact';
|
|
|
33
55
|
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
34
56
|
import htm from 'htm';
|
|
35
57
|
const html = htm.bind(h);
|
|
36
|
-
import { t } from '/js/i18n.js';
|
|
58
|
+
import { t, getLocale } from '/js/i18n.js';
|
|
37
59
|
import { escHtml } from '/js/utils.js';
|
|
38
60
|
import { getNodeUrl, getProfile, updateProfile, changePassword } from '/js/services/auth.js';
|
|
39
61
|
import { listApps } from '/js/services/apps.js';
|
|
62
|
+
import { listAgents } from '/js/services/agents.js';
|
|
63
|
+
import { listAllSchedules } from '/js/services/schedules.js';
|
|
64
|
+
import * as orgService from '/js/services/organisms.js';
|
|
65
|
+
import { listRecents } from '/js/recents.js';
|
|
66
|
+
import { getMemory, createMemory } from '/js/services/memory.js';
|
|
40
67
|
import { Spinner } from './shared.js';
|
|
41
68
|
import { minidenticon } from '/lib/minidenticons.min.js';
|
|
42
69
|
|
|
70
|
+
/* ───── Small time helpers (reuse the organisms rel-time keys) ───── */
|
|
71
|
+
|
|
72
|
+
function fmtDateLocal(s) {
|
|
73
|
+
return new Date(s).toLocaleDateString(getLocale() === 'fi' ? 'fi-FI' : undefined);
|
|
74
|
+
}
|
|
75
|
+
function relTime(s) {
|
|
76
|
+
const ts = new Date(s).getTime();
|
|
77
|
+
if (!Number.isFinite(ts)) return '';
|
|
78
|
+
const mins = Math.max(0, Math.round((Date.now() - ts) / 60000));
|
|
79
|
+
if (mins < 60) return (t('organisms.relMin') || '{n} min ago').replace('{n}', String(mins));
|
|
80
|
+
const hours = Math.round(mins / 60);
|
|
81
|
+
if (hours < 24) return (t('organisms.relHours') || '{n} h ago').replace('{n}', String(hours));
|
|
82
|
+
const days = Math.round(hours / 24);
|
|
83
|
+
if (days <= 7) return (t('organisms.relDays') || '{n} d ago').replace('{n}', String(days));
|
|
84
|
+
return fmtDateLocal(s);
|
|
85
|
+
}
|
|
86
|
+
function fmtClock(s) {
|
|
87
|
+
const d = new Date(s);
|
|
88
|
+
if (!Number.isFinite(d.getTime())) return '';
|
|
89
|
+
const loc = getLocale() === 'fi' ? 'fi-FI' : undefined;
|
|
90
|
+
return d.toDateString() === new Date().toDateString()
|
|
91
|
+
? d.toLocaleTimeString(loc, { hour: '2-digit', minute: '2-digit' })
|
|
92
|
+
: d.toLocaleString(loc, { day: 'numeric', month: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ───── Home-card navigation: prime the organisms tab's sessionStorage, then open it ───── */
|
|
96
|
+
|
|
97
|
+
function openProfileTab(tabId) {
|
|
98
|
+
window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId } }));
|
|
99
|
+
}
|
|
100
|
+
function gotoWorkspace(orgId, wsId, wsTab) {
|
|
101
|
+
try {
|
|
102
|
+
sessionStorage.setItem('aimeat.ws.openId', orgId);
|
|
103
|
+
sessionStorage.setItem('aimeat.ws.openWs', wsId);
|
|
104
|
+
if (wsTab) sessionStorage.setItem(`aimeat.ws.${orgId}.${wsId}.tab`, wsTab);
|
|
105
|
+
} catch { /* noop */ }
|
|
106
|
+
openProfileTab('organisms');
|
|
107
|
+
}
|
|
108
|
+
function gotoOrganism(orgId, homeTab) {
|
|
109
|
+
try {
|
|
110
|
+
sessionStorage.setItem('aimeat.ws.openId', orgId);
|
|
111
|
+
sessionStorage.removeItem('aimeat.ws.openWs');
|
|
112
|
+
if (homeTab) sessionStorage.setItem('aimeat.org.tab', homeTab);
|
|
113
|
+
} catch { /* noop */ }
|
|
114
|
+
openProfileTab('organisms');
|
|
115
|
+
}
|
|
116
|
+
function gotoOrganismsList() {
|
|
117
|
+
try { sessionStorage.removeItem('aimeat.ws.openId'); sessionStorage.removeItem('aimeat.ws.openWs'); } catch { /* noop */ }
|
|
118
|
+
openProfileTab('organisms');
|
|
119
|
+
}
|
|
120
|
+
|
|
43
121
|
/* ───── Tier heuristic ───── */
|
|
44
122
|
|
|
45
123
|
const TIER_LEVELS = { 'new': 0, 'active': 1, 'experienced': 2 };
|
|
@@ -72,7 +150,7 @@ export function tierLevel(tier) {
|
|
|
72
150
|
|
|
73
151
|
/* ───── Edit Profile Modal ───── */
|
|
74
152
|
|
|
75
|
-
function EditProfileModal({ session, onClose, onSaved }) {
|
|
153
|
+
function EditProfileModal({ session, onClose, onSaved, onChangePassword }) {
|
|
76
154
|
const [loading, setLoading] = useState(true);
|
|
77
155
|
const [saving, setSaving] = useState(false);
|
|
78
156
|
const [fields, setFields] = useState({ display_name: '', bio: '', avatar: '', locale: 'en' });
|
|
@@ -149,10 +227,13 @@ function EditProfileModal({ session, onClose, onSaved }) {
|
|
|
149
227
|
</label>
|
|
150
228
|
<label class="pf-edit-label">
|
|
151
229
|
${t('profile.landing.editAvatar')}
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
230
|
+
<div class="pf-avatar-row">
|
|
231
|
+
<input type="text" class="pf-edit-input" value=${fields.avatar}
|
|
232
|
+
placeholder=${t('profile.landing.editAvatarPlaceholder')}
|
|
233
|
+
maxlength="50"
|
|
234
|
+
onInput=${(e) => set('avatar', e.target.value)} />
|
|
235
|
+
<span class="pf-avatar-preview" aria-hidden="true">${fields.avatar || '🙂'}</span>
|
|
236
|
+
</div>
|
|
156
237
|
</label>
|
|
157
238
|
<label class="pf-edit-label">
|
|
158
239
|
${t('profile.landing.editLocale')}
|
|
@@ -161,14 +242,18 @@ function EditProfileModal({ session, onClose, onSaved }) {
|
|
|
161
242
|
<option value="en">English</option>
|
|
162
243
|
<option value="fi">Suomi</option>
|
|
163
244
|
</select>
|
|
245
|
+
<div class="pf-edit-hint">${t('profile.landing.editLocaleHint') || 'Your preferred language — used for the portal UI; agents can read it from your profile to answer in it.'}</div>
|
|
164
246
|
</label>
|
|
165
247
|
<div class="pf-edit-label">
|
|
166
248
|
${t('profile.landing.editEmail')}
|
|
167
249
|
<div class="pf-edit-readonly">${currentEmail || t('profile.landing.editEmailNone')}</div>
|
|
168
|
-
<
|
|
250
|
+
<a href="#" class="pf-edit-link" onClick=${(e) => { e.preventDefault(); onClose(); window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'email' } })); }}>
|
|
251
|
+
${t('profile.landing.editEmailLink') || 'Change in the Email tab →'}</a>
|
|
169
252
|
</div>
|
|
170
253
|
</div>
|
|
171
254
|
<div class="pf-edit-footer">
|
|
255
|
+
<a href="#" class="pf-edit-link pf-edit-footer-left" onClick=${(e) => { e.preventDefault(); onChangePassword?.(); }}>
|
|
256
|
+
${t('profile.landing.changePassword')}…</a>
|
|
172
257
|
<button class="btn-outline" onClick=${onClose} disabled=${saving}>
|
|
173
258
|
${t('profile.landing.editCancel')}
|
|
174
259
|
</button>
|
|
@@ -184,6 +269,18 @@ function EditProfileModal({ session, onClose, onSaved }) {
|
|
|
184
269
|
|
|
185
270
|
/* ───── Change Password Modal ───── */
|
|
186
271
|
|
|
272
|
+
/* Password input with a neutral show/hide toggle (text-presentation eye, gray — red would read
|
|
273
|
+
* as an error). */
|
|
274
|
+
function PwInput({ value, onInput }) {
|
|
275
|
+
const [show, setShow] = useState(false);
|
|
276
|
+
return html`
|
|
277
|
+
<div class="pf-pw-wrap">
|
|
278
|
+
<input type=${show ? 'text' : 'password'} class="pf-edit-input" value=${value} onInput=${onInput} />
|
|
279
|
+
<button type="button" class="pf-pw-eye"
|
|
280
|
+
onClick=${() => setShow(s => !s)}>${show ? (t('profile.landing.hidePassword') || 'Hide') : (t('profile.landing.showPassword') || 'Show')}</button>
|
|
281
|
+
</div>`;
|
|
282
|
+
}
|
|
283
|
+
|
|
187
284
|
function ChangePasswordModal({ onClose, onChanged }) {
|
|
188
285
|
const [current, setCurrent] = useState('');
|
|
189
286
|
const [newPw, setNewPw] = useState('');
|
|
@@ -191,6 +288,16 @@ function ChangePasswordModal({ onClose, onChanged }) {
|
|
|
191
288
|
const [saving, setSaving] = useState(false);
|
|
192
289
|
const [err, setErr] = useState('');
|
|
193
290
|
|
|
291
|
+
// Live checklist — the requirements line becomes useful when it ticks green while typing.
|
|
292
|
+
const rules = [
|
|
293
|
+
{ ok: newPw.length >= 8, label: t('profile.landing.pwMin') || 'At least 8 characters' },
|
|
294
|
+
{ ok: /[A-Z]/.test(newPw), label: t('profile.landing.pwUpper') || 'An uppercase letter' },
|
|
295
|
+
{ ok: /[a-z]/.test(newPw), label: t('profile.landing.pwLower') || 'A lowercase letter' },
|
|
296
|
+
{ ok: /\d/.test(newPw), label: t('profile.landing.pwDigit') || 'A number' },
|
|
297
|
+
];
|
|
298
|
+
const rulesOk = rules.every(r => r.ok);
|
|
299
|
+
const mismatch = confirm.length > 0 && newPw !== confirm;
|
|
300
|
+
|
|
194
301
|
const save = async () => {
|
|
195
302
|
setErr('');
|
|
196
303
|
if (newPw !== confirm) {
|
|
@@ -225,28 +332,28 @@ function ChangePasswordModal({ onClose, onChanged }) {
|
|
|
225
332
|
<div class="pf-edit-body">
|
|
226
333
|
<label class="pf-edit-label">
|
|
227
334
|
${t('profile.landing.currentPassword')}
|
|
228
|
-
|
|
229
|
-
onInput=${(e) => setCurrent(e.target.value)} />
|
|
335
|
+
<${PwInput} value=${current} onInput=${(e) => setCurrent(e.target.value)} />
|
|
230
336
|
</label>
|
|
231
337
|
<label class="pf-edit-label">
|
|
232
338
|
${t('profile.landing.newPassword')}
|
|
233
|
-
|
|
234
|
-
onInput=${(e) => setNewPw(e.target.value)} />
|
|
339
|
+
<${PwInput} value=${newPw} onInput=${(e) => setNewPw(e.target.value)} />
|
|
235
340
|
</label>
|
|
341
|
+
<ul class="pf-pw-rules">
|
|
342
|
+
${rules.map(r => html`<li class=${r.ok ? 'ok' : ''} key=${r.label}>${r.ok ? '✓' : '○'} ${r.label}</li>`)}
|
|
343
|
+
</ul>
|
|
236
344
|
<label class="pf-edit-label">
|
|
237
345
|
${t('profile.landing.confirmPassword')}
|
|
238
|
-
|
|
239
|
-
onInput=${(e) => setConfirm(e.target.value)} />
|
|
346
|
+
<${PwInput} value=${confirm} onInput=${(e) => setConfirm(e.target.value)} />
|
|
240
347
|
</label>
|
|
241
|
-
|
|
348
|
+
${mismatch ? html`<div class="pf-edit-error">${t('profile.landing.passwordMismatch')}</div>` : null}
|
|
242
349
|
${err && html`<div class="pf-edit-error">${err}</div>`}
|
|
243
350
|
</div>
|
|
244
351
|
<div class="pf-edit-footer">
|
|
245
352
|
<button class="btn-outline" onClick=${onClose} disabled=${saving}>
|
|
246
353
|
${t('profile.landing.editCancel')}
|
|
247
354
|
</button>
|
|
248
|
-
<button class="btn-primary" onClick=${save} disabled=${saving || !current || !
|
|
249
|
-
${saving ? t('profile.landing.passwordChanging') : t('profile.landing.
|
|
355
|
+
<button class="btn-primary" onClick=${save} disabled=${saving || !current || !rulesOk || !confirm || mismatch}>
|
|
356
|
+
${saving ? t('profile.landing.passwordChanging') : (t('profile.landing.changePasswordBtn') || 'Change password')}
|
|
250
357
|
</button>
|
|
251
358
|
</div>
|
|
252
359
|
</div>
|
|
@@ -254,27 +361,181 @@ function ChangePasswordModal({ onClose, onChanged }) {
|
|
|
254
361
|
`;
|
|
255
362
|
}
|
|
256
363
|
|
|
364
|
+
/* ───── Home dashboard cards ───── */
|
|
365
|
+
|
|
366
|
+
/* "Waiting for you" — everything that needs the user's decision, aggregated across organisms:
|
|
367
|
+
* pending publish approvals (per workspace), pending join requests (orgs they manage), and
|
|
368
|
+
* incoming organism invitations. Renders nothing when there is nothing to do. */
|
|
369
|
+
function WaitingForYou({ owner }) {
|
|
370
|
+
const [items, setItems] = useState(null);
|
|
371
|
+
const load = useCallback(async () => {
|
|
372
|
+
try {
|
|
373
|
+
const out = [];
|
|
374
|
+
const orgsResp = await orgService.listOrganisms({ member: owner }).catch(() => null);
|
|
375
|
+
const orgs = orgsResp?.data?.organisms || [];
|
|
376
|
+
await Promise.all(orgs.map(async (org) => {
|
|
377
|
+
const canManage = org.creatorGhii === owner || (org.admins || []).includes(owner);
|
|
378
|
+
const [aps, reqs] = await Promise.all([
|
|
379
|
+
orgService.listApprovals(org.id, 'pending').catch(() => []),
|
|
380
|
+
canManage ? orgService.listJoinRequests(org.id).catch(() => null) : Promise.resolve(null),
|
|
381
|
+
]);
|
|
382
|
+
if ((aps || []).length > 0) {
|
|
383
|
+
const byWs = {};
|
|
384
|
+
for (const a of aps) { const w = a.arguments?.ws || ''; byWs[w] = (byWs[w] || 0) + 1; }
|
|
385
|
+
let names = {};
|
|
386
|
+
try { names = Object.fromEntries((await orgService.listWorkspaces(org.id)).map(w => [w.id, w.name])); } catch { /* ids will do */ }
|
|
387
|
+
for (const [wsId, n] of Object.entries(byWs)) {
|
|
388
|
+
out.push({ kind: 'review', n, orgId: org.id, orgName: org.name, wsId, wsName: names[wsId] || wsId });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const pend = ((reqs?.data?.join_requests) || []).filter(r => r.status === 'pending').length;
|
|
392
|
+
if (pend > 0) out.push({ kind: 'join', n: pend, orgId: org.id, orgName: org.name });
|
|
393
|
+
}));
|
|
394
|
+
const inv = await orgService.listMyInvitations().catch(() => null);
|
|
395
|
+
for (const e of (inv?.data?.invitations || [])) out.push({ kind: 'invite', orgName: e?.organism?.name || '' });
|
|
396
|
+
setItems(out);
|
|
397
|
+
} catch { setItems([]); }
|
|
398
|
+
}, [owner]);
|
|
399
|
+
useEffect(() => { load(); }, [load]);
|
|
400
|
+
const liveRef = useRef(load); liveRef.current = load;
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
const h = () => liveRef.current();
|
|
403
|
+
window.addEventListener('aimeat-live-update', h);
|
|
404
|
+
return () => window.removeEventListener('aimeat-live-update', h);
|
|
405
|
+
}, []);
|
|
406
|
+
|
|
407
|
+
if (!items || items.length === 0) return null;
|
|
408
|
+
return html`
|
|
409
|
+
<div class="pf-waiting">
|
|
410
|
+
<div class="pf-waiting-title">${'📨 '}${t('profile.landing.waitingTitle') || 'Waiting for you'}</div>
|
|
411
|
+
${items.map((it, i) => html`
|
|
412
|
+
<div class="pf-waiting-row" key=${i}>
|
|
413
|
+
<span class="pf-waiting-text">
|
|
414
|
+
${it.kind === 'review' ? html`
|
|
415
|
+
<b>${(t('profile.landing.draftsToReview') || '{n} drafts to review').replace('{n}', String(it.n))}</b>
|
|
416
|
+
<span class="pf-waiting-where"> · ${escHtml(it.orgName)} / ${escHtml(it.wsName)}</span>` : null}
|
|
417
|
+
${it.kind === 'join' ? html`
|
|
418
|
+
<b>${it.n === 1 ? (t('profile.landing.joinReqOne') || '1 join request') : (t('profile.landing.joinReqMany') || '{n} join requests').replace('{n}', String(it.n))}</b>
|
|
419
|
+
<span class="pf-waiting-where"> · ${escHtml(it.orgName)}</span>` : null}
|
|
420
|
+
${it.kind === 'invite' ? html`
|
|
421
|
+
<b>${t('profile.landing.inviteWaiting') || 'You’re invited'}</b>
|
|
422
|
+
<span class="pf-waiting-where"> · ${escHtml(it.orgName)}</span>` : null}
|
|
423
|
+
</span>
|
|
424
|
+
${it.kind === 'review' ? html`
|
|
425
|
+
<button class="btn-outline btn-sm" onClick=${() => (it.wsId ? gotoWorkspace(it.orgId, it.wsId, 'review') : gotoOrganism(it.orgId))}>${t('profile.landing.reviewBtn') || 'Review'}</button>` : null}
|
|
426
|
+
${it.kind === 'join' ? html`
|
|
427
|
+
<button class="btn-outline btn-sm" onClick=${() => gotoOrganism(it.orgId, 'members')}>${t('profile.landing.viewBtn') || 'View'}</button>` : null}
|
|
428
|
+
${it.kind === 'invite' ? html`
|
|
429
|
+
<button class="btn-outline btn-sm" onClick=${() => gotoOrganismsList()}>${t('profile.landing.viewBtn') || 'View'}</button>` : null}
|
|
430
|
+
</div>
|
|
431
|
+
`)}
|
|
432
|
+
</div>
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* "Continue" — the last opened things across types (workspace / app / organism), with real
|
|
437
|
+
* display names. Backed by /js/recents.js (device-local). Renders nothing when empty. */
|
|
438
|
+
const RECENT_ICONS = { workspace: '🗂', app: '▦', organism: '🏢', board: '📋' };
|
|
439
|
+
function ContinueCard() {
|
|
440
|
+
const [items] = useState(() => listRecents(5));
|
|
441
|
+
if (!items.length) return null;
|
|
442
|
+
const openItem = (it) => {
|
|
443
|
+
if (it.type === 'workspace' && it.data?.orgId) gotoWorkspace(it.data.orgId, it.data.wsId);
|
|
444
|
+
else if (it.type === 'organism' && it.data?.orgId) gotoOrganism(it.data.orgId);
|
|
445
|
+
else if (it.type === 'app' && it.data?.filename) window.open(`/v1/apps/${encodeURIComponent(it.data.owner)}/${encodeURIComponent(it.data.filename)}?mode=inline`, '_blank');
|
|
446
|
+
};
|
|
447
|
+
return html`
|
|
448
|
+
<div class="pf-home-card">
|
|
449
|
+
<div class="pf-home-card-title">${t('profile.landing.continueTitle') || 'Continue'}</div>
|
|
450
|
+
${items.map(it => html`
|
|
451
|
+
<button class="pf-home-row" key=${it.type + it.id} onClick=${() => openItem(it)}>
|
|
452
|
+
<span class="pf-home-row-ico">${RECENT_ICONS[it.type] || '•'}</span>
|
|
453
|
+
<span class="pf-home-row-label">${escHtml(it.label)}</span>
|
|
454
|
+
<span class="pf-home-row-meta">${relTime(it.at)}</span>
|
|
455
|
+
</button>
|
|
456
|
+
`)}
|
|
457
|
+
</div>
|
|
458
|
+
`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* "Agents" — who has been active today, who is idle, and the next scheduled run. */
|
|
462
|
+
function AgentsCard({ owner }) {
|
|
463
|
+
const [agents, setAgents] = useState(null);
|
|
464
|
+
const [nextJob, setNextJob] = useState(null);
|
|
465
|
+
const load = useCallback(async () => {
|
|
466
|
+
try {
|
|
467
|
+
const list = (await listAgents(owner)).filter(a => !String(a.name || '').startsWith('session-'));
|
|
468
|
+
list.sort((a, b) => String(b.last_seen || '').localeCompare(String(a.last_seen || '')));
|
|
469
|
+
setAgents(list);
|
|
470
|
+
} catch { setAgents([]); }
|
|
471
|
+
try {
|
|
472
|
+
const r = await listAllSchedules();
|
|
473
|
+
const all = [...(r?.data?.managed || []), ...(r?.data?.extensions || []), ...(r?.data?.agentInternal || [])]
|
|
474
|
+
.filter(s => s.enabled !== false && s.nextRunAt && new Date(s.nextRunAt).getTime() > Date.now())
|
|
475
|
+
.sort((a, b) => String(a.nextRunAt).localeCompare(String(b.nextRunAt)));
|
|
476
|
+
setNextJob(all[0] || null);
|
|
477
|
+
} catch { /* scheduler row is optional */ }
|
|
478
|
+
}, [owner]);
|
|
479
|
+
useEffect(() => { load(); }, [load]);
|
|
480
|
+
const liveRef = useRef(load); liveRef.current = load;
|
|
481
|
+
useEffect(() => {
|
|
482
|
+
const h = () => liveRef.current();
|
|
483
|
+
window.addEventListener('aimeat-live-update', h);
|
|
484
|
+
return () => window.removeEventListener('aimeat-live-update', h);
|
|
485
|
+
}, []);
|
|
486
|
+
|
|
487
|
+
if (!agents || (agents.length === 0 && !nextJob)) return null;
|
|
488
|
+
const todayStr = new Date().toDateString();
|
|
489
|
+
const isToday = (s) => s && new Date(s).toDateString() === todayStr;
|
|
490
|
+
const activeToday = agents.filter(a => isToday(a.last_seen)).length;
|
|
491
|
+
return html`
|
|
492
|
+
<div class="pf-home-card">
|
|
493
|
+
<button class="pf-home-card-title pf-home-card-link" onClick=${() => openProfileTab('agents')}>
|
|
494
|
+
${t('profile.landing.agentsTitle') || 'Agents'}
|
|
495
|
+
${activeToday > 0 ? html`<span class="pf-home-card-note"> · ${(t('profile.landing.activeTodayCount') || '{n} active today').replace('{n}', String(activeToday))}</span>` : null}
|
|
496
|
+
</button>
|
|
497
|
+
${agents.slice(0, 3).map(a => html`
|
|
498
|
+
<button class="pf-home-row" key=${a.gaii || a.name}
|
|
499
|
+
onClick=${() => { try { sessionStorage.setItem('aimeat.agents.open', a.name); } catch { /* noop */ } openProfileTab('agents'); }}>
|
|
500
|
+
<span class="pf-home-row-ico">${'🤖'}</span>
|
|
501
|
+
<span class="pf-home-row-label">${escHtml(a.display_name || a.name)}</span>
|
|
502
|
+
<span class="pf-home-row-meta ${isToday(a.last_seen) ? 'pf-ok' : ''}">
|
|
503
|
+
${a.last_seen ? (isToday(a.last_seen) ? (t('profile.landing.agentActiveToday') || 'active today') : relTime(a.last_seen)) : '—'}
|
|
504
|
+
</span>
|
|
505
|
+
</button>
|
|
506
|
+
`)}
|
|
507
|
+
${nextJob ? html`
|
|
508
|
+
<button class="pf-home-row" key="nextjob" onClick=${() => openProfileTab('scheduler')}>
|
|
509
|
+
<span class="pf-home-row-ico">⏰</span>
|
|
510
|
+
<span class="pf-home-row-label">${escHtml(nextJob.name || nextJob.id || '')}</span>
|
|
511
|
+
<span class="pf-home-row-meta">${(t('profile.landing.nextRunAt') || 'next run {time}').replace('{time}', fmtClock(nextJob.nextRunAt))}</span>
|
|
512
|
+
</button>` : null}
|
|
513
|
+
</div>
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
516
|
+
|
|
257
517
|
/* ───── Sub-components ───── */
|
|
258
518
|
|
|
259
|
-
function ProfileCard({ tier, stats, session, onEditProfile,
|
|
519
|
+
function ProfileCard({ tier, stats, session, onEditProfile, switchTab }) {
|
|
260
520
|
const NODE_URL = getNodeUrl();
|
|
261
521
|
const isNew = tier === 'new';
|
|
262
522
|
const isExperienced = tier === 'experienced';
|
|
263
523
|
const avatarSvg = minidenticon(session.owner || 'user');
|
|
264
524
|
|
|
525
|
+
// Stats are NAVIGATION, not decoration \u2014 each one opens its own section.
|
|
526
|
+
const stat = (icon, val, labelKey, tabId, green) => html`
|
|
527
|
+
<button class="pf-lp-stat pf-lp-stat-link" onClick=${() => switchTab?.(tabId)}>
|
|
528
|
+
${icon} <span class="pf-lp-stat-val${green ? ' pf-lp-stat-green' : ''}">${val}</span> ${t(labelKey)}
|
|
529
|
+
</button>`;
|
|
530
|
+
|
|
265
531
|
return html`
|
|
266
532
|
<div class="pf-lp-card">
|
|
267
533
|
<div class="pf-lp-card-header">
|
|
268
|
-
<div class="pf-lp-avatar"
|
|
534
|
+
<div class="pf-lp-avatar" role="button" tabindex="0" title=${t('profile.landing.editProfile')}
|
|
535
|
+
onClick=${() => onEditProfile?.()} dangerouslySetInnerHTML=${{ __html: avatarSvg }}></div>
|
|
269
536
|
<div class="pf-lp-info">
|
|
270
537
|
<div class="pf-lp-name-row">
|
|
271
538
|
<span class="pf-lp-name">${escHtml(session.displayName || session.owner)}</span>
|
|
272
|
-
<a href="#" class="pf-lp-edit" onClick=${(e) => { e.preventDefault(); onEditProfile?.(); }}>
|
|
273
|
-
${t('profile.landing.editProfile')} \u2192
|
|
274
|
-
</a>
|
|
275
|
-
<a href="#" class="pf-lp-edit" onClick=${(e) => { e.preventDefault(); onChangePassword?.(); }}>
|
|
276
|
-
${t('profile.landing.changePassword')} \u2192
|
|
277
|
-
</a>
|
|
278
539
|
</div>
|
|
279
540
|
<div class="pf-lp-ghii">${escHtml(session.ghii || '')}</div>
|
|
280
541
|
<div class="pf-lp-node">${t('profile.node')}: ${escHtml(NODE_URL)}</div>
|
|
@@ -288,27 +549,21 @@ function ProfileCard({ tier, stats, session, onEditProfile, onChangePassword })
|
|
|
288
549
|
</div>`
|
|
289
550
|
}
|
|
290
551
|
</div>
|
|
552
|
+
<button class="btn-outline btn-sm pf-lp-profile-btn" onClick=${() => onEditProfile?.()}>
|
|
553
|
+
${t('profile.landing.profileBtn') || 'Profile'}</button>
|
|
291
554
|
</div>
|
|
292
555
|
<div class="pf-lp-stats">
|
|
293
556
|
${isNew ? html`
|
|
294
|
-
${stats.memory > 0 &&
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
${(stats.balance != null && stats.balance !== '-' && stats.balance > 0) && html`
|
|
298
|
-
<div class="pf-lp-stat">
|
|
299
|
-
\u{1F48E} <span class="pf-lp-stat-val pf-lp-stat-green">${stats.balance}</span> ${t('profile.stats.morsels')}
|
|
300
|
-
</div>
|
|
301
|
-
`}
|
|
557
|
+
${stats.memory > 0 && stat('\u{1F9E0}', stats.memory, 'profile.stats.memories', 'memory')}
|
|
558
|
+
${(stats.balance != null && stats.balance !== '-' && stats.balance > 0)
|
|
559
|
+
&& stat('\u{1F48E}', stats.balance, 'profile.stats.morsels', 'wallet', true)}
|
|
302
560
|
` : html`
|
|
303
|
-
${stats.apps > 0 &&
|
|
304
|
-
${stats.memory > 0 &&
|
|
305
|
-
${(stats.balance != null && stats.balance !== '-' && stats.balance > 0)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
${stats.
|
|
309
|
-
${isExperienced && stats.agents > 0 && html`
|
|
310
|
-
<div class="pf-lp-stat">\u{1F916} <span class="pf-lp-stat-val">${stats.agents}</span> ${t('profile.stats.agents')}</div>
|
|
311
|
-
`}
|
|
561
|
+
${stats.apps > 0 && stat('\u{1F4F1}', stats.apps, 'profile.stats.apps', 'apps')}
|
|
562
|
+
${stats.memory > 0 && stat('\u{1F9E0}', stats.memory, 'profile.stats.memories', 'memory')}
|
|
563
|
+
${(stats.balance != null && stats.balance !== '-' && stats.balance > 0)
|
|
564
|
+
&& stat('\u{1F48E}', stats.balance, 'profile.stats.morsels', 'wallet', true)}
|
|
565
|
+
${stats.services > 0 && stat('\u{1F50C}', stats.services, 'profile.stats.services', 'actions')}
|
|
566
|
+
${isExperienced && stats.agents > 0 && stat('\u{1F916}', stats.agents, 'profile.stats.agents', 'agents')}
|
|
312
567
|
`}
|
|
313
568
|
</div>
|
|
314
569
|
</div>
|
|
@@ -404,10 +659,15 @@ function GhostTiles({ switchTab }) {
|
|
|
404
659
|
`;
|
|
405
660
|
}
|
|
406
661
|
|
|
407
|
-
|
|
662
|
+
/* Onboarding promo — shown only while the user has fewer than 3 apps, and dismissable for good.
|
|
663
|
+
* After that the same content lives on the Extensions page; for a seasoned user it was dead space. */
|
|
664
|
+
function CortexSection({ switchTab, onDismiss }) {
|
|
408
665
|
return html`
|
|
409
|
-
<div class="pf-landing-section">
|
|
410
|
-
<div class="pf-menu-title">${t('profile.landing.cortexSectionTitle')}
|
|
666
|
+
<div class="pf-landing-section pf-promo">
|
|
667
|
+
<div class="pf-menu-title">${t('profile.landing.cortexSectionTitle')}
|
|
668
|
+
<button class="pf-promo-dismiss" title=${t('profile.landing.promoDismiss') || 'Hide'}
|
|
669
|
+
onClick=${(e) => { e.stopPropagation(); onDismiss?.(); }}>✕</button>
|
|
670
|
+
</div>
|
|
411
671
|
<div class="pf-cortex-grid">
|
|
412
672
|
<div class="pf-cortex-card" onClick=${() => switchTab('extensions')}>
|
|
413
673
|
<div class="pf-cortex-header">
|
|
@@ -426,48 +686,8 @@ function CortexSection({ switchTab }) {
|
|
|
426
686
|
`;
|
|
427
687
|
}
|
|
428
688
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const toggleApp = (filename) => {
|
|
433
|
-
setExpandedApp(prev => prev === filename ? null : filename);
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
const launchApp = (e, app) => {
|
|
437
|
-
e.stopPropagation();
|
|
438
|
-
window.open(`/v1/apps/${app.owner}/${app.filename}?mode=inline`, '_blank');
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
return html`
|
|
442
|
-
<div class="pf-app-strip">
|
|
443
|
-
<div class="pf-app-strip-header">
|
|
444
|
-
<div class="pf-menu-title">${t('profile.landing.myApps')}</div>
|
|
445
|
-
<a href="#" class="pf-app-strip-link" onClick=${(e) => { e.preventDefault(); switchTab('apps'); }}>
|
|
446
|
-
${t('profile.landing.allApps')} \u2192
|
|
447
|
-
</a>
|
|
448
|
-
</div>
|
|
449
|
-
<div class="pf-app-row">
|
|
450
|
-
${apps.length > 0 ? apps.map(app => html`
|
|
451
|
-
<div class="pf-app-chip ${expandedApp === app.filename ? 'pf-app-chip-expanded' : ''}"
|
|
452
|
-
key=${app.filename || app.name}
|
|
453
|
-
onClick=${() => toggleApp(app.filename)}>
|
|
454
|
-
<span class="pf-chip-icon">\u{1F4F1}</span>
|
|
455
|
-
${escHtml(app.name || app.filename || 'App')}
|
|
456
|
-
${expandedApp === app.filename && html`
|
|
457
|
-
<button class="pf-chip-launch" onClick=${(e) => launchApp(e, app)}
|
|
458
|
-
title=${t('profile.landing.openApp')} aria-label=${t('profile.landing.openApp')}>\u{1F517}</button>
|
|
459
|
-
`}
|
|
460
|
-
</div>
|
|
461
|
-
`) : html`
|
|
462
|
-
<div class="pf-app-chip pf-app-chip-ghost" onClick=${() => switchTab('packages')}>
|
|
463
|
-
<span class="pf-chip-icon">+</span>
|
|
464
|
-
${t('profile.landing.installFirst')} \u2192
|
|
465
|
-
</div>
|
|
466
|
-
`}
|
|
467
|
-
</div>
|
|
468
|
-
</div>
|
|
469
|
-
`;
|
|
470
|
-
}
|
|
689
|
+
/* (AppStrip removed \u2014 the cross-type "Continue" card replaced it: raw filenames in a horizontal
|
|
690
|
+
* scroller duplicated the Apps tab and read as a file listing.) */
|
|
471
691
|
|
|
472
692
|
/* ───── Generic layout components ───── */
|
|
473
693
|
|
|
@@ -521,46 +741,62 @@ function InlineView({ tabId, label, onClose, renderTab }) {
|
|
|
521
741
|
* Every tab is always present and grouped into stable sections — no activity-based
|
|
522
742
|
* hiding, so humans and agentic developers can predict where each tab lives.
|
|
523
743
|
* Group titles reuse existing i18n keys; tab labels reuse profile.tabs.* / *.tabLabel. */
|
|
744
|
+
/* Grouping follows the information-refinement pipeline and usage frequency, not an
|
|
745
|
+
* abstract taxonomy (the old Daily/Personal/Technical groups are gone). Badges remain
|
|
746
|
+
* reserved for action-required counts only — never static totals. */
|
|
524
747
|
const SIDEBAR_GROUPS = [
|
|
525
|
-
{ titleKey: 'profile.landing.
|
|
526
|
-
{ id: '
|
|
527
|
-
{ id: 'scheduler', icon: '⏰', labelKey: 'profile.tabs.scheduler' },
|
|
748
|
+
{ titleKey: 'profile.landing.menuInformation', items: [ // raw → curated → governed
|
|
749
|
+
{ id: 'organisms', icon: '\u{1F3E2}', labelKey: 'profile.tabs.organisms' },
|
|
528
750
|
{ id: 'memory', icon: '\u{1F9E0}', labelKey: 'profile.tabs.memory' },
|
|
529
|
-
{ id: 'boards', icon: '\u{1F4CB}', labelKey: 'profile.tabs.boards' },
|
|
530
751
|
{ id: 'knowledge', icon: '\u{1F4DA}', labelKey: 'knowledge.tabLabel' },
|
|
752
|
+
{ id: 'boards', icon: '\u{1F4CB}', labelKey: 'profile.tabs.boards' },
|
|
753
|
+
] },
|
|
754
|
+
{ titleKey: 'profile.landing.menuAutomation', items: [ // agents + their infrastructure
|
|
755
|
+
{ id: 'agents', icon: '\u{1F916}', labelKey: 'profile.tabs.agents' },
|
|
756
|
+
{ id: 'offers', icon: '\u{1F9F0}', labelKey: 'profile.tabs.offers' },
|
|
757
|
+
{ id: 'scheduler', icon: '⏰', labelKey: 'profile.tabs.scheduler' },
|
|
758
|
+
{ id: 'actions', icon: '\u{1F6E0}️', labelKey: 'profile.tabs.services' },
|
|
759
|
+
{ id: 'mcp', icon: '\u{1F517}', labelKey: 'profile.tabs.mcp' },
|
|
760
|
+
] },
|
|
761
|
+
{ titleKey: 'profile.landing.menuActivity', items: [ // communication + events
|
|
531
762
|
{ id: 'notifications', icon: '\u{1F514}', labelKey: 'profile.tabs.notifications' },
|
|
532
|
-
{ id: '
|
|
763
|
+
{ id: 'email', icon: '\u{1F4E7}', labelKey: 'profile.tabs.email' },
|
|
764
|
+
{ id: 'chatsessions', icon: '\u{1F4AC}', labelKey: 'profile.tabs.chatSessions' },
|
|
533
765
|
] },
|
|
534
766
|
{ titleKey: 'profile.landing.menuBuildShare', items: [
|
|
767
|
+
{ id: 'apps', icon: '⚙️', labelKey: 'profile.tabs.apps' },
|
|
535
768
|
{ id: 'generator', icon: '\u{1F534}', labelKey: 'profile.generator.tabLabel' },
|
|
536
|
-
|
|
537
|
-
|
|
769
|
+
/* foundry removed from the menu 2026-06-10 (owner: not in use). The tab module and
|
|
770
|
+
* its route id still exist — restore by re-adding this item. */
|
|
538
771
|
{ id: 'extensions', icon: '\u{1F50C}', labelKey: 'profile.tabs.extensions' },
|
|
539
772
|
{ id: 'capabilities', icon: '⚡', labelKey: 'capabilities.tabLabel' },
|
|
540
|
-
{ id: 'portfolio', icon: '\u{1F3A8}', labelKey: 'portfolio.tabLabel' },
|
|
541
773
|
{ id: 'packages', icon: '\u{1F4E6}', labelKey: 'profile.tabs.packages' },
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
774
|
+
{ id: 'portfolio', icon: '\u{1F3A8}', labelKey: 'portfolio.tabLabel' },
|
|
775
|
+
{ id: 'calibrator', icon: '\u{1F3AF}', labelKey: 'profile.calibrator.tabLabel' },
|
|
776
|
+
/* TODO(owner 2026-06-10): "work" placement is undecided — parked at the bottom of
|
|
777
|
+
* Build & Share until re-evaluated. */
|
|
545
778
|
{ id: 'work', icon: '\u{1F4CB}', labelKey: 'profile.tabs.work' },
|
|
546
|
-
{ id: 'mcp', icon: '\u{1F517}', labelKey: 'profile.tabs.mcp' },
|
|
547
|
-
{ id: 'chatsessions', icon: '\u{1F4AC}', labelKey: 'profile.tabs.chatSessions' },
|
|
548
779
|
] },
|
|
549
|
-
{ titleKey: 'profile.landing.
|
|
780
|
+
{ titleKey: 'profile.landing.menuAccount', items: [
|
|
550
781
|
{ id: 'wallet', icon: '\u{1F48E}', labelKey: 'profile.tabs.wallet' },
|
|
551
|
-
{ id: 'email', icon: '\u{1F4E7}', labelKey: 'profile.tabs.email' },
|
|
552
|
-
{ id: 'access', icon: '\u{1F510}', labelKey: 'profile.tabs.access' },
|
|
553
|
-
{ id: 'organisms', icon: '\u{1F3E2}', labelKey: 'profile.tabs.organisms' },
|
|
554
782
|
{ id: 'dataWallet', icon: '\u{1F512}', labelKey: 'profile.tabs.dataWallet' },
|
|
783
|
+
{ id: 'access', icon: '\u{1F510}', labelKey: 'profile.tabs.access' },
|
|
555
784
|
] },
|
|
556
|
-
|
|
785
|
+
/* Operator-only: the group AND its routes are gated on the operator role (open()
|
|
786
|
+
* refuses these ids for non-operators; the underlying APIs enforce server-side).
|
|
787
|
+
* nodeStats left the menu — it lives as a tab on the Nodes page now. */
|
|
788
|
+
{ titleKey: 'profile.landing.menuInfra', adminOnly: true, items: [
|
|
557
789
|
{ id: 'federation', icon: '\u{1F310}', labelKey: 'profile.tabs.federation' },
|
|
558
790
|
{ id: 'nodes', icon: '\u{1F5A5}️', labelKey: 'profile.tabs.nodes' },
|
|
559
|
-
{ id: 'nodeStats', icon: '\u{1F4CA}', labelKey: 'profile.tabs.nodeStats' },
|
|
560
791
|
{ id: 'security', icon: '\u{1F6E1}️', labelKey: 'profile.tabs.security' },
|
|
561
792
|
] },
|
|
562
793
|
];
|
|
563
794
|
|
|
795
|
+
// Flat item lookup (pinned section renders items by id).
|
|
796
|
+
const SIDEBAR_ITEM_BY_ID = Object.fromEntries(SIDEBAR_GROUPS.flatMap(g => g.items.map(it => [it.id, it])));
|
|
797
|
+
const INFRA_TAB_IDS = new Set(['federation', 'nodes', 'nodeStats', 'security']);
|
|
798
|
+
const DEFAULT_PINS = ['organisms', 'agents', 'memory', 'scheduler'];
|
|
799
|
+
|
|
564
800
|
/* ───── Main landing page ───── */
|
|
565
801
|
|
|
566
802
|
export default function LandingPage({ tier, stats, session, navigate, showToast, locale, renderTab, getTabLabel }) {
|
|
@@ -583,6 +819,9 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
|
|
|
583
819
|
});
|
|
584
820
|
const [expanded, setExpanded] = useState(false);
|
|
585
821
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
822
|
+
// Extensions promo: onboarding-only (apps < 3) and dismissable for good.
|
|
823
|
+
const [showPromo, setShowPromo] = useState(() => { try { return localStorage.getItem('aimeat.cortexPromoDismissed') !== '1'; } catch { return true; } });
|
|
824
|
+
const dismissPromo = () => { setShowPromo(false); try { localStorage.setItem('aimeat.cortexPromoDismissed', '1'); } catch { /* noop */ } };
|
|
586
825
|
|
|
587
826
|
const owner = session.owner;
|
|
588
827
|
|
|
@@ -606,7 +845,13 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
|
|
|
606
845
|
}, []);
|
|
607
846
|
|
|
608
847
|
/* Open a tab inline below a specific slot. Toggle if same tab clicked again. */
|
|
848
|
+
const isOperator = (session?.roles || []).includes('operator');
|
|
849
|
+
|
|
609
850
|
const open = useCallback((tabId, slot) => {
|
|
851
|
+
// Infrastructure tabs are operator-only — refuse them here too so deep links
|
|
852
|
+
// (sessionStorage restore, aimeat-open-tab events) can't open the views.
|
|
853
|
+
// The underlying APIs enforce the role server-side regardless.
|
|
854
|
+
if (INFRA_TAB_IDS.has(tabId) && !isOperator) return;
|
|
610
855
|
setOpenView(prev => {
|
|
611
856
|
const next = (prev?.tabId === tabId) ? null : { tabId, slot };
|
|
612
857
|
if (next) {
|
|
@@ -616,7 +861,39 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
|
|
|
616
861
|
}
|
|
617
862
|
return next;
|
|
618
863
|
});
|
|
864
|
+
}, [isOperator]);
|
|
865
|
+
|
|
866
|
+
/* ── Pinned items: 3–5 favourites under Home, persisted per user (memory key
|
|
867
|
+
`sidebar.pins`, same pattern as organisms.ui). Defaults for new users. ── */
|
|
868
|
+
const [pins, setPins] = useState(DEFAULT_PINS);
|
|
869
|
+
useEffect(() => {
|
|
870
|
+
getMemory('sidebar.pins')
|
|
871
|
+
.then(r => { const v = r?.data?.value; if (Array.isArray(v) && v.length) setPins(v.filter(id => SIDEBAR_ITEM_BY_ID[id])); })
|
|
872
|
+
.catch(() => { /* defaults stand */ });
|
|
619
873
|
}, []);
|
|
874
|
+
const togglePin = (id) => {
|
|
875
|
+
setPins(prev => {
|
|
876
|
+
let next;
|
|
877
|
+
if (prev.includes(id)) next = prev.filter(x => x !== id);
|
|
878
|
+
else if (prev.length >= 5) { showToast?.(t('profile.landing.pinLimit') || 'Max 5 pinned items'); return prev; }
|
|
879
|
+
else next = [...prev, id];
|
|
880
|
+
createMemory('sidebar.pins', next, 'private').catch(() => {});
|
|
881
|
+
return next;
|
|
882
|
+
});
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
/* ── Group collapse/expand, remembered per browser. ── */
|
|
886
|
+
const [collapsedGroups, setCollapsedGroups] = useState(() => {
|
|
887
|
+
try { return new Set(JSON.parse(localStorage.getItem('aimeat.sidebar.collapsed') || '[]')); } catch { return new Set(); }
|
|
888
|
+
});
|
|
889
|
+
const toggleGroup = (key) => {
|
|
890
|
+
setCollapsedGroups(prev => {
|
|
891
|
+
const next = new Set(prev);
|
|
892
|
+
if (next.has(key)) next.delete(key); else next.add(key);
|
|
893
|
+
try { localStorage.setItem('aimeat.sidebar.collapsed', JSON.stringify([...next])); } catch { /* noop */ }
|
|
894
|
+
return next;
|
|
895
|
+
});
|
|
896
|
+
};
|
|
620
897
|
|
|
621
898
|
const close = useCallback(() => {
|
|
622
899
|
sessionStorage.removeItem('aimeat-profile-tab');
|
|
@@ -659,6 +936,7 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
|
|
|
659
936
|
session=${session}
|
|
660
937
|
onClose=${() => setEditOpen(false)}
|
|
661
938
|
onSaved=${() => { setEditOpen(false); showToast?.(t('profile.landing.editSaved')); }}
|
|
939
|
+
onChangePassword=${() => { setEditOpen(false); setPwOpen(true); }}
|
|
662
940
|
/>`}
|
|
663
941
|
|
|
664
942
|
${pwOpen && html`<${ChangePasswordModal}
|
|
@@ -671,7 +949,7 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
|
|
|
671
949
|
|
|
672
950
|
<aside class="pf-sidebar">
|
|
673
951
|
<div class="pf-side-identity">
|
|
674
|
-
<div class="pf-side-avatar"></div>
|
|
952
|
+
<div class="pf-side-avatar" dangerouslySetInnerHTML=${{ __html: minidenticon(owner || 'user') }}></div>
|
|
675
953
|
<div class="pf-side-id-name">${session.displayName || owner}</div>
|
|
676
954
|
</div>
|
|
677
955
|
|
|
@@ -680,20 +958,41 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
|
|
|
680
958
|
<span class="pf-side-ico">\u{1F3E0}</span><span class="pf-side-label">${t('profile.landing.home')}</span>
|
|
681
959
|
</button>
|
|
682
960
|
|
|
683
|
-
${
|
|
684
|
-
|
|
685
|
-
<
|
|
686
|
-
|
|
687
|
-
<
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
961
|
+
${(() => {
|
|
962
|
+
const renderItem = (it, pinned) => html`
|
|
963
|
+
<button class="pf-side-item${isOpen(it.id) ? ' pf-side-item--active' : ''}" key=${(pinned ? 'pin-' : '') + it.id}
|
|
964
|
+
onClick=${() => { open(it.id, 'main'); setDrawerOpen(false); }}>
|
|
965
|
+
<span class="pf-side-ico">${it.icon}</span>
|
|
966
|
+
<span class="pf-side-label">${t(it.labelKey)}</span>
|
|
967
|
+
${it.badgeStat && typeof stats?.[it.badgeStat] === 'number' && stats[it.badgeStat] > 0
|
|
968
|
+
? html`<span class="pf-side-badge">${stats[it.badgeStat]}</span>` : null}
|
|
969
|
+
<span class="pf-side-pin${pins.includes(it.id) ? ' pf-side-pin--on' : ''}"
|
|
970
|
+
role="button" tabindex="-1"
|
|
971
|
+
title=${pins.includes(it.id) ? (t('profile.landing.pinRemove') || 'Unpin') : (t('profile.landing.pinAdd') || 'Pin')}
|
|
972
|
+
onClick=${(e) => { e.stopPropagation(); togglePin(it.id); }}>📌</span>
|
|
973
|
+
</button>`;
|
|
974
|
+
const pinnedItems = pins.map(id => SIDEBAR_ITEM_BY_ID[id]).filter(Boolean)
|
|
975
|
+
.filter(it => !(INFRA_TAB_IDS.has(it.id) && !isOperator));
|
|
976
|
+
return html`
|
|
977
|
+
${pinnedItems.length > 0 && html`
|
|
978
|
+
<div class="pf-side-group">
|
|
979
|
+
<div class="pf-side-group-title">${t('profile.landing.menuPinned') || 'Pinned'}</div>
|
|
980
|
+
${pinnedItems.map(it => renderItem(it, true))}
|
|
981
|
+
</div>
|
|
982
|
+
`}
|
|
983
|
+
${SIDEBAR_GROUPS.filter(g => !g.adminOnly || isOperator).map(g => {
|
|
984
|
+
const collapsed = collapsedGroups.has(g.titleKey);
|
|
985
|
+
return html`
|
|
986
|
+
<div class="pf-side-group" key=${g.titleKey}>
|
|
987
|
+
<button class="pf-side-group-title pf-side-group-toggle" onClick=${() => toggleGroup(g.titleKey)}>
|
|
988
|
+
<span class="pf-chevron ${collapsed ? '' : 'pf-chevron-open'}">▼</span> ${t(g.titleKey)}
|
|
989
|
+
</button>
|
|
990
|
+
${!collapsed && g.items.map(it => renderItem(it, false))}
|
|
991
|
+
</div>
|
|
992
|
+
`;
|
|
993
|
+
})}
|
|
994
|
+
`;
|
|
995
|
+
})()}
|
|
697
996
|
</aside>
|
|
698
997
|
|
|
699
998
|
<main class="pf-content">
|
|
@@ -705,12 +1004,17 @@ export default function LandingPage({ tier, stats, session, navigate, showToast,
|
|
|
705
1004
|
` : html`
|
|
706
1005
|
<${ProfileCard} tier=${tier} stats=${stats} session=${session}
|
|
707
1006
|
onEditProfile=${() => setEditOpen(true)}
|
|
708
|
-
|
|
1007
|
+
switchTab=${(id) => open(id, 'main')} />
|
|
709
1008
|
${isNew ? html`<${HeroOnboarding} switchTab=${(id) => open(id, 'main')} />` : null}
|
|
710
1009
|
${(isNew || isActive) ? html`<${KnowledgeCallout} switchTab=${() => open('knowledge', 'main')} />` : null}
|
|
711
1010
|
${isNew ? html`<${GhostTiles} switchTab=${() => open('packages', 'main')} />` : null}
|
|
712
|
-
<${
|
|
713
|
-
|
|
1011
|
+
<${WaitingForYou} owner=${owner} />
|
|
1012
|
+
<div class="pf-home-grid">
|
|
1013
|
+
<${ContinueCard} />
|
|
1014
|
+
<${AgentsCard} owner=${owner} />
|
|
1015
|
+
</div>
|
|
1016
|
+
${(showPromo && apps.length < 3) ? html`
|
|
1017
|
+
<${CortexSection} switchTab=${() => open('extensions', 'main')} onDismiss=${dismissPromo} />` : null}
|
|
714
1018
|
`}
|
|
715
1019
|
</main>
|
|
716
1020
|
</div>
|