aimeat 1.35.0 → 1.37.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/dist/.env.example +31 -0
- package/dist/locales/en.json +192 -27
- package/dist/locales/fi.json +192 -27
- package/dist/public/components/Markdown.js +9 -0
- package/dist/public/components/MemoryEmbed.js +151 -0
- package/dist/public/components/Mermaid.js +4 -1
- package/dist/public/components/NotificationBell.js +45 -19
- package/dist/public/css/components/markdown.css +15 -0
- package/dist/public/css/views/agents-detail.css +17 -0
- package/dist/public/css/views/portfolio.css +51 -0
- package/dist/public/css/views/profile.css +31 -0
- package/dist/public/js/services/agents.js +9 -0
- package/dist/public/js/services/organisms.js +37 -1
- package/dist/public/llms-template.txt +278 -0
- package/dist/public/spa.html +39 -2
- package/dist/public/sw.js +50 -34
- package/dist/public/views/my-company.js +13 -1
- package/dist/public/views/portfolio.js +267 -44
- package/dist/public/views/profile/agents/agent-card.js +4 -0
- package/dist/public/views/profile/agents/tab-contracts.js +112 -0
- package/dist/public/views/profile/landing-page.js +10 -1
- package/dist/public/views/profile/memory-tab.js +6 -2
- package/dist/public/views/profile/notifications-tab.js +9 -1
- package/dist/public/views/profile/organisms/home.js +23 -3
- package/dist/public/views/profile/organisms/participants-panel.js +89 -17
- package/dist/public/views/profile/organisms/workspace-apps.js +130 -0
- package/dist/public/views/profile/organisms/workspace.js +5 -0
- package/dist/public/views/profile/organisms-tab.js +1 -1
- package/dist/public/views/secretary.js +13 -0
- package/dist/scripts/archive-stale-versions.d.ts +33 -0
- package/dist/scripts/archive-stale-versions.d.ts.map +1 -0
- package/dist/scripts/archive-stale-versions.js +278 -0
- package/dist/scripts/archive-stale-versions.js.map +1 -0
- package/dist/src/cli/connect/mcp/tools/agent-management.d.ts +4 -1
- package/dist/src/cli/connect/mcp/tools/agent-management.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-management.js +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-management.js.map +1 -1
- package/dist/src/cli/init-wizard.d.ts.map +1 -1
- package/dist/src/cli/init-wizard.js +32 -0
- package/dist/src/cli/init-wizard.js.map +1 -1
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +21 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/data/aimeat-iam-package.d.ts +10 -2
- package/dist/src/data/aimeat-iam-package.d.ts.map +1 -1
- package/dist/src/data/aimeat-iam-package.js +39 -10
- package/dist/src/data/aimeat-iam-package.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +802 -60
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +5 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +2 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +54 -2
- package/dist/src/generated/prisma-postgres/index.js +5 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +2 -0
- package/dist/src/generated/prisma-postgres/wasm.js +5 -3
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +1 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts +3 -0
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +21 -7
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +1 -1
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/core.js +1 -1
- package/dist/src/mcp/core.js.map +1 -1
- package/dist/src/mcp/extensions.d.ts.map +1 -1
- package/dist/src/mcp/extensions.js +21 -0
- package/dist/src/mcp/extensions.js.map +1 -1
- package/dist/src/mcp/memory-extended.js +1 -1
- package/dist/src/mcp/memory-extended.js.map +1 -1
- package/dist/src/mcp/organisms.d.ts +1 -0
- package/dist/src/mcp/organisms.d.ts.map +1 -1
- package/dist/src/mcp/organisms.js +23 -6
- package/dist/src/mcp/organisms.js.map +1 -1
- package/dist/src/mcp/workspaces.d.ts +7 -0
- package/dist/src/mcp/workspaces.d.ts.map +1 -1
- package/dist/src/mcp/workspaces.js +23 -5
- package/dist/src/mcp/workspaces.js.map +1 -1
- package/dist/src/middleware/subdomain.d.ts +9 -0
- package/dist/src/middleware/subdomain.d.ts.map +1 -1
- package/dist/src/middleware/subdomain.js +24 -3
- package/dist/src/middleware/subdomain.js.map +1 -1
- package/dist/src/models/schemas.d.ts +2 -0
- package/dist/src/models/schemas.d.ts.map +1 -1
- package/dist/src/models/schemas.js +2 -2
- package/dist/src/models/schemas.js.map +1 -1
- package/dist/src/routes/admin-features.js +1 -1
- package/dist/src/routes/admin-features.js.map +1 -1
- package/dist/src/routes/agent-onboarding.d.ts.map +1 -1
- package/dist/src/routes/agent-onboarding.js +44 -7
- package/dist/src/routes/agent-onboarding.js.map +1 -1
- package/dist/src/routes/agents.d.ts +8 -0
- package/dist/src/routes/agents.d.ts.map +1 -1
- package/dist/src/routes/agents.js +91 -5
- package/dist/src/routes/agents.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts +5 -0
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +52 -20
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/boards.d.ts.map +1 -1
- package/dist/src/routes/boards.js +4 -1
- package/dist/src/routes/boards.js.map +1 -1
- package/dist/src/routes/bootstrap.d.ts +2 -0
- package/dist/src/routes/bootstrap.d.ts.map +1 -1
- package/dist/src/routes/bootstrap.js +5 -0
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/capabilities.d.ts.map +1 -1
- package/dist/src/routes/capabilities.js +4 -1
- package/dist/src/routes/capabilities.js.map +1 -1
- package/dist/src/routes/catalogue.d.ts.map +1 -1
- package/dist/src/routes/catalogue.js +5 -4
- package/dist/src/routes/catalogue.js.map +1 -1
- package/dist/src/routes/extensions.d.ts.map +1 -1
- package/dist/src/routes/extensions.js +11 -0
- package/dist/src/routes/extensions.js.map +1 -1
- package/dist/src/routes/ghii.d.ts.map +1 -1
- package/dist/src/routes/ghii.js +65 -17
- package/dist/src/routes/ghii.js.map +1 -1
- package/dist/src/routes/lib-editor.d.ts +26 -0
- package/dist/src/routes/lib-editor.d.ts.map +1 -0
- package/dist/src/routes/lib-editor.js +215 -0
- package/dist/src/routes/lib-editor.js.map +1 -0
- package/dist/src/routes/lib-markdown.d.ts +16 -1
- package/dist/src/routes/lib-markdown.d.ts.map +1 -1
- package/dist/src/routes/lib-markdown.js +281 -2
- package/dist/src/routes/lib-markdown.js.map +1 -1
- package/dist/src/routes/lib-organism.d.ts +28 -0
- package/dist/src/routes/lib-organism.d.ts.map +1 -0
- package/dist/src/routes/lib-organism.js +298 -0
- package/dist/src/routes/lib-organism.js.map +1 -0
- package/dist/src/routes/lib-portfolio-standalone.d.ts +22 -0
- package/dist/src/routes/lib-portfolio-standalone.d.ts.map +1 -0
- package/dist/src/routes/lib-portfolio-standalone.js +68 -0
- package/dist/src/routes/lib-portfolio-standalone.js.map +1 -0
- package/dist/src/routes/libs.d.ts +18 -0
- package/dist/src/routes/libs.d.ts.map +1 -1
- package/dist/src/routes/libs.js +116 -18
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/memory.d.ts +3 -0
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +58 -4
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/notifications.d.ts +5 -1
- package/dist/src/routes/notifications.d.ts.map +1 -1
- package/dist/src/routes/notifications.js +68 -3
- package/dist/src/routes/notifications.js.map +1 -1
- package/dist/src/routes/oauth-login.d.ts +26 -21
- package/dist/src/routes/oauth-login.d.ts.map +1 -1
- package/dist/src/routes/oauth-login.js +257 -229
- package/dist/src/routes/oauth-login.js.map +1 -1
- package/dist/src/routes/openrouter.d.ts +3 -0
- package/dist/src/routes/openrouter.d.ts.map +1 -1
- package/dist/src/routes/openrouter.js +10 -5
- package/dist/src/routes/openrouter.js.map +1 -1
- package/dist/src/routes/organisms.d.ts +6 -0
- package/dist/src/routes/organisms.d.ts.map +1 -1
- package/dist/src/routes/organisms.js +203 -16
- package/dist/src/routes/organisms.js.map +1 -1
- package/dist/src/routes/owners.d.ts.map +1 -1
- package/dist/src/routes/owners.js +12 -8
- package/dist/src/routes/owners.js.map +1 -1
- package/dist/src/routes/portfolio.d.ts +41 -1
- package/dist/src/routes/portfolio.d.ts.map +1 -1
- package/dist/src/routes/portfolio.js +94 -30
- package/dist/src/routes/portfolio.js.map +1 -1
- package/dist/src/routes/push.d.ts +15 -0
- package/dist/src/routes/push.d.ts.map +1 -1
- package/dist/src/routes/push.js +16 -1
- package/dist/src/routes/push.js.map +1 -1
- 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 +10 -2
- package/dist/src/routes/site.js.map +1 -1
- package/dist/src/routes/subdomains.d.ts +5 -0
- package/dist/src/routes/subdomains.d.ts.map +1 -1
- package/dist/src/routes/subdomains.js +56 -0
- package/dist/src/routes/subdomains.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +10 -15
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
- package/dist/src/server-bootstrap/static-files.js +9 -4
- package/dist/src/server-bootstrap/static-files.js.map +1 -1
- package/dist/src/services/access-guard.d.ts.map +1 -1
- package/dist/src/services/access-guard.js +7 -0
- package/dist/src/services/access-guard.js.map +1 -1
- package/dist/src/services/ai-completion.d.ts +2 -0
- package/dist/src/services/ai-completion.d.ts.map +1 -1
- package/dist/src/services/ai-completion.js +2 -1
- package/dist/src/services/ai-completion.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/ecosystem-access.d.ts +24 -8
- package/dist/src/services/ecosystem-access.d.ts.map +1 -1
- package/dist/src/services/ecosystem-access.js +27 -5
- package/dist/src/services/ecosystem-access.js.map +1 -1
- package/dist/src/services/generator-autopilot.d.ts +2 -0
- package/dist/src/services/generator-autopilot.d.ts.map +1 -1
- package/dist/src/services/generator-autopilot.js +3 -1
- package/dist/src/services/generator-autopilot.js.map +1 -1
- package/dist/src/services/iam/app-commands.d.ts +75 -0
- package/dist/src/services/iam/app-commands.d.ts.map +1 -0
- package/dist/src/services/iam/app-commands.js +55 -0
- package/dist/src/services/iam/app-commands.js.map +1 -0
- package/dist/src/services/iam/define-app-iam.d.ts +45 -0
- package/dist/src/services/iam/define-app-iam.d.ts.map +1 -0
- package/dist/src/services/iam/define-app-iam.js +75 -0
- package/dist/src/services/iam/define-app-iam.js.map +1 -0
- package/dist/src/services/iam/model.d.ts +113 -0
- package/dist/src/services/iam/model.d.ts.map +1 -0
- package/dist/src/services/iam/model.js +115 -0
- package/dist/src/services/iam/model.js.map +1 -0
- package/dist/src/services/mailbox-notification.d.ts +1 -0
- package/dist/src/services/mailbox-notification.d.ts.map +1 -1
- package/dist/src/services/mailbox-notification.js +3 -0
- package/dist/src/services/mailbox-notification.js.map +1 -1
- package/dist/src/services/notebook-ai.d.ts +2 -0
- package/dist/src/services/notebook-ai.d.ts.map +1 -1
- package/dist/src/services/notebook-ai.js +1 -1
- package/dist/src/services/notebook-ai.js.map +1 -1
- package/dist/src/services/notify.d.ts +11 -1
- package/dist/src/services/notify.d.ts.map +1 -1
- package/dist/src/services/notify.js +39 -2
- package/dist/src/services/notify.js.map +1 -1
- package/dist/src/services/oidc-providers.d.ts +94 -0
- package/dist/src/services/oidc-providers.d.ts.map +1 -0
- package/dist/src/services/oidc-providers.js +181 -0
- package/dist/src/services/oidc-providers.js.map +1 -0
- package/dist/src/services/organism-privacy.d.ts +46 -0
- package/dist/src/services/organism-privacy.d.ts.map +1 -0
- package/dist/src/services/organism-privacy.js +42 -0
- package/dist/src/services/organism-privacy.js.map +1 -0
- package/dist/src/services/organism-template.d.ts.map +1 -1
- package/dist/src/services/organism-template.js +3 -1
- package/dist/src/services/organism-template.js.map +1 -1
- package/dist/src/services/prompt-defaults.d.ts +2 -0
- package/dist/src/services/prompt-defaults.d.ts.map +1 -1
- package/dist/src/services/prompt-defaults.js +5 -0
- package/dist/src/services/prompt-defaults.js.map +1 -1
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +16 -4
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/skill-bundle/generator.d.ts +2 -0
- package/dist/src/services/skill-bundle/generator.d.ts.map +1 -1
- package/dist/src/services/skill-bundle/generator.js +3 -1
- package/dist/src/services/skill-bundle/generator.js.map +1 -1
- package/dist/src/services/workspace-access.d.ts +2 -0
- package/dist/src/services/workspace-access.d.ts.map +1 -1
- package/dist/src/services/workspace-access.js +20 -8
- package/dist/src/services/workspace-access.js.map +1 -1
- package/dist/src/services/workspace-engagements.d.ts +65 -0
- package/dist/src/services/workspace-engagements.d.ts.map +1 -0
- package/dist/src/services/workspace-engagements.js +92 -0
- package/dist/src/services/workspace-engagements.js.map +1 -0
- package/dist/src/services/workspace-meta.d.ts +9 -2
- package/dist/src/services/workspace-meta.d.ts.map +1 -1
- package/dist/src/services/workspace-meta.js +22 -2
- package/dist/src/services/workspace-meta.js.map +1 -1
- package/dist/src/storage/interface.d.ts +10 -2
- 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 +1 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +29 -2
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +1 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +23 -8
- 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 +9 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/identity.repository.d.ts +2 -0
- package/dist/src/storage/repositories/identity.repository.d.ts.map +1 -1
- package/dist/src/utils/env-config.d.ts.map +1 -1
- package/dist/src/utils/env-config.js +23 -0
- package/dist/src/utils/env-config.js.map +1 -1
- package/dist/src/utils/env-validator.d.ts.map +1 -1
- package/dist/src/utils/env-validator.js +8 -0
- package/dist/src/utils/env-validator.js.map +1 -1
- package/dist/src/utils/node-descriptor.d.ts.map +1 -1
- package/dist/src/utils/node-descriptor.js +1 -0
- package/dist/src/utils/node-descriptor.js.map +1 -1
- package/dist/static/app-catalog.html +8 -2
- package/dist/static/icons/badge-96.png +0 -0
- package/dist/static/icons/icon-192.png +0 -0
- package/dist/static/icons/icon-512.png +0 -0
- package/dist/static/manifest.json +19 -17
- package/dist/static/sw.js +13 -11
- package/package.json +3 -1
- package/prisma/schema.postgres.prisma +2 -0
- package/prisma/schema.prisma +2 -0
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
* @description Portfolio view — builder (select content, generate AI prompt,
|
|
4
4
|
* upload HTML) and public viewer (sandboxed iframe render).
|
|
5
5
|
* @version-history
|
|
6
|
+
* v1.5.0 — 2026-07-03 — Portfolio origin surfaces: builder shows the standalone
|
|
7
|
+
* URL (<username>.portfolio.<apex>) + an aimeat-badge toggle (showBadge, saved
|
|
8
|
+
* to portfolio.config); viewer toolbar links to the standalone page. Both come
|
|
9
|
+
* from the server (standalone_url on /v1/portfolio/config and data/:username).
|
|
10
|
+
* v1.4.0 — 2026-07-03 — Viewer bridge: the trusted parent posts login state
|
|
11
|
+
* ('aimeat-portfolio-auth') into the sandboxed portfolio and serves a narrow
|
|
12
|
+
* fetch RPC ('aimeat-portfolio-fetch' → '...-fetch-result') that reads the
|
|
13
|
+
* portfolio owner's records with the VISITOR's session — enabling the new
|
|
14
|
+
* 'members' memory visibility (logged-in-only content, never in the HTML).
|
|
15
|
+
* Requests are allowlisted to data.owner_gaiis; the visitor's JWT never
|
|
16
|
+
* enters the iframe. Prompt documents the protocol + members entries.
|
|
17
|
+
* v1.3.2 — 2026-07-03 — Builder prompt fixes for the sandboxed viewer reality:
|
|
18
|
+
* memory fetch URLs now use the anonymous public route /v1/memory/:gaii/:key
|
|
19
|
+
* (old /v1/memory/:key always 401s without auth) and only for public-visibility
|
|
20
|
+
* records; forbid credentialed fetch() (Origin:null + wildcard CORS rejects it);
|
|
21
|
+
* auth-gate pattern defaults to logged-out placeholder + listens for an
|
|
22
|
+
* 'aimeat-portfolio-auth' postMessage instead of window.AIMEAT (unreachable
|
|
23
|
+
* from the opaque-origin sandbox).
|
|
24
|
+
* v1.3.1 — 2026-07-03 — Stamp the SPA's CSP nonce onto <script> tags before setting
|
|
25
|
+
* srcdoc (viewer + builder preview): about:srcdoc inherits the parent document's
|
|
26
|
+
* CSP (script-src 'self' 'nonce-…'), so the portfolio's nonce-less inline script
|
|
27
|
+
* never executed. The sandbox (opaque origin, no allow-same-origin) remains the
|
|
28
|
+
* security boundary — same trust level as the app-launch 'unsafe-inline' CSP.
|
|
29
|
+
* v1.3.0 — 2026-07-03 — Public viewer renders full-bleed: portfolio iframe fills the
|
|
30
|
+
* viewport below the nav (no more 900px column), slim toolbar with back link + owner
|
|
31
|
+
* name + a native Fullscreen button; drop the dead contentDocument autosize hack
|
|
32
|
+
* (opaque-origin sandbox always threw). Campsite: btn btn-* → btn-*.
|
|
6
33
|
* v1.2.0 — 2026-06-10 — Workflow round: ①②③ flow banner explains the AI-chat round-trip;
|
|
7
34
|
* group summaries show selected/total + select-all checkbox; Generate disabled with a hint
|
|
8
35
|
* when nothing is selected; "Custom" type and custom auth-gate open describe-textareas that
|
|
@@ -27,6 +54,24 @@ import { CopyButton } from '/components/CopyButton.js';
|
|
|
27
54
|
|
|
28
55
|
const NODE_URL = typeof window !== 'undefined' ? window.location.origin : '';
|
|
29
56
|
|
|
57
|
+
/* ── CSP nonce stamping for srcdoc iframes ──
|
|
58
|
+
about:srcdoc inherits the SPA document's CSP, whose script-src is
|
|
59
|
+
'self' + a per-request nonce (no 'unsafe-inline') — so a portfolio's
|
|
60
|
+
inline <script> is blocked unless it carries that nonce. Stamp the
|
|
61
|
+
SPA's own nonce onto the portfolio's script tags before rendering
|
|
62
|
+
(client-side mirror of src/utils/csp-nonce.ts). Isolation does not
|
|
63
|
+
rest on CSP here: the sandbox (allow-scripts only → opaque origin,
|
|
64
|
+
no session/cookies/storage) is the security boundary, matching the
|
|
65
|
+
'unsafe-inline' CSP the app-launch endpoint grants published apps. */
|
|
66
|
+
function stampCspNonce(htmlStr) {
|
|
67
|
+
let nonce = '';
|
|
68
|
+
for (const s of document.scripts) {
|
|
69
|
+
if (s.nonce) { nonce = s.nonce; break; }
|
|
70
|
+
}
|
|
71
|
+
if (!nonce) return htmlStr;
|
|
72
|
+
return htmlStr.replace(/<script(?=[\s>])/gi, `<script nonce="${nonce}"`);
|
|
73
|
+
}
|
|
74
|
+
|
|
30
75
|
/* ── Auth helpers ── */
|
|
31
76
|
function getSession() {
|
|
32
77
|
const a = window.AIMEAT?.auth;
|
|
@@ -127,12 +172,33 @@ The user wants to create a personal portfolio website. Generate a single, self-c
|
|
|
127
172
|
}
|
|
128
173
|
}
|
|
129
174
|
|
|
130
|
-
// Selected memory entries
|
|
175
|
+
// Selected memory entries — three delivery paths, decided by record visibility:
|
|
176
|
+
// public → plain anonymous fetch from /v1/memory/:gaii/:key
|
|
177
|
+
// members → requested from the hosting viewer over the postMessage bridge
|
|
178
|
+
// (the viewer fetches with the VISITOR's session; anonymous → no data)
|
|
179
|
+
// other → not deliverable to visitors at all; bake preview or placeholder
|
|
180
|
+
const publicMems = selectedMemories.filter(m => m.visibility === 'public');
|
|
181
|
+
const memberMems = selectedMemories.filter(m => m.visibility === 'members');
|
|
182
|
+
const gatedMems = selectedMemories.filter(m => m.visibility !== 'public' && m.visibility !== 'members');
|
|
131
183
|
if (selectedMemories.length > 0) {
|
|
132
184
|
prompt += `\n## Memory Entries\n`;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
185
|
+
if (publicMems.length > 0) {
|
|
186
|
+
prompt += `These PUBLIC entries can be fetched live (anonymously, no auth) for dynamic portfolio content:\n`;
|
|
187
|
+
for (const mem of publicMems) {
|
|
188
|
+
prompt += `- ${mem.key} → GET ${url}/v1/memory/${encodeURIComponent(mem.gaii || ghii)}/${encodeURIComponent(mem.key)}\n`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (memberMems.length > 0) {
|
|
192
|
+
prompt += `These MEMBERS-ONLY entries are readable by logged-in node members. Do NOT fetch them directly (an anonymous fetch returns 404). Request each from the hosting viewer over the postMessage bridge (see "Viewer Bridge" below) and render the value inside a [data-auth-required] section, keeping a [data-auth-placeholder] fallback:\n`;
|
|
193
|
+
for (const mem of memberMems) {
|
|
194
|
+
prompt += `- key: ${mem.key} · gaii: ${mem.gaii || ghii}\n`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (gatedMems.length > 0) {
|
|
198
|
+
prompt += `These entries are NOT readable by visitors at all (any fetch attempt returns 401/404). Represent each with its preview text baked into the HTML, or a placeholder the user can edit:\n`;
|
|
199
|
+
for (const mem of gatedMems) {
|
|
200
|
+
prompt += `- ${mem.key} (visibility: ${mem.visibility})${mem.preview ? ` — preview: ${mem.preview}` : ''}\n`;
|
|
201
|
+
}
|
|
136
202
|
}
|
|
137
203
|
}
|
|
138
204
|
|
|
@@ -183,35 +249,82 @@ The user wants to create a personal portfolio website. Generate a single, self-c
|
|
|
183
249
|
- **CRITICAL — Images and files:** ONLY use URLs listed in the "Selected Images" section above. Do NOT invent, guess, or fabricate any URLs. If a resource (avatar, resume, screenshot, etc.) is not listed above, it does not exist.
|
|
184
250
|
${resourceNotes}${sectionNotes}- For missing images: Use inline SVG placeholders, CSS gradients, or emoji — never a broken image URL
|
|
185
251
|
- For project screenshots: If no screenshot URL is provided for a project, use a styled CSS placeholder card instead of an <img> tag
|
|
186
|
-
- If memory entries are
|
|
252
|
+
- If PUBLIC memory entries are listed above: fetch them live with plain \`fetch(url)\` calls to those exact URLs
|
|
253
|
+
- **CRITICAL — fetch() must stay credential-free:** always call \`fetch(url)\` with no options object — never set \`credentials\` ('include'/'same-origin'), and never send an Authorization header. The portfolio runs in an opaque-origin sandbox (the browser sends \`Origin: null\`); the node answers CORS with a wildcard, and browsers reject credentialed requests to wildcard responses. Only the anonymous public URLs listed above are reachable.
|
|
187
254
|
- Mobile-responsive design (works on phone, tablet, desktop)
|
|
188
255
|
- Include proper <meta> tags for SEO and social sharing (og:title, og:description, og:image)
|
|
189
256
|
- **IMPORTANT — CSP compatibility:** The portfolio HTML will be rendered inside a sandboxed iframe. All JavaScript MUST be inside a single \`<script>\` tag at the end of \`<body>\`. Do NOT use inline event handlers (onclick=, onload=, etc.) — use addEventListener instead. Do NOT use external script/CSS CDN links.
|
|
190
257
|
`;
|
|
191
258
|
|
|
192
|
-
if (authGates.length > 0) {
|
|
259
|
+
if (authGates.length > 0 || memberMems.length > 0) {
|
|
193
260
|
prompt += `
|
|
194
|
-
##
|
|
195
|
-
|
|
196
|
-
|
|
261
|
+
## Viewer Bridge (auth state + members-only data)
|
|
262
|
+
The portfolio renders inside a sandboxed iframe with an opaque origin: it has NO access to the
|
|
263
|
+
hosting page's session, cookies, or \`window.AIMEAT\` — login state cannot be read directly.
|
|
264
|
+
The hosting AIMEAT viewer bridges over postMessage. Two message flows:
|
|
265
|
+
|
|
266
|
+
1. Viewer → portfolio: \`{ type: 'aimeat-portfolio-auth', loggedIn: boolean }\` — posted on load
|
|
267
|
+
and whenever the visitor's login state changes.
|
|
268
|
+
2. Portfolio → viewer: \`{ type: 'aimeat-portfolio-fetch', gaii: string, key: string, id: number }\`
|
|
269
|
+
(id = your own correlation number). The viewer answers with
|
|
270
|
+
\`{ type: 'aimeat-portfolio-fetch-result', id, ok: boolean, gaii, key, value }\` — \`value\` is the
|
|
271
|
+
memory record's value when \`ok\` is true, otherwise null (not logged in / no access).
|
|
272
|
+
The viewer only serves the gaii+key pairs listed in "Memory Entries" above.
|
|
273
|
+
|
|
274
|
+
Implementation pattern — default to logged-out, activate on the viewer's signal:
|
|
197
275
|
|
|
198
276
|
\`\`\`javascript
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
277
|
+
function applyAuthState(isLoggedIn) {
|
|
278
|
+
document.querySelectorAll('[data-auth-required]').forEach(el => {
|
|
279
|
+
el.style.display = isLoggedIn ? '' : 'none';
|
|
280
|
+
});
|
|
281
|
+
document.querySelectorAll('[data-auth-placeholder]').forEach(el => {
|
|
282
|
+
el.style.display = isLoggedIn ? 'none' : '';
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
applyAuthState(false);
|
|
286
|
+
|
|
287
|
+
// Members-only data: ask the viewer, resolve by correlation id.
|
|
288
|
+
let bridgeSeq = 0;
|
|
289
|
+
const bridgePending = {};
|
|
290
|
+
function fetchMemberData(gaii, key) {
|
|
291
|
+
return new Promise((resolve) => {
|
|
292
|
+
const id = ++bridgeSeq;
|
|
293
|
+
bridgePending[id] = resolve;
|
|
294
|
+
window.parent.postMessage({ type: 'aimeat-portfolio-fetch', gaii, key, id }, '*');
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
window.addEventListener('message', (e) => {
|
|
299
|
+
const d = e.data;
|
|
300
|
+
if (!d) return;
|
|
301
|
+
if (d.type === 'aimeat-portfolio-auth') {
|
|
302
|
+
applyAuthState(!!d.loggedIn);
|
|
303
|
+
// (Re-)request members-only data when the viewer reports a logged-in visitor.
|
|
304
|
+
if (d.loggedIn) loadMemberSections();
|
|
305
|
+
} else if (d.type === 'aimeat-portfolio-fetch-result' && bridgePending[d.id]) {
|
|
306
|
+
bridgePending[d.id](d.ok ? d.value : null);
|
|
307
|
+
delete bridgePending[d.id];
|
|
308
|
+
}
|
|
207
309
|
});
|
|
310
|
+
|
|
311
|
+
async function loadMemberSections() {
|
|
312
|
+
// Example: const contact = await fetchMemberData('<gaii>', '<key>');
|
|
313
|
+
// if (contact) { render it into the [data-auth-required] section }
|
|
314
|
+
}
|
|
208
315
|
\`\`\`
|
|
209
316
|
|
|
210
|
-
Wrap
|
|
317
|
+
Wrap gated content in \`<div data-auth-required>\` and add a placeholder:
|
|
211
318
|
\`<div data-auth-placeholder>Log in to see more content</div>\`
|
|
212
319
|
|
|
213
|
-
|
|
214
|
-
|
|
320
|
+
Rules:
|
|
321
|
+
- Show the placeholder until data actually arrives — no spinners waiting on the bridge
|
|
322
|
+
(an older viewer never answers, and the placeholder is the correct fallback).
|
|
323
|
+
- Members-only values arrive ONLY via the bridge — never bake them into the HTML and never
|
|
324
|
+
fetch them directly.
|
|
325
|
+
- Content that is merely wrapped in [data-auth-required] but baked into the HTML is a
|
|
326
|
+
convenience, not a security boundary — it is visible in the HTML source. Keep truly
|
|
327
|
+
private data out of the portfolio entirely.
|
|
215
328
|
`;
|
|
216
329
|
}
|
|
217
330
|
|
|
@@ -265,6 +378,11 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
265
378
|
|
|
266
379
|
// Existing portfolio state
|
|
267
380
|
const [existingConfig, setExistingConfig] = useState(null);
|
|
381
|
+
// Standalone portfolio-origin URL (<username>.portfolio.<apex>) — null when
|
|
382
|
+
// the node has the portfolio origin disabled.
|
|
383
|
+
const [standaloneUrl, setStandaloneUrl] = useState(null);
|
|
384
|
+
// Whether the standalone page shows the aimeat attribution badge (default on).
|
|
385
|
+
const [showBadge, setShowBadge] = useState(true);
|
|
268
386
|
|
|
269
387
|
// Load catalog + existing config
|
|
270
388
|
useEffect(() => {
|
|
@@ -275,7 +393,11 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
275
393
|
]).then(([catRes, cfgRes]) => {
|
|
276
394
|
if (catRes.ok !== false && catRes.data) setCatalog(catRes.data);
|
|
277
395
|
else setErrMsg('Failed to load content catalog');
|
|
278
|
-
if (cfgRes.ok !== false && cfgRes.data?.config)
|
|
396
|
+
if (cfgRes.ok !== false && cfgRes.data?.config) {
|
|
397
|
+
setExistingConfig(cfgRes.data.config);
|
|
398
|
+
setShowBadge(cfgRes.data.config.showBadge !== false);
|
|
399
|
+
}
|
|
400
|
+
if (cfgRes.ok !== false && cfgRes.data?.standalone_url) setStandaloneUrl(cfgRes.data.standalone_url);
|
|
279
401
|
setLoading(false);
|
|
280
402
|
}).catch(() => {
|
|
281
403
|
setErrMsg('Network error');
|
|
@@ -283,6 +405,14 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
283
405
|
});
|
|
284
406
|
}, [session]);
|
|
285
407
|
|
|
408
|
+
// Persist the badge toggle immediately (it only affects the standalone page).
|
|
409
|
+
const handleBadgeToggle = async (value) => {
|
|
410
|
+
setShowBadge(value);
|
|
411
|
+
const next = { ...(existingConfig || {}), enabled: existingConfig?.enabled || false, showBadge: value, tags: ['portfolio'] };
|
|
412
|
+
setExistingConfig(next);
|
|
413
|
+
await apiPut('/v1/portfolio/config', next);
|
|
414
|
+
};
|
|
415
|
+
|
|
286
416
|
// Toggle helpers
|
|
287
417
|
const toggleSet = (setter, value) => {
|
|
288
418
|
setter(prev => {
|
|
@@ -352,6 +482,7 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
352
482
|
selectedBoards: [...selectedBoards],
|
|
353
483
|
selectedCortex: [...selectedCortex],
|
|
354
484
|
selectedMemories: [...selectedMemories],
|
|
485
|
+
showBadge,
|
|
355
486
|
tags: ['portfolio'],
|
|
356
487
|
});
|
|
357
488
|
};
|
|
@@ -407,6 +538,7 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
407
538
|
authGates: [...authGates],
|
|
408
539
|
publishedAt: new Date().toISOString(),
|
|
409
540
|
htmlSizeKb: sizeKb,
|
|
541
|
+
showBadge,
|
|
410
542
|
tags: ['portfolio'],
|
|
411
543
|
});
|
|
412
544
|
setExistingConfig({ ...(existingConfig || {}), enabled: true });
|
|
@@ -481,7 +613,10 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
481
613
|
<span class="portfolio-published-dot">●</span>
|
|
482
614
|
<span>${t('portfolio.builder.enabled')}</span>
|
|
483
615
|
<a href=${publicUrl} target="_blank" class="portfolio-published-link">${t('portfolio.builder.viewPublic')}</a>
|
|
484
|
-
|
|
616
|
+
${standaloneUrl && html`
|
|
617
|
+
<a href=${standaloneUrl} target="_blank" rel="noopener" class="portfolio-published-link">${tr('portfolio.builder.viewStandalone', 'Own address')} ↗</a>
|
|
618
|
+
`}
|
|
619
|
+
<button class="btn-ghost btn-sm" disabled=${uploading} onClick=${handleUnpublish}>
|
|
485
620
|
${tr('portfolio.builder.unpublish', 'Unpublish')}
|
|
486
621
|
</button>
|
|
487
622
|
</div>
|
|
@@ -684,7 +819,7 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
684
819
|
<h3><span class="portfolio-step-number">4</span> ${t('portfolio.builder.step4Title')}</h3>
|
|
685
820
|
|
|
686
821
|
<div class="portfolio-generate-row">
|
|
687
|
-
<button class="btn
|
|
822
|
+
<button class="btn-primary" disabled=${totalSelected === 0} onClick=${handleGenerate}>
|
|
688
823
|
${t('portfolio.builder.generateBtn')}
|
|
689
824
|
</button>
|
|
690
825
|
${totalSelected === 0 && html`
|
|
@@ -698,7 +833,7 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
698
833
|
<div class="portfolio-prompt-actions">
|
|
699
834
|
<${CopyButton} text=${generatedPrompt} className="btn-primary"
|
|
700
835
|
label=${t('portfolio.builder.copyPrompt')} copiedLabel=${t('portfolio.builder.promptCopied')} />
|
|
701
|
-
<button class="btn
|
|
836
|
+
<button class="btn-ghost" onClick=${handleDownloadPrompt}>
|
|
702
837
|
${t('portfolio.builder.downloadPrompt')}
|
|
703
838
|
</button>
|
|
704
839
|
</div>
|
|
@@ -723,6 +858,17 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
723
858
|
${tr('portfolio.builder.publishTarget', 'Will be published at:')}
|
|
724
859
|
<a href=${publicUrl} target="_blank" class="portfolio-publish-url">${publicUrl}</a>
|
|
725
860
|
</div>
|
|
861
|
+
${standaloneUrl && html`
|
|
862
|
+
<div class="portfolio-publish-target">
|
|
863
|
+
${tr('portfolio.builder.standaloneTarget', 'Also served standalone at:')}
|
|
864
|
+
<a href=${standaloneUrl} target="_blank" rel="noopener" class="portfolio-publish-url">${standaloneUrl}</a>
|
|
865
|
+
</div>
|
|
866
|
+
<div class="portfolio-source-item portfolio-badge-toggle">
|
|
867
|
+
<input type="checkbox" id="pf-show-badge" checked=${showBadge}
|
|
868
|
+
onChange=${(e) => handleBadgeToggle(e.target.checked)} />
|
|
869
|
+
<label for="pf-show-badge">${tr('portfolio.builder.showBadge', 'Show the aimeat.io badge on the standalone page')}</label>
|
|
870
|
+
</div>
|
|
871
|
+
`}
|
|
726
872
|
|
|
727
873
|
<div class="portfolio-upload-zone ${dragover ? 'dragover' : ''}"
|
|
728
874
|
onClick=${() => fileInputRef.current?.click()}
|
|
@@ -750,18 +896,18 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
750
896
|
></textarea>
|
|
751
897
|
${pastedHtml.trim() && html`
|
|
752
898
|
<div class="portfolio-paste-actions">
|
|
753
|
-
<button class="btn
|
|
899
|
+
<button class="btn-primary"
|
|
754
900
|
onClick=${handlePublishPaste} disabled=${uploading}>
|
|
755
901
|
${uploading ? '...' : tr('portfolio.builder.publishPaste', 'Publish pasted HTML')}
|
|
756
902
|
</button>
|
|
757
|
-
<button class="btn
|
|
903
|
+
<button class="btn-ghost" onClick=${() => setShowPreview(p => !p)}>
|
|
758
904
|
${showPreview ? tr('portfolio.builder.closePreview', 'Close preview') : tr('portfolio.builder.previewBtn', 'Preview')}
|
|
759
905
|
</button>
|
|
760
906
|
</div>
|
|
761
907
|
`}
|
|
762
908
|
${showPreview && pastedHtml.trim() && html`
|
|
763
909
|
<!-- Same sandbox as the public viewer: opaque origin, no session access. -->
|
|
764
|
-
<iframe class="portfolio-viewer-frame portfolio-preview-frame" srcdoc=${pastedHtml}
|
|
910
|
+
<iframe class="portfolio-viewer-frame portfolio-preview-frame" srcdoc=${stampCspNonce(pastedHtml)}
|
|
765
911
|
sandbox="allow-scripts"></iframe>
|
|
766
912
|
`}
|
|
767
913
|
|
|
@@ -778,7 +924,7 @@ function PortfolioBuilder({ session, navigate }) {
|
|
|
778
924
|
</div>
|
|
779
925
|
|
|
780
926
|
<div style="margin-top:2rem;">
|
|
781
|
-
<button class="btn
|
|
927
|
+
<button class="btn-ghost" onClick=${() => navigate('/v1/profile')}>
|
|
782
928
|
← ${tr('portfolio.builder.backToProfile', 'Profile')}
|
|
783
929
|
</button>
|
|
784
930
|
</div>
|
|
@@ -792,6 +938,76 @@ function PortfolioViewer({ username, navigate }) {
|
|
|
792
938
|
const [data, setData] = useState(null);
|
|
793
939
|
const [loading, setLoading] = useState(true);
|
|
794
940
|
const [errMsg, setErrMsg] = useState(null);
|
|
941
|
+
const frameRef = useRef(null);
|
|
942
|
+
|
|
943
|
+
// ── Portfolio bridge ──
|
|
944
|
+
// The sandboxed portfolio (opaque origin) cannot see the visitor's session.
|
|
945
|
+
// This trusted parent brokers two things over postMessage:
|
|
946
|
+
// 1. 'aimeat-portfolio-auth' {loggedIn} — posted on frame load + auth change,
|
|
947
|
+
// so HTML-baked [data-auth-required] sections can toggle (convenience).
|
|
948
|
+
// 2. 'aimeat-portfolio-fetch' {gaii, key, id} → 'aimeat-portfolio-fetch-result'
|
|
949
|
+
// {id, ok, gaii, key, value} — the parent fetches the record with the
|
|
950
|
+
// VISITOR's JWT (which never enters the iframe) via the public read route,
|
|
951
|
+
// so 'members'-visibility records reach logged-in viewers only. Requests
|
|
952
|
+
// are allowlisted to the portfolio owner's identities (data.owner_gaiis)
|
|
953
|
+
// so portfolio JS cannot spend the visitor's token on anything else.
|
|
954
|
+
const postAuthState = (win) => {
|
|
955
|
+
// '*' targetOrigin: an opaque-origin frame is not addressable any other way;
|
|
956
|
+
// the payload is just a boolean.
|
|
957
|
+
win?.postMessage({ type: 'aimeat-portfolio-auth', loggedIn: !!getSession() }, '*');
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
useEffect(() => {
|
|
961
|
+
if (!data?.has_html) return undefined;
|
|
962
|
+
const ownerGaiis = Array.isArray(data.owner_gaiis) ? data.owner_gaiis : [];
|
|
963
|
+
|
|
964
|
+
const onAuthChange = () => postAuthState(frameRef.current?.contentWindow);
|
|
965
|
+
|
|
966
|
+
const onMessage = async (e) => {
|
|
967
|
+
const win = frameRef.current?.contentWindow;
|
|
968
|
+
if (!win || e.source !== win) return; // only our portfolio frame
|
|
969
|
+
const d = e.data;
|
|
970
|
+
if (!d || d.type !== 'aimeat-portfolio-fetch') return;
|
|
971
|
+
const reply = (ok, value) => win.postMessage({
|
|
972
|
+
type: 'aimeat-portfolio-fetch-result',
|
|
973
|
+
id: d.id ?? null, ok, gaii: d.gaii ?? null, key: d.key ?? null, value,
|
|
974
|
+
}, '*');
|
|
975
|
+
if (typeof d.gaii !== 'string' || typeof d.key !== 'string' || !ownerGaiis.includes(d.gaii)) {
|
|
976
|
+
reply(false, null);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const s = getSession();
|
|
980
|
+
try {
|
|
981
|
+
const resp = await fetch(`/v1/memory/${encodeURIComponent(d.gaii)}/${encodeURIComponent(d.key)}`,
|
|
982
|
+
s ? { headers: { Authorization: 'Bearer ' + s.jwt } } : undefined);
|
|
983
|
+
const j = await resp.json().catch(() => null);
|
|
984
|
+
const ok = !!(resp.ok && j && j.ok !== false);
|
|
985
|
+
reply(ok, ok ? (j.data?.value ?? null) : null);
|
|
986
|
+
} catch (_) {
|
|
987
|
+
reply(false, null);
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
|
|
991
|
+
window.addEventListener('message', onMessage);
|
|
992
|
+
window.addEventListener('aimeat-auth-change', onAuthChange);
|
|
993
|
+
// aimeat-auth-change fires from the login pill's onLogout BEFORE the async
|
|
994
|
+
// auth.logout() has cleared the session, so getSession() still reads the old
|
|
995
|
+
// state at that moment. The auth lib's own 'login'/'logout' events fire only
|
|
996
|
+
// AFTER the state change — subscribe to those too so the frame flips reliably.
|
|
997
|
+
const authApi = window.AIMEAT?.auth;
|
|
998
|
+
if (authApi?.on) {
|
|
999
|
+
authApi.on('login', onAuthChange);
|
|
1000
|
+
authApi.on('logout', onAuthChange);
|
|
1001
|
+
}
|
|
1002
|
+
return () => {
|
|
1003
|
+
window.removeEventListener('message', onMessage);
|
|
1004
|
+
window.removeEventListener('aimeat-auth-change', onAuthChange);
|
|
1005
|
+
if (authApi?.off) {
|
|
1006
|
+
authApi.off('login', onAuthChange);
|
|
1007
|
+
authApi.off('logout', onAuthChange);
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}, [data]);
|
|
795
1011
|
|
|
796
1012
|
useEffect(() => {
|
|
797
1013
|
fetch(`${NODE_URL}/v1/portfolio/data/${encodeURIComponent(username)}`)
|
|
@@ -818,7 +1034,7 @@ function PortfolioViewer({ username, navigate }) {
|
|
|
818
1034
|
<div class="portfolio-not-found">
|
|
819
1035
|
<h2>${t('portfolio.viewer.notFound')}</h2>
|
|
820
1036
|
<p>${t('portfolio.viewer.notFoundDesc')}</p>
|
|
821
|
-
<button class="btn
|
|
1037
|
+
<button class="btn-ghost" style="margin-top:1rem;" onClick=${() => navigate('/v1/portal')}>
|
|
822
1038
|
${t('portfolio.viewer.backToPortal')}
|
|
823
1039
|
</button>
|
|
824
1040
|
</div>
|
|
@@ -826,25 +1042,32 @@ function PortfolioViewer({ username, navigate }) {
|
|
|
826
1042
|
`;
|
|
827
1043
|
}
|
|
828
1044
|
|
|
829
|
-
// If portfolio HTML exists, render it in a sandboxed iframe
|
|
1045
|
+
// If portfolio HTML exists, render it full-bleed in a sandboxed iframe:
|
|
1046
|
+
// the frame fills the viewport below the nav + toolbar, the portfolio
|
|
1047
|
+
// scrolls inside it. The toolbar offers native fullscreen for a truly
|
|
1048
|
+
// chrome-free view (Esc exits).
|
|
830
1049
|
if (data.has_html && data.portfolio_html) {
|
|
831
1050
|
return html`
|
|
832
|
-
<div class="portfolio-
|
|
833
|
-
<div
|
|
834
|
-
<button class="btn btn-
|
|
1051
|
+
<div class="portfolio-viewer-full">
|
|
1052
|
+
<div class="portfolio-viewer-bar">
|
|
1053
|
+
<button class="btn-ghost btn-sm" onClick=${() => navigate('/v1/portal')}>
|
|
835
1054
|
← ${t('portfolio.viewer.backToPortal')}
|
|
836
1055
|
</button>
|
|
837
|
-
<span
|
|
1056
|
+
<span class="portfolio-viewer-owner">${escHtml(data.display_name || username)}'s portfolio</span>
|
|
1057
|
+
${data.standalone_url && html`
|
|
1058
|
+
<a class="btn-ghost btn-sm" href=${data.standalone_url} target="_blank" rel="noopener">
|
|
1059
|
+
${tr('portfolio.viewer.standalone', 'Own address')} ↗
|
|
1060
|
+
</a>
|
|
1061
|
+
`}
|
|
1062
|
+
<button class="btn-ghost btn-sm portfolio-viewer-fs"
|
|
1063
|
+
title=${tr('portfolio.viewer.fullscreen', 'Fullscreen')}
|
|
1064
|
+
onClick=${() => frameRef.current?.requestFullscreen?.()}>
|
|
1065
|
+
⛶ ${tr('portfolio.viewer.fullscreen', 'Fullscreen')}
|
|
1066
|
+
</button>
|
|
838
1067
|
</div>
|
|
839
|
-
<iframe class="portfolio-viewer-frame"
|
|
840
|
-
sandbox="allow-scripts"
|
|
841
|
-
onLoad=${(e) =>
|
|
842
|
-
try {
|
|
843
|
-
const h = e.target.contentDocument?.body?.scrollHeight;
|
|
844
|
-
if (h) e.target.style.height = h + 40 + 'px';
|
|
845
|
-
} catch (_) { /* cross-origin — ignore */ }
|
|
846
|
-
}}
|
|
847
|
-
></iframe>
|
|
1068
|
+
<iframe ref=${frameRef} class="portfolio-viewer-frame portfolio-viewer-frame-full"
|
|
1069
|
+
srcdoc=${stampCspNonce(data.portfolio_html)} sandbox="allow-scripts"
|
|
1070
|
+
onLoad=${(e) => postAuthState(e.target.contentWindow)}></iframe>
|
|
848
1071
|
</div>
|
|
849
1072
|
`;
|
|
850
1073
|
}
|
|
@@ -855,7 +1078,7 @@ function PortfolioViewer({ username, navigate }) {
|
|
|
855
1078
|
<div class="portfolio-not-found">
|
|
856
1079
|
<h2>${escHtml(data.display_name || username)}</h2>
|
|
857
1080
|
<p>${data.bio ? escHtml(data.bio) : t('portfolio.viewer.notFoundDesc')}</p>
|
|
858
|
-
<button class="btn
|
|
1081
|
+
<button class="btn-ghost" style="margin-top:1rem;" onClick=${() => navigate('/v1/portal')}>
|
|
859
1082
|
${t('portfolio.viewer.backToPortal')}
|
|
860
1083
|
</button>
|
|
861
1084
|
</div>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @description Agent card component with collapsed/expanded states,
|
|
4
4
|
* Two-Zone Header (identity + state-dependent status), and tab bar.
|
|
5
5
|
* @version-history
|
|
6
|
+
* v1.19.0 -- 2026-07-03 -- Add Contracts sub-tab (tab-contracts.js) — agent-side contract-engagement view.
|
|
6
7
|
* v1.18.0 -- 2026-06-30 -- Onboarding step labels use tOr() so a missing agentOnboarding.steps.*
|
|
7
8
|
* key falls back to the server-provided step.title instead of rendering the raw key.
|
|
8
9
|
* v1.17.0 -- 2026-06-24 -- Secretary P5 (S-A): render a "Specialist · <role>" badge for agents tagged
|
|
@@ -71,6 +72,7 @@ import TabIntegration from './tab-integration.js';
|
|
|
71
72
|
import TabTasks from './tab-tasks.js';
|
|
72
73
|
import TabMessages from './tab-messages.js';
|
|
73
74
|
import TabDataAccess from './tab-data-access.js';
|
|
75
|
+
import TabContracts from './tab-contracts.js';
|
|
74
76
|
import TabDirectives from './tab-directives.js';
|
|
75
77
|
import TabAgentConfig from './tab-agent-config.js';
|
|
76
78
|
import TabActivity from './tab-activity.js';
|
|
@@ -94,6 +96,7 @@ const TABS = [
|
|
|
94
96
|
{ id: 'tasks', key: 'profile.agents.detail.tabs.tasks' },
|
|
95
97
|
{ id: 'messages', key: 'profile.agents.detail.tabs.messages' },
|
|
96
98
|
{ id: 'data-access', key: 'profile.agents.detail.tabs.data_access' },
|
|
99
|
+
{ id: 'contracts', key: 'profile.agents.detail.tabs.contracts' },
|
|
97
100
|
{ id: 'quality', key: 'profile.agents.detail.tabs.quality' },
|
|
98
101
|
{ id: 'activity', key: 'profile.agents.detail.tabs.activity' },
|
|
99
102
|
{ id: 'schedules', key: 'profile.agents.detail.tabs.schedules' },
|
|
@@ -695,6 +698,7 @@ function renderTabContent(activeTab, agent, onboarding, session, showToast, allA
|
|
|
695
698
|
case 'tasks': return html`<${TabTasks} ...${props} openTaskId=${openTaskId} openTaskNonce=${openTaskNonce} />`;
|
|
696
699
|
case 'messages': return html`<${TabMessages} ...${props} />`;
|
|
697
700
|
case 'data-access': return html`<${TabDataAccess} ...${props} />`;
|
|
701
|
+
case 'contracts': return html`<${TabContracts} ...${props} />`;
|
|
698
702
|
case 'directives': return html`<${TabDirectives} ...${props} />`;
|
|
699
703
|
case 'agent-config': return html`<${TabAgentConfig} ...${props} onDeleteClick=${onDeleteClick} />`;
|
|
700
704
|
case 'activity': return html`<${TabActivity} ...${props} />`;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file tab-contracts.js
|
|
3
|
+
* @description Agent detail › Contracts sub-tab. Makes the agent's workspace-contract engagements
|
|
4
|
+
* visible from the AGENT side (previously only the workspace knew, and only as a derived record
|
|
5
|
+
* trace). Shows what the agent OFFERS (its contract.* capability tags) and, first-class, WHERE each
|
|
6
|
+
* contract is currently active or was retired — with a Retire off-switch and a Re-adopt action.
|
|
7
|
+
* @structure TabContracts — offered-contracts row + Active engagements + Retired (history)
|
|
8
|
+
* @usage <${TabContracts} agent=${agent} agentName=${agent.name} showToast=${showToast} />
|
|
9
|
+
* @version-history
|
|
10
|
+
* v1.0.0 — 2026-07-03 — Initial agent Contracts sub-tab (engagement visibility + activate/deactivate).
|
|
11
|
+
*/
|
|
12
|
+
import { h } from 'preact';
|
|
13
|
+
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
14
|
+
import htm from 'htm';
|
|
15
|
+
import { onLiveUpdate } from '/lib/live-updates.js';
|
|
16
|
+
import { t } from '/js/i18n.js';
|
|
17
|
+
import { getAgentEngagements, contractNamesOf, offersWorkspaceContract, adoptContractTask } from '/js/services/agents.js';
|
|
18
|
+
import { retireEngagement, activateEngagement } from '/js/services/organisms.js';
|
|
19
|
+
|
|
20
|
+
const html = htm.bind(h);
|
|
21
|
+
|
|
22
|
+
const fmtDay = (iso) => { if (!iso) return ''; const d = new Date(iso); return isNaN(d.getTime()) ? '' : d.toLocaleDateString(); };
|
|
23
|
+
const engKey = (e) => `${e.organism_id}:${e.ws}:${e.contract || ''}`;
|
|
24
|
+
|
|
25
|
+
export default function TabContracts({ agent, agentName, showToast }) {
|
|
26
|
+
const [engagements, setEngagements] = useState([]);
|
|
27
|
+
const [loading, setLoading] = useState(true);
|
|
28
|
+
const [busy, setBusy] = useState('');
|
|
29
|
+
const capabilities = contractNamesOf(agent);
|
|
30
|
+
const advertises = offersWorkspaceContract(agent);
|
|
31
|
+
|
|
32
|
+
const load = useCallback(async () => {
|
|
33
|
+
try { setEngagements(await getAgentEngagements(agentName)); }
|
|
34
|
+
finally { setLoading(false); }
|
|
35
|
+
}, [agentName]);
|
|
36
|
+
const loadRef = useRef(load); loadRef.current = load;
|
|
37
|
+
useEffect(() => { load(); return onLiveUpdate(['organisms'], () => loadRef.current()); }, [load]);
|
|
38
|
+
|
|
39
|
+
// Retire = the off-switch: the agent's loop skips this workspace once its engagement is retired.
|
|
40
|
+
const retire = async (e) => {
|
|
41
|
+
setBusy(engKey(e));
|
|
42
|
+
try {
|
|
43
|
+
const r = await retireEngagement(e.organism_id, e.ws, e.agent, e.contract);
|
|
44
|
+
if (r?.ok === false) showToast?.(r?.error?.message || (t('organisms.retireFailed') || 'Could not retire the contract'), true);
|
|
45
|
+
else showToast?.(t('profile.agents.detail.contracts.retiredToast') || 'Retired — the agent will skip this workspace');
|
|
46
|
+
await load();
|
|
47
|
+
} catch (err) { showToast?.(err?.message || 'Error', true); }
|
|
48
|
+
finally { setBusy(''); }
|
|
49
|
+
};
|
|
50
|
+
// Re-adopt = flip back to active + re-queue the provisioning task, mirroring the workspace panel.
|
|
51
|
+
const readopt = async (e) => {
|
|
52
|
+
setBusy(engKey(e));
|
|
53
|
+
try {
|
|
54
|
+
await activateEngagement(e.organism_id, e.ws, e.agent, e.contract).catch(() => {});
|
|
55
|
+
await adoptContractTask(agentName, { organismId: e.organism_id, ws: e.ws, contract: e.contract });
|
|
56
|
+
showToast?.(t('profile.agents.detail.contracts.readoptedToast') || 'Re-adopted — the agent will pick this workspace back up');
|
|
57
|
+
await load();
|
|
58
|
+
} catch (err) { showToast?.(err?.message || 'Error', true); }
|
|
59
|
+
finally { setBusy(''); }
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
if (loading) return html`<div class="pf-agd-empty">${t('profile.loading') || 'Loading…'}</div>`;
|
|
63
|
+
|
|
64
|
+
const active = engagements.filter(e => e.state === 'active');
|
|
65
|
+
const retired = engagements.filter(e => e.state === 'retired');
|
|
66
|
+
const label = (e) => e.contract || (t('organisms.bareContract') || 'contract');
|
|
67
|
+
|
|
68
|
+
return html`
|
|
69
|
+
<div class="pf-agd-contracts">
|
|
70
|
+
<div class="pf-agd-section-title">${t('profile.agents.detail.contracts.offersTitle') || 'Contracts this agent offers'}</div>
|
|
71
|
+
<div class="section-desc">${t('profile.agents.detail.contracts.offersDesc') || 'The workspace contracts this agent advertises. Owners see these when choosing an agent for a workspace.'}</div>
|
|
72
|
+
<div class="agc-caps">
|
|
73
|
+
${advertises || capabilities.length
|
|
74
|
+
? (capabilities.length ? capabilities : ['']).map(c => html`<span class="badge badge-info pj-mini" key=${c}>${'📜 '}${c || (t('organisms.bareContract') || 'contract')}</span>`)
|
|
75
|
+
: html`<span class="pf-agd-empty">${t('profile.agents.detail.contracts.noOffers') || 'This agent advertises no workspace contract (add a workspace-contract + contract.<id> tag in Data Access).'}</span>`}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="pf-agd-section-title agc-mt">${t('profile.agents.detail.contracts.activeTitle') || 'Active in these workspaces'}</div>
|
|
79
|
+
${active.length ? html`
|
|
80
|
+
<div class="agc-list">
|
|
81
|
+
${active.map(e => html`
|
|
82
|
+
<div class="agc-row" key=${engKey(e)}>
|
|
83
|
+
<span class="agc-where">
|
|
84
|
+
<span class="badge badge-success pj-mini">${'✓ '}${label(e)}</span>
|
|
85
|
+
<strong>${e.wsName || e.ws}</strong>
|
|
86
|
+
<span class="agc-org">${e.organismName || e.organism_id}</span>
|
|
87
|
+
<span class="agc-since">${(t('profile.agents.detail.contracts.since') || 'since {d}').replace('{d}', fmtDay(e.adoptedAt))}</span>
|
|
88
|
+
</span>
|
|
89
|
+
<button class="btn-outline btn-sm" disabled=${busy === engKey(e)}
|
|
90
|
+
title=${t('organisms.retireHint') || 'Stop this agent from working in this workspace — its loop skips it and the chip becomes “retired”. Its past work stays as history.'}
|
|
91
|
+
onClick=${() => retire(e)}>${busy === engKey(e) ? '…' : (t('organisms.retire') || 'Retire')}</button>
|
|
92
|
+
</div>`)}
|
|
93
|
+
</div>`
|
|
94
|
+
: html`<div class="pf-agd-empty">${t('profile.agents.detail.contracts.noneActive') || 'Not active in any workspace yet. Owners adopt this agent from a workspace’s People panel.'}</div>`}
|
|
95
|
+
|
|
96
|
+
${retired.length ? html`
|
|
97
|
+
<div class="pf-agd-section-title agc-mt">${t('profile.agents.detail.contracts.retiredTitle') || 'Retired (history)'}</div>
|
|
98
|
+
<div class="agc-list">
|
|
99
|
+
${retired.map(e => html`
|
|
100
|
+
<div class="agc-row agc-row--retired" key=${engKey(e)}>
|
|
101
|
+
<span class="agc-where">
|
|
102
|
+
<span class="badge badge-muted pj-mini">${label(e)}</span>
|
|
103
|
+
<strong>${e.wsName || e.ws}</strong>
|
|
104
|
+
<span class="agc-org">${e.organismName || e.organism_id}</span>
|
|
105
|
+
<span class="agc-since">${(t('profile.agents.detail.contracts.until') || 'used here until {d}').replace('{d}', fmtDay(e.retiredAt))}</span>
|
|
106
|
+
</span>
|
|
107
|
+
<button class="btn-ghost btn-sm" disabled=${busy === engKey(e)}
|
|
108
|
+
onClick=${() => readopt(e)}>${busy === engKey(e) ? '…' : (t('organisms.reAdopt') || 'Re-adopt')}</button>
|
|
109
|
+
</div>`)}
|
|
110
|
+
</div>` : null}
|
|
111
|
+
</div>`;
|
|
112
|
+
}
|
|
@@ -197,7 +197,7 @@ export function tierLevel(tier) {
|
|
|
197
197
|
function EditProfileModal({ session, onClose, onSaved, onChangePassword }) {
|
|
198
198
|
const [loading, setLoading] = useState(true);
|
|
199
199
|
const [saving, setSaving] = useState(false);
|
|
200
|
-
const [fields, setFields] = useState({ display_name: '', bio: '', avatar: '', locale: 'en' });
|
|
200
|
+
const [fields, setFields] = useState({ display_name: '', bio: '', avatar: '', locale: 'en', directory_listed: false });
|
|
201
201
|
const [currentEmail, setCurrentEmail] = useState('');
|
|
202
202
|
|
|
203
203
|
useEffect(() => {
|
|
@@ -213,6 +213,7 @@ function EditProfileModal({ session, onClose, onSaved, onChangePassword }) {
|
|
|
213
213
|
bio: d.bio || '',
|
|
214
214
|
avatar: d.avatar || '',
|
|
215
215
|
locale: d.locale || 'en',
|
|
216
|
+
directory_listed: d.directory_listed === true,
|
|
216
217
|
});
|
|
217
218
|
setCurrentEmail(d.notification_email || '');
|
|
218
219
|
}
|
|
@@ -299,6 +300,14 @@ function EditProfileModal({ session, onClose, onSaved, onChangePassword }) {
|
|
|
299
300
|
<a href="#" class="pf-edit-link" onClick=${(e) => { e.preventDefault(); onClose(); window.dispatchEvent(new CustomEvent('aimeat-open-tab', { detail: { tabId: 'email' } })); }}>
|
|
300
301
|
${t('profile.landing.editEmailLink') || 'Change in the Email tab →'}</a>
|
|
301
302
|
</div>
|
|
303
|
+
<label class="pf-edit-check">
|
|
304
|
+
<input type="checkbox" checked=${fields.directory_listed}
|
|
305
|
+
onChange=${(e) => set('directory_listed', e.target.checked)} />
|
|
306
|
+
<span>
|
|
307
|
+
<span class="pf-edit-check-title">${t('profile.landing.editDirectoryListed') || 'List me in the member directory'}</span>
|
|
308
|
+
<span class="pf-edit-hint">${t('profile.landing.editDirectoryHint') || 'Off by default. When on, other signed-in members can find you (name, bio, avatar) in the directory. Anonymous visitors never see it.'}</span>
|
|
309
|
+
</span>
|
|
310
|
+
</label>
|
|
302
311
|
</div>
|
|
303
312
|
<div class="pf-edit-footer">
|
|
304
313
|
<a href="#" class="pf-edit-link pf-edit-footer-left" onClick=${(e) => { e.preventDefault(); onChangePassword?.(); }}>
|