aimeat 1.26.0 → 1.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/.env.example +19 -0
- package/dist/locales/en.json +507 -288
- package/dist/locales/fi.json +476 -257
- package/dist/public/css/components/app-sandbox.css +62 -0
- package/dist/public/css/views/app-grant.css +83 -0
- package/dist/public/css/views/inbox.css +30 -0
- package/dist/public/css/views/landing.css +16 -2
- package/dist/public/css/views/living.css +187 -0
- package/dist/public/css/views/notebook.css +89 -0
- package/dist/public/css/views/profile.css +42 -85
- package/dist/public/js/app-sandbox.js +42 -0
- package/dist/public/js/services/generator-prompts-base.js +40 -3
- package/dist/public/js/services/living.js +506 -0
- package/dist/public/js/services/notebook-plan.js +211 -0
- package/dist/public/js/services/notebook.js +149 -0
- package/dist/public/js/services/organisms.js +10 -2
- package/dist/public/js/services/tracked-responses.js +79 -0
- package/dist/public/js/utils.js +15 -0
- package/dist/public/spa.html +20 -1
- package/dist/public/types/aimeat-globals.d.ts +4 -0
- package/dist/public/views/app-grant.js +210 -0
- package/dist/public/views/business.js +8 -1
- package/dist/public/views/how-it-works.js +5 -1
- package/dist/public/views/landing-node-totals.js +71 -0
- package/dist/public/views/landing.js +88 -79
- package/dist/public/views/profile/access-tab.js +75 -0
- package/dist/public/views/profile/agents-tab.js +3 -2
- package/dist/public/views/profile/apps-tab.js +17 -0
- package/dist/public/views/profile/inbox-tab.js +204 -7
- package/dist/public/views/profile/landing-page.js +255 -163
- package/dist/public/views/profile/living-tab.js +480 -0
- package/dist/public/views/profile/notebook-card.js +489 -0
- package/dist/public/views/profile/notebook-helpers.js +48 -0
- package/dist/public/views/profile/notebook-tab.js +40 -260
- package/dist/public/views/profile.js +2 -0
- package/dist/scripts/reassign-apps.d.ts +2 -0
- package/dist/scripts/reassign-apps.d.ts.map +1 -0
- package/dist/scripts/reassign-apps.js +99 -0
- package/dist/scripts/reassign-apps.js.map +1 -0
- package/dist/scripts/screenshot-worker.d.ts +2 -0
- package/dist/scripts/screenshot-worker.d.ts.map +1 -0
- package/dist/scripts/screenshot-worker.js +14 -0
- package/dist/scripts/screenshot-worker.js.map +1 -0
- package/dist/src/auth/jwt.d.ts +2 -0
- package/dist/src/auth/jwt.d.ts.map +1 -1
- package/dist/src/auth/jwt.js +2 -0
- package/dist/src/auth/jwt.js.map +1 -1
- package/dist/src/auth/middleware.d.ts.map +1 -1
- package/dist/src/auth/middleware.js +6 -2
- package/dist/src/auth/middleware.js.map +1 -1
- package/dist/src/cli/init-wizard.d.ts +7 -3
- package/dist/src/cli/init-wizard.d.ts.map +1 -1
- package/dist/src/cli/init-wizard.js +50 -5
- package/dist/src/cli/init-wizard.js.map +1 -1
- package/dist/src/cli/screenshot-worker.d.ts +2 -0
- package/dist/src/cli/screenshot-worker.d.ts.map +1 -0
- package/dist/src/cli/screenshot-worker.js +156 -0
- package/dist/src/cli/screenshot-worker.js.map +1 -0
- package/dist/src/config.d.ts +23 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +28 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +2756 -1285
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +21 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +18 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +1648 -155
- package/dist/src/generated/prisma-postgres/index.js +21 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +21 -0
- package/dist/src/generated/prisma-postgres/wasm.js +21 -3
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/apps.d.ts +2 -0
- package/dist/src/mcp/apps.d.ts.map +1 -1
- package/dist/src/mcp/apps.js +32 -2
- package/dist/src/mcp/apps.js.map +1 -1
- package/dist/src/mcp/extensions.d.ts +1 -0
- package/dist/src/mcp/extensions.d.ts.map +1 -1
- package/dist/src/mcp/extensions.js +10 -1
- package/dist/src/mcp/extensions.js.map +1 -1
- package/dist/src/mcp/workspaces.d.ts.map +1 -1
- package/dist/src/mcp/workspaces.js +4 -1
- package/dist/src/mcp/workspaces.js.map +1 -1
- package/dist/src/middleware/rate-limit.d.ts +10 -0
- package/dist/src/middleware/rate-limit.d.ts.map +1 -1
- package/dist/src/middleware/rate-limit.js +24 -4
- package/dist/src/middleware/rate-limit.js.map +1 -1
- package/dist/src/middleware/subdomain.d.ts +15 -3
- package/dist/src/middleware/subdomain.d.ts.map +1 -1
- package/dist/src/middleware/subdomain.js +35 -12
- package/dist/src/middleware/subdomain.js.map +1 -1
- package/dist/src/routes/admin.js +4 -2
- package/dist/src/routes/admin.js.map +1 -1
- package/dist/src/routes/agent-webhook.d.ts.map +1 -1
- package/dist/src/routes/agent-webhook.js +3 -2
- package/dist/src/routes/agent-webhook.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts +35 -0
- package/dist/src/routes/app-grants.d.ts.map +1 -0
- package/dist/src/routes/app-grants.js +370 -0
- package/dist/src/routes/app-grants.js.map +1 -0
- package/dist/src/routes/apps.d.ts +18 -0
- package/dist/src/routes/apps.d.ts.map +1 -1
- package/dist/src/routes/apps.js +232 -13
- package/dist/src/routes/apps.js.map +1 -1
- package/dist/src/routes/extensions.d.ts +2 -0
- package/dist/src/routes/extensions.d.ts.map +1 -1
- package/dist/src/routes/extensions.js +18 -9
- package/dist/src/routes/extensions.js.map +1 -1
- package/dist/src/routes/foundry.d.ts.map +1 -1
- package/dist/src/routes/foundry.js +13 -1
- package/dist/src/routes/foundry.js.map +1 -1
- package/dist/src/routes/generator.d.ts.map +1 -1
- package/dist/src/routes/generator.js +13 -1
- package/dist/src/routes/generator.js.map +1 -1
- package/dist/src/routes/ghii.d.ts.map +1 -1
- package/dist/src/routes/ghii.js +20 -10
- package/dist/src/routes/ghii.js.map +1 -1
- package/dist/src/routes/librarian.d.ts.map +1 -1
- package/dist/src/routes/librarian.js +64 -2
- package/dist/src/routes/librarian.js.map +1 -1
- package/dist/src/routes/libs.d.ts +15 -0
- package/dist/src/routes/libs.d.ts.map +1 -1
- package/dist/src/routes/libs.js +301 -2
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/living.d.ts +18 -0
- package/dist/src/routes/living.d.ts.map +1 -0
- package/dist/src/routes/living.js +63 -0
- package/dist/src/routes/living.js.map +1 -0
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +4 -1
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/messages.d.ts +2 -0
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +16 -87
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/oauth-login.d.ts +22 -0
- package/dist/src/routes/oauth-login.d.ts.map +1 -0
- package/dist/src/routes/oauth-login.js +219 -0
- package/dist/src/routes/oauth-login.js.map +1 -0
- package/dist/src/routes/organisms.d.ts.map +1 -1
- package/dist/src/routes/organisms.js +4 -1
- package/dist/src/routes/organisms.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 +20 -12
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +3 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -1
- package/dist/src/routes/public-stats.js +48 -0
- package/dist/src/routes/public-stats.js.map +1 -1
- package/dist/src/routes/storage-files.js +10 -10
- package/dist/src/routes/storage-files.js.map +1 -1
- package/dist/src/routes/subdomains.d.ts +33 -0
- package/dist/src/routes/subdomains.d.ts.map +1 -1
- package/dist/src/routes/subdomains.js +182 -9
- package/dist/src/routes/subdomains.js.map +1 -1
- package/dist/src/routes/tracked-responses.d.ts +27 -0
- package/dist/src/routes/tracked-responses.d.ts.map +1 -0
- package/dist/src/routes/tracked-responses.js +160 -0
- package/dist/src/routes/tracked-responses.js.map +1 -0
- package/dist/src/routes/upload.js +8 -0
- package/dist/src/routes/upload.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +33 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +4 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/server-bootstrap/static-files.d.ts +8 -0
- package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
- package/dist/src/server-bootstrap/static-files.js +52 -3
- package/dist/src/server-bootstrap/static-files.js.map +1 -1
- package/dist/src/server.d.ts.map +1 -1
- package/dist/src/server.js +5 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/core-jobs.d.ts.map +1 -1
- package/dist/src/services/core-jobs.js +5 -0
- package/dist/src/services/core-jobs.js.map +1 -1
- package/dist/src/services/event-bus.d.ts +10 -0
- package/dist/src/services/event-bus.d.ts.map +1 -1
- package/dist/src/services/event-bus.js +9 -0
- package/dist/src/services/event-bus.js.map +1 -1
- package/dist/src/services/generator-prompts/resolvers.d.ts +2 -0
- package/dist/src/services/generator-prompts/resolvers.d.ts.map +1 -1
- package/dist/src/services/generator-prompts/resolvers.js +22 -3
- package/dist/src/services/generator-prompts/resolvers.js.map +1 -1
- package/dist/src/services/hooks.js +2 -2
- package/dist/src/services/hooks.js.map +1 -1
- package/dist/src/services/job-seeding.d.ts.map +1 -1
- package/dist/src/services/job-seeding.js +2 -0
- package/dist/src/services/job-seeding.js.map +1 -1
- package/dist/src/services/living-author-prompt.d.ts +14 -0
- package/dist/src/services/living-author-prompt.d.ts.map +1 -0
- package/dist/src/services/living-author-prompt.js +47 -0
- package/dist/src/services/living-author-prompt.js.map +1 -0
- package/dist/src/services/living-author.d.ts +55 -0
- package/dist/src/services/living-author.d.ts.map +1 -0
- package/dist/src/services/living-author.js +91 -0
- package/dist/src/services/living-author.js.map +1 -0
- package/dist/src/services/living-pulse.d.ts +29 -0
- package/dist/src/services/living-pulse.d.ts.map +1 -0
- package/dist/src/services/living-pulse.js +363 -0
- package/dist/src/services/living-pulse.js.map +1 -0
- package/dist/src/services/message-send.d.ts +24 -0
- package/dist/src/services/message-send.d.ts.map +1 -0
- package/dist/src/services/message-send.js +114 -0
- package/dist/src/services/message-send.js.map +1 -0
- package/dist/src/services/notebook-ai.d.ts +36 -0
- package/dist/src/services/notebook-ai.d.ts.map +1 -0
- package/dist/src/services/notebook-ai.js +51 -0
- package/dist/src/services/notebook-ai.js.map +1 -0
- package/dist/src/services/notebook-classify.d.ts +32 -5
- package/dist/src/services/notebook-classify.d.ts.map +1 -1
- package/dist/src/services/notebook-classify.js +59 -43
- package/dist/src/services/notebook-classify.js.map +1 -1
- package/dist/src/services/notebook-distribute-prompt.d.ts +16 -0
- package/dist/src/services/notebook-distribute-prompt.d.ts.map +1 -0
- package/dist/src/services/notebook-distribute-prompt.js +40 -0
- package/dist/src/services/notebook-distribute-prompt.js.map +1 -0
- package/dist/src/services/notebook-plan-prompt.d.ts +19 -0
- package/dist/src/services/notebook-plan-prompt.d.ts.map +1 -0
- package/dist/src/services/notebook-plan-prompt.js +54 -0
- package/dist/src/services/notebook-plan-prompt.js.map +1 -0
- package/dist/src/services/notebook-plan.d.ts +70 -0
- package/dist/src/services/notebook-plan.d.ts.map +1 -0
- package/dist/src/services/notebook-plan.js +118 -0
- package/dist/src/services/notebook-plan.js.map +1 -0
- package/dist/src/services/oidc-client.d.ts +3 -0
- package/dist/src/services/oidc-client.d.ts.map +1 -1
- package/dist/src/services/oidc-client.js +17 -2
- package/dist/src/services/oidc-client.js.map +1 -1
- package/dist/src/services/prompt-defaults.d.ts.map +1 -1
- package/dist/src/services/prompt-defaults.js +30 -0
- package/dist/src/services/prompt-defaults.js.map +1 -1
- package/dist/src/services/quota.d.ts +14 -1
- package/dist/src/services/quota.d.ts.map +1 -1
- package/dist/src/services/quota.js +28 -1
- package/dist/src/services/quota.js.map +1 -1
- package/dist/src/services/scheduler.d.ts +5 -0
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +4 -0
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/screenshot-capture.d.ts +28 -0
- package/dist/src/services/screenshot-capture.d.ts.map +1 -0
- package/dist/src/services/screenshot-capture.js +154 -0
- package/dist/src/services/screenshot-capture.js.map +1 -0
- package/dist/src/services/track-registry.d.ts +29 -0
- package/dist/src/services/track-registry.d.ts.map +1 -0
- package/dist/src/services/track-registry.js +61 -0
- package/dist/src/services/track-registry.js.map +1 -0
- package/dist/src/services/tracked-response.d.ts +98 -0
- package/dist/src/services/tracked-response.d.ts.map +1 -0
- package/dist/src/services/tracked-response.js +304 -0
- package/dist/src/services/tracked-response.js.map +1 -0
- package/dist/src/services/webhook-dispatcher.d.ts.map +1 -1
- package/dist/src/services/webhook-dispatcher.js +3 -2
- package/dist/src/services/webhook-dispatcher.js.map +1 -1
- package/dist/src/storage/interface.d.ts +19 -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 +16 -1
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +123 -0
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +10 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +119 -6
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/identity.d.ts +1 -0
- package/dist/src/storage/providers/sqlite/repos/identity.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/identity.js +10 -4
- package/dist/src/storage/providers/sqlite/repos/identity.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts +16 -3
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +31 -4
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/app-grant.repository.d.ts +22 -0
- package/dist/src/storage/repositories/app-grant.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/app-grant.repository.js +2 -0
- package/dist/src/storage/repositories/app-grant.repository.js.map +1 -0
- package/dist/src/storage/repositories/app.repository.d.ts +9 -0
- package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/identity.repository.d.ts +1 -0
- package/dist/src/storage/repositories/identity.repository.d.ts.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/utils/env-config.d.ts +5 -2
- package/dist/src/utils/env-config.d.ts.map +1 -1
- package/dist/src/utils/env-config.js +63 -2
- package/dist/src/utils/env-config.js.map +1 -1
- package/dist/src/utils/env-validator.d.ts +6 -3
- package/dist/src/utils/env-validator.d.ts.map +1 -1
- package/dist/src/utils/env-validator.js +14 -3
- package/dist/src/utils/env-validator.js.map +1 -1
- package/dist/src/utils/url-validator.d.ts +15 -0
- package/dist/src/utils/url-validator.d.ts.map +1 -1
- package/dist/src/utils/url-validator.js +120 -30
- package/dist/src/utils/url-validator.js.map +1 -1
- package/dist/static/app-catalog.html +505 -30
- package/dist/static/app-login.js +67 -0
- package/dist/static/app-silent.html +14 -0
- package/dist/static/app-silent.js +32 -0
- package/package.json +3 -1
- package/prisma/schema.postgres.prisma +39 -18
- package/prisma/schema.prisma +21 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file app-grant.js
|
|
3
|
+
* @description Trusted consent page (apex origin) for the H-2 app-grant flow. An app on the
|
|
4
|
+
* isolated app origin sends the owner here (GET /v1/app-grants/authorize → 302 → /v1/app-grant
|
|
5
|
+
* ?req=...). The owner, authenticated on aimeat.io, reviews the requesting app + the exact
|
|
6
|
+
* scopes and Allows or Denies. On Allow we POST /v1/app-grants/authorize-consent and get back the
|
|
7
|
+
* app's redirect_url carrying a one-time code. Two delivery modes:
|
|
8
|
+
* • web_message (popup): postMessage { type:'aimeat_app_grant', code, state } to the app
|
|
9
|
+
* popup-opener (targeted at the exact app origin) and close — the user stays in the app.
|
|
10
|
+
* • query (full redirect, legacy): navigate to redirect_url.
|
|
11
|
+
* The app never sees the session, only the scoped, revocable grant token it then exchanges.
|
|
12
|
+
* "Advanced" lets the owner grant a SUBSET of the requested scopes (never more).
|
|
13
|
+
* @structure default export AppGrant() — loads the pending request, renders the trust prompt +
|
|
14
|
+
* scopes (+ advanced per-scope checkboxes), Allow/Deny.
|
|
15
|
+
* @usage routed at /v1/app-grant?req=<id> by spa.html
|
|
16
|
+
* @version-history
|
|
17
|
+
* v1.0.0 — 2026-06-20 — Initial (H-2 app-origin isolation, Phase 3: consent page).
|
|
18
|
+
* v1.1.0 — 2026-06-20 — Popup (web_message) delivery + "trust this app" reframe + Advanced
|
|
19
|
+
* per-scope subset selection (consent flow wired to apps via the SDK).
|
|
20
|
+
*/
|
|
21
|
+
import { h } from 'preact';
|
|
22
|
+
import { useState, useEffect } from 'preact/hooks';
|
|
23
|
+
import htm from 'htm';
|
|
24
|
+
import { t } from '/js/i18n.js';
|
|
25
|
+
import { api } from '/js/api.js';
|
|
26
|
+
import { escHtml } from '/js/utils.js';
|
|
27
|
+
|
|
28
|
+
const html = htm.bind(h);
|
|
29
|
+
const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
|
|
30
|
+
|
|
31
|
+
export default function AppGrant() {
|
|
32
|
+
const [state, setState] = useState({ status: 'loading', request: null, error: '' });
|
|
33
|
+
const [submitting, setSubmitting] = useState(false);
|
|
34
|
+
const [advanced, setAdvanced] = useState(false);
|
|
35
|
+
const [selected, setSelected] = useState(() => new Set());
|
|
36
|
+
const [existingGrant, setExistingGrant] = useState(null); // the grant this app already holds (manage mode)
|
|
37
|
+
|
|
38
|
+
const requestId = new URLSearchParams(window.location.search).get('req') || '';
|
|
39
|
+
const [authed, setAuthed] = useState(() => !!window.AIMEAT?.auth?.hasSession);
|
|
40
|
+
|
|
41
|
+
// Become reactive to login: the consent popup may open with no one logged in (login_required), and
|
|
42
|
+
// the user signs in right here (auth.showLoginModal) — flip to the consent view on success.
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const onLogin = () => setAuthed(true);
|
|
45
|
+
window.AIMEAT?.auth?.on?.('login', onLogin);
|
|
46
|
+
return () => window.AIMEAT?.auth?.off?.('login', onLogin);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
function doLogin() {
|
|
50
|
+
if (window.AIMEAT?.auth?.showLoginModal) window.AIMEAT.auth.showLoginModal({ onLogin: () => setAuthed(true) });
|
|
51
|
+
else window.location.href = '/v1/profile';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!requestId) { setState({ status: 'error', error: tr('appGrant.missing', 'No authorization request.') }); return; }
|
|
56
|
+
if (!authed) { setState({ status: 'login' }); return; }
|
|
57
|
+
let live = true;
|
|
58
|
+
api(`/v1/app-grants/request/${encodeURIComponent(requestId)}`)
|
|
59
|
+
.then(async (res) => {
|
|
60
|
+
if (!live) return;
|
|
61
|
+
// If this app already holds a grant, this is "manage mode": pre-check the currently granted
|
|
62
|
+
// scopes and offer Revoke. Best-effort — a failed lookup just falls back to first-consent.
|
|
63
|
+
let grant = null;
|
|
64
|
+
try {
|
|
65
|
+
const list = await api('/v1/app-grants');
|
|
66
|
+
grant = (list.data?.grants || []).find((g) => g.app === res.data.app) || null;
|
|
67
|
+
} catch { /* ignore */ }
|
|
68
|
+
if (!live) return;
|
|
69
|
+
const reqScopes = (res.data.scopes || []).map((s) => s.scope);
|
|
70
|
+
const granted = grant ? grant.scopes : reqScopes;
|
|
71
|
+
setExistingGrant(grant);
|
|
72
|
+
setSelected(new Set(granted));
|
|
73
|
+
// Pass-through: when NOT explicitly managing (the gear) and the app already holds a grant that
|
|
74
|
+
// covers what it's asking for, just approve silently — no second prompt for an app you trust.
|
|
75
|
+
const covers = grant && reqScopes.every((s) => grant.scopes.includes(s));
|
|
76
|
+
if (grant && !res.data.manage && covers) { setState({ status: 'autoapprove', request: res.data }); return; }
|
|
77
|
+
if (grant) setAdvanced(true); // manage / add-scopes → show the per-scope checkboxes up front
|
|
78
|
+
setState({ status: 'ready', request: res.data });
|
|
79
|
+
})
|
|
80
|
+
.catch((e) => { if (live) setState({ status: 'error', error: e.message || tr('appGrant.expired', 'This request has expired.') }); });
|
|
81
|
+
return () => { live = false; };
|
|
82
|
+
}, [requestId, authed]);
|
|
83
|
+
|
|
84
|
+
// Pass-through approval (already-granted app, not managing): approve once, silently.
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (state.status === 'autoapprove' && !submitting) approve();
|
|
87
|
+
// eslint-disable-next-line
|
|
88
|
+
}, [state.status]);
|
|
89
|
+
|
|
90
|
+
async function revoke() {
|
|
91
|
+
if (!existingGrant) return;
|
|
92
|
+
setSubmitting(true);
|
|
93
|
+
try {
|
|
94
|
+
await api(`/v1/app-grants/${encodeURIComponent(existingGrant.grant_id)}`, { method: 'DELETE' });
|
|
95
|
+
if (state.request?.response_mode === 'web_message' && window.opener) {
|
|
96
|
+
window.opener.postMessage({ type: 'aimeat_app_grant', revoked: true, state: state.request.state }, state.request.app_origin);
|
|
97
|
+
window.close();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
window.location.href = state.request?.app_origin || '/v1/profile';
|
|
101
|
+
} catch (e) {
|
|
102
|
+
setState((s) => ({ ...s, status: 'error', error: e.message || 'Failed to revoke.' }));
|
|
103
|
+
setSubmitting(false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function toggle(scope) {
|
|
108
|
+
setSelected((prev) => { const n = new Set(prev); if (n.has(scope)) n.delete(scope); else n.add(scope); return n; });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function approve() {
|
|
112
|
+
if (selected.size === 0) return;
|
|
113
|
+
setSubmitting(true);
|
|
114
|
+
try {
|
|
115
|
+
const res = await api('/v1/app-grants/authorize-consent', {
|
|
116
|
+
method: 'POST', body: JSON.stringify({ request_id: requestId, scopes: [...selected] }),
|
|
117
|
+
});
|
|
118
|
+
const url = new URL(res.data.redirect_url);
|
|
119
|
+
const code = url.searchParams.get('code');
|
|
120
|
+
const st = url.searchParams.get('state');
|
|
121
|
+
// Popup mode: hand the code back to the app (popup-opener) at its exact origin, then close.
|
|
122
|
+
if (state.request?.response_mode === 'web_message' && window.opener) {
|
|
123
|
+
window.opener.postMessage({ type: 'aimeat_app_grant', code, state: st }, state.request.app_origin);
|
|
124
|
+
window.close();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
window.location.href = res.data.redirect_url; // legacy full-redirect mode
|
|
128
|
+
} catch (e) {
|
|
129
|
+
setState((s) => ({ ...s, status: 'error', error: e.message || 'Failed to approve.' }));
|
|
130
|
+
setSubmitting(false);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function deny() {
|
|
135
|
+
if (state.request?.response_mode === 'web_message' && window.opener) {
|
|
136
|
+
window.opener.postMessage({ type: 'aimeat_app_grant', code: null, state: null }, state.request.app_origin);
|
|
137
|
+
window.close();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const origin = state.request?.app_origin;
|
|
141
|
+
window.location.href = origin || '/v1/profile';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (state.status === 'loading' || state.status === 'autoapprove') {
|
|
145
|
+
const msg = state.status === 'autoapprove' ? tr('appGrant.signingIn', 'Signing you in…') : tr('common.loading', 'Loading…');
|
|
146
|
+
return html`<div class="agr-wrap"><p class="agr-muted">${msg}</p></div>`;
|
|
147
|
+
}
|
|
148
|
+
if (state.status === 'login') {
|
|
149
|
+
return html`
|
|
150
|
+
<div class="agr-wrap">
|
|
151
|
+
<div class="agr-card">
|
|
152
|
+
<h1 class="agr-title">${tr('appGrant.loginTitle', 'Log in to continue')}</h1>
|
|
153
|
+
<p class="agr-muted">${tr('appGrant.loginBody', 'Log in on aimeat.io, then re-open the app’s link to approve access.')}</p>
|
|
154
|
+
<button class="btn-primary agr-btn" onClick=${doLogin}>${tr('appGrant.loginCta', 'Log in')}</button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>`;
|
|
157
|
+
}
|
|
158
|
+
if (state.status === 'error') {
|
|
159
|
+
return html`
|
|
160
|
+
<div class="agr-wrap">
|
|
161
|
+
<div class="agr-card">
|
|
162
|
+
<h1 class="agr-title">${tr('appGrant.errorTitle', 'Cannot grant access')}</h1>
|
|
163
|
+
<p class="agr-muted">${escHtml(state.error)}</p>
|
|
164
|
+
<button class="btn-outline agr-btn" onClick=${deny}>${tr('common.back', 'Back')}</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const req = state.request;
|
|
170
|
+
return html`
|
|
171
|
+
<div class="agr-wrap">
|
|
172
|
+
<div class="agr-card">
|
|
173
|
+
<span class="agr-badge">${tr('appGrant.externalBadge', 'External app')}</span>
|
|
174
|
+
<h1 class="agr-title">${existingGrant ? tr('appGrant.manageTitle', 'Manage this app’s access') : tr('appGrant.trustTitle', 'Trust this app?')}</h1>
|
|
175
|
+
<div class="agr-app">
|
|
176
|
+
<div class="agr-app-name">${escHtml(req.app_name)}</div>
|
|
177
|
+
<div class="agr-app-origin">${escHtml(req.app_origin)}</div>
|
|
178
|
+
</div>
|
|
179
|
+
<p class="agr-muted">${tr('appGrant.trustIntro', 'This app is not yours. It gets its OWN scoped, revocable key — never your login session. You can revoke it anytime in Profile › Access.')}</p>
|
|
180
|
+
|
|
181
|
+
<div class="agr-scopes-label">${tr('appGrant.needsLabel', 'This app needs:')}</div>
|
|
182
|
+
<ul class="agr-scopes">
|
|
183
|
+
${req.scopes.map((s) => html`
|
|
184
|
+
<li class="agr-scope" key=${s.scope}>
|
|
185
|
+
${advanced && html`
|
|
186
|
+
<input type="checkbox" class="agr-scope-check" checked=${selected.has(s.scope)}
|
|
187
|
+
onChange=${() => toggle(s.scope)} aria-label=${s.scope} />`}
|
|
188
|
+
<span class="agr-scope-text">
|
|
189
|
+
<span class="agr-scope-desc">${escHtml(s.description || s.scope)}</span>
|
|
190
|
+
<span class="agr-scope-name">${escHtml(s.scope)}</span>
|
|
191
|
+
</span>
|
|
192
|
+
</li>`)}
|
|
193
|
+
</ul>
|
|
194
|
+
|
|
195
|
+
<button class="agr-advanced-toggle" onClick=${() => setAdvanced((v) => !v)}>
|
|
196
|
+
${advanced ? tr('appGrant.advancedHide', '▲ Hide advanced') : tr('appGrant.advancedShow', '⚙ Advanced — choose permissions')}
|
|
197
|
+
</button>
|
|
198
|
+
|
|
199
|
+
<div class="agr-actions">
|
|
200
|
+
${existingGrant
|
|
201
|
+
? html`<button class="btn-danger agr-btn" onClick=${revoke} disabled=${submitting}>${tr('appGrant.revoke', 'Revoke access')}</button>`
|
|
202
|
+
: html`<button class="btn-outline agr-btn" onClick=${deny} disabled=${submitting}>${tr('appGrant.deny', 'Don’t trust')}</button>`}
|
|
203
|
+
<button class="btn-primary agr-btn" onClick=${approve} disabled=${submitting || selected.size === 0}>
|
|
204
|
+
${submitting ? tr('appGrant.approving', 'Allowing…') : (existingGrant ? tr('appGrant.saveCta', 'Save changes') : tr('appGrant.trustCta', '❤ Trust — allow access'))}
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
${!existingGrant && html`<p class="agr-next">${tr('appGrant.nextNote', 'Next time this app logs you in automatically — no prompt.')}</p>`}
|
|
208
|
+
</div>
|
|
209
|
+
</div>`;
|
|
210
|
+
}
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* @usage routed at /v1/business by spa.html
|
|
10
10
|
* @version-history
|
|
11
11
|
* v1.0.0 — 2026-06-10 — Initial (owner spec).
|
|
12
|
+
* v1.1.0 — 2026-06-20 — H-2: published-app proof links open in a sandboxed opaque-origin
|
|
13
|
+
* iframe (openAppSandboxed) instead of a top-level apex ?mode=inline tab.
|
|
12
14
|
*/
|
|
13
15
|
import { h } from 'preact';
|
|
14
16
|
import { useState, useEffect } from 'preact/hooks';
|
|
@@ -17,6 +19,7 @@ const html = htm.bind(h);
|
|
|
17
19
|
import { t } from '/js/i18n.js';
|
|
18
20
|
import { escHtml } from '/js/utils.js';
|
|
19
21
|
import { ContactCard } from '/components/ContactCard.js';
|
|
22
|
+
import { openAppSandboxed, isAppHtmlUrl } from '/js/app-sandbox.js';
|
|
20
23
|
|
|
21
24
|
const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
|
|
22
25
|
const CONTACT = 'mailto:jouni.miikki@aimeat.io';
|
|
@@ -42,7 +45,11 @@ export default function Business({ navigate }) {
|
|
|
42
45
|
<p class="ld-case-text">${text}</p>
|
|
43
46
|
${quote && html`<blockquote class="ld-case-quote">“${quote}”<footer>— ${quoteBy}</footer></blockquote>`}
|
|
44
47
|
${proofHref
|
|
45
|
-
|
|
48
|
+
// H-2: published-app proof links open in a sandboxed opaque-origin iframe, never a
|
|
49
|
+
// top-level apex document. Non-app evidence (public viewers, external sites) opens normally.
|
|
50
|
+
? (isAppHtmlUrl(proofHref)
|
|
51
|
+
? html`<a class="ld-case-proof" href="#" role="button" onClick=${(e) => { e.preventDefault(); openAppSandboxed(proofHref, proofLabel); }}>${proofLabel}</a>`
|
|
52
|
+
: html`<a class="ld-case-proof" href=${proofHref} target="_blank" rel="noopener">${proofLabel}</a>`)
|
|
46
53
|
: proofNote && html`<div class="ld-case-proofnote">${proofNote}</div>`}
|
|
47
54
|
<a class="ld-path-cta" href=${ctaHref}>${ctaLabel}</a>
|
|
48
55
|
</div>
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @usage routed at /v1/how-it-works by spa.html
|
|
9
9
|
* @version-history
|
|
10
10
|
* v1.0.0 — 2026-06-10 — Initial (owner spec).
|
|
11
|
+
* v1.1.0 — 2026-06-20 — H-2: the live-example app link opens in a sandboxed opaque-origin
|
|
12
|
+
* iframe (openAppSandboxed) instead of a top-level apex ?mode=inline tab.
|
|
11
13
|
*/
|
|
12
14
|
import { h } from 'preact';
|
|
13
15
|
import { useState, useEffect } from 'preact/hooks';
|
|
@@ -15,6 +17,7 @@ import htm from 'htm';
|
|
|
15
17
|
const html = htm.bind(h);
|
|
16
18
|
import { t } from '/js/i18n.js';
|
|
17
19
|
import { escHtml } from '/js/utils.js';
|
|
20
|
+
import { openAppSandboxed } from '/js/app-sandbox.js';
|
|
18
21
|
|
|
19
22
|
const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
|
|
20
23
|
|
|
@@ -144,7 +147,8 @@ export default function HowItWorks({ navigate }) {
|
|
|
144
147
|
<div class="ld-stats-own">
|
|
145
148
|
${tr('hiw.exampleText', 'AIMEAT Sanomat writes itself every evening. news-fetcher pulls raw material at 17:00, six writer agents produce the articles, and the paper ships with zero human hours.')}
|
|
146
149
|
${paper && html`
|
|
147
|
-
<a class="ld-stats-cta" href=${
|
|
150
|
+
<a class="ld-stats-cta" href="#" role="button" onClick=${(e) => { e.preventDefault();
|
|
151
|
+
openAppSandboxed(`/v1/apps/${encodeURIComponent(paper.owner)}/${encodeURIComponent(paper.filename)}?mode=inline`, paper.filename); }}>
|
|
148
152
|
${tr('hiw.exampleCta', "Read tonight's paper →")}</a>`}
|
|
149
153
|
</div>
|
|
150
154
|
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file landing-node-totals.js
|
|
3
|
+
* @description Landing-page "Happening on this node" panel. Replaces the previously
|
|
4
|
+
* often-empty 3-tab activity feed with cumulative, never-empty node counters:
|
|
5
|
+
* public apps, public organisms, connected agents (+ online now), public knowledge
|
|
6
|
+
* packages, and total app downloads. One fetch of GET /v1/public/node-totals on
|
|
7
|
+
* mount, refreshed on the aimeat-live-update window event and on a slow interval.
|
|
8
|
+
* Rationale: an empty feed reads as "broken"; counts always show the node is alive.
|
|
9
|
+
* @structure default export NodeTotals() — sibling component imported by landing.js.
|
|
10
|
+
* @usage import NodeTotals from './landing-node-totals.js'; <${NodeTotals} />
|
|
11
|
+
* @version-history
|
|
12
|
+
* v1.0.0 — 2026-06-20 — Initial: cumulative node counters replacing the activity feed.
|
|
13
|
+
*/
|
|
14
|
+
import { h } from 'preact';
|
|
15
|
+
import { useState, useEffect } from 'preact/hooks';
|
|
16
|
+
import htm from 'htm';
|
|
17
|
+
const html = htm.bind(h);
|
|
18
|
+
import { t } from '/js/i18n.js';
|
|
19
|
+
|
|
20
|
+
const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
|
|
21
|
+
const num = (n) => (Number(n) || 0).toLocaleString();
|
|
22
|
+
|
|
23
|
+
export default function NodeTotals() {
|
|
24
|
+
const [totals, setTotals] = useState(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let alive = true;
|
|
28
|
+
const load = () => {
|
|
29
|
+
fetch('/v1/public/node-totals')
|
|
30
|
+
.then(r => r.json())
|
|
31
|
+
.then(j => { if (alive && j && j.ok !== false && j.data) setTotals(j.data); })
|
|
32
|
+
.catch(() => { /* keep last value — panel renders a graceful placeholder */ });
|
|
33
|
+
};
|
|
34
|
+
load();
|
|
35
|
+
const onLive = () => load();
|
|
36
|
+
window.addEventListener('aimeat-live-update', onLive);
|
|
37
|
+
const iv = setInterval(load, 60_000);
|
|
38
|
+
return () => {
|
|
39
|
+
alive = false;
|
|
40
|
+
window.removeEventListener('aimeat-live-update', onLive);
|
|
41
|
+
clearInterval(iv);
|
|
42
|
+
};
|
|
43
|
+
}, []);
|
|
44
|
+
|
|
45
|
+
const d = totals || {};
|
|
46
|
+
const cards = [
|
|
47
|
+
{ icon: '🎮', value: d.apps, label: tr('landing.totalApps', 'public apps') },
|
|
48
|
+
{ icon: '🧬', value: d.organisms, label: tr('landing.totalOrganisms', 'organisms') },
|
|
49
|
+
{
|
|
50
|
+
icon: '🤖', value: d.agents, label: tr('landing.totalAgents', 'agents'),
|
|
51
|
+
sub: d.agents_online > 0 ? `${num(d.agents_online)} ${tr('landing.totalOnline', 'online now')}` : null,
|
|
52
|
+
},
|
|
53
|
+
{ icon: '📚', value: d.knowledge_packages, label: tr('landing.totalKnowledge', 'knowledge packages') },
|
|
54
|
+
{ icon: '⬇️', value: d.downloads, label: tr('landing.totalDownloads', 'app downloads') },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
return html`
|
|
58
|
+
<section class="pa nt">
|
|
59
|
+
<h2 class="pa-title">${tr('landing.feedTitle', 'Happening on this node')}</h2>
|
|
60
|
+
<p class="pa-sub">${tr('landing.totalsSub', "What's live on this node right now.")}</p>
|
|
61
|
+
<div class="nt-grid">
|
|
62
|
+
${cards.map(c => html`
|
|
63
|
+
<div class="nt-card" key=${c.label}>
|
|
64
|
+
<span class="nt-icon">${c.icon}</span>
|
|
65
|
+
<span class="nt-value">${totals ? num(c.value) : '–'}</span>
|
|
66
|
+
<span class="nt-label">${c.label}</span>
|
|
67
|
+
${c.sub ? html`<span class="nt-sub">${c.sub}</span>` : null}
|
|
68
|
+
</div>`)}
|
|
69
|
+
</div>
|
|
70
|
+
</section>`;
|
|
71
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file landing.js
|
|
3
|
-
* @description Logged-out landing page: reward first, explanation second. Hero is
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* only). Two CTAs (try it free · get your own) → proof gallery → live activity feed →
|
|
3
|
+
* @description Logged-out landing page: reward first, explanation second. Hero is the
|
|
4
|
+
* build-prompt pitch — copy one prompt and your AI builds you a real app on AIMEAT that you
|
|
5
|
+
* own and publish. Two CTAs (copy build prompt · get your own) → LIVE wall of the real apps
|
|
6
|
+
* people published here (manifest-driven, from the apps API) → node totals panel →
|
|
8
7
|
* the 3-step build loop + build prompt → "ask your own AI" → today's node stats +
|
|
9
8
|
* ownership line (the sales close) → footer. Logged-in visitors are forwarded to the
|
|
10
9
|
* profile Home dashboard. No protocol terms (GHII/GAII/CSM/federation) above the fold;
|
|
11
10
|
* a working result does the selling.
|
|
12
|
-
* @structure default export Landing({ navigate }) +
|
|
11
|
+
* @structure default export Landing({ navigate }) + BuildHero/Gallery(live wall)/StatsPanel/BuildAppPrompt/BuildAgentPrompt/AskYourAI
|
|
13
12
|
* @usage routed at /v1/portal (and '/' for browsers) by spa.html
|
|
14
13
|
* @version-history
|
|
15
14
|
* v1.0.0 — 2026-06-10 — Initial: landing/portal split (owner spec).
|
|
@@ -24,6 +23,15 @@
|
|
|
24
23
|
* v2.1.0 — 2026-06-17 — Add BuildAgentPrompt: a copy-paste "build an agent in 10 minutes" prompt
|
|
25
24
|
* for the local crewaimeat fleet (Ollama/Gemma, no keys); Hero "Get your own →" now points to
|
|
26
25
|
* the desktop installer GitHub Release (was /v1/pricing).
|
|
26
|
+
* v3.0.0 — 2026-06-20 — Value-first hero: replace the Sanomat newspaper Hero with BuildHero
|
|
27
|
+
* (copy the build prompt → your AI builds you an app you own + publish); the gallery becomes a
|
|
28
|
+
* LIVE wall of the real apps people published here (manifest-driven from /v1/apps).
|
|
29
|
+
* v3.1.0 — 2026-06-20 — Wall: fixed 3-up grid + filter search; cards show author + publish
|
|
30
|
+
* date/time. Hero subline adds "let your agents keep it running" (Sanomat as the example).
|
|
31
|
+
* v3.2.0 — 2026-06-20 — H-2: wall cards open published apps in a sandboxed opaque-origin
|
|
32
|
+
* iframe (openAppSandboxed) instead of a top-level apex ?mode=inline link.
|
|
33
|
+
* v3.3.0 — 2026-06-20 — Replace PublicActivityFeed (read as broken when empty) with NodeTotals:
|
|
34
|
+
* cumulative "this node has X" counters (apps/organisms/agents+online/knowledge/downloads).
|
|
27
35
|
*/
|
|
28
36
|
import { h } from 'preact';
|
|
29
37
|
import { useState, useEffect } from 'preact/hooks';
|
|
@@ -31,7 +39,8 @@ import htm from 'htm';
|
|
|
31
39
|
const html = htm.bind(h);
|
|
32
40
|
import { t } from '/js/i18n.js';
|
|
33
41
|
import { escHtml } from '/js/utils.js';
|
|
34
|
-
import
|
|
42
|
+
import { openAppSandboxed } from '/js/app-sandbox.js';
|
|
43
|
+
import NodeTotals from './landing-node-totals.js';
|
|
35
44
|
|
|
36
45
|
// t() echoes the key when a translation is missing — fall back to readable English.
|
|
37
46
|
const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
|
|
@@ -67,44 +76,63 @@ function StatsPanel({ navigate }) {
|
|
|
67
76
|
`;
|
|
68
77
|
}
|
|
69
78
|
|
|
70
|
-
/* ──
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
/* ── Live wall — the REAL apps people built with their AI and published to this node (from the
|
|
80
|
+
apps API, manifest-driven). Three per row + a filter. Each card: name · description · who made
|
|
81
|
+
it · when. The proof the loop works: your creation lands on this same wall. ── */
|
|
82
|
+
function fmtPublished(iso) {
|
|
83
|
+
try {
|
|
84
|
+
const d = new Date(iso);
|
|
85
|
+
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
86
|
+
} catch { return ''; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function Gallery() {
|
|
90
|
+
const [apps, setApps] = useState([]);
|
|
91
|
+
const [q, setQ] = useState('');
|
|
74
92
|
useEffect(() => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
.catch(() => { /* curated cards render regardless */ });
|
|
93
|
+
fetch('/v1/apps?sort=popular&limit=60').then(r => r.json())
|
|
94
|
+
.then(j => setApps(j?.data?.apps || []))
|
|
95
|
+
.catch(() => { /* empty state renders */ });
|
|
79
96
|
}, []);
|
|
80
97
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{ name: 'Battleship (Deep Six)', desc: tr('landing.galleryDeepSix', 'Two-player battleship on an agent platform. Bring an opponent — or two browsers.'),
|
|
87
|
-
href: 'https://aimeat.io/v1/apps/anonymous/deep-six.html?mode=inline', badge: tr('landing.twoPlayers', '2 players') },
|
|
88
|
-
{ name: 'Solar System Simulator', desc: tr('landing.gallerySolar', 'Simulate the solar system.'),
|
|
89
|
-
href: 'https://aimeat.io/v1/apps/anonymous/solar-system.html?mode=inline' },
|
|
90
|
-
{ name: 'Math Graph 3D', desc: tr('landing.galleryGraph', 'Plot 3D math functions in the browser.'),
|
|
91
|
-
href: 'https://aimeat.io/v1/apps/anonymous/graph.html?mode=inline' },
|
|
92
|
-
{ name: 'Band Jam', desc: tr('landing.galleryBandJam', 'Make music with your friends, live!'),
|
|
93
|
-
href: 'https://aimeat.io/v1/apps/anonymous/band-jam.html?mode=inline' },
|
|
94
|
-
];
|
|
98
|
+
const ql = q.trim().toLowerCase();
|
|
99
|
+
const shown = !ql ? apps : apps.filter((a) => {
|
|
100
|
+
const m = a.manifest || {};
|
|
101
|
+
return [m.name, m.description, m.authorDisplay, a.owner].some(v => (v || '').toLowerCase().includes(ql));
|
|
102
|
+
});
|
|
95
103
|
|
|
96
104
|
return html`
|
|
97
105
|
<div class="ld-section">
|
|
98
|
-
<h2 class="ld-h2">${tr('landing.
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
<h2 class="ld-h2">${tr('landing.wallTitle', 'Built by people with their AI. Yours goes here too.')}</h2>
|
|
107
|
+
<input class="ld-wall-search" type="search" value=${q}
|
|
108
|
+
onInput=${(e) => setQ(e.target.value)}
|
|
109
|
+
placeholder=${tr('landing.wallSearch', 'Search apps…')}
|
|
110
|
+
aria-label=${tr('landing.wallSearch', 'Search apps')} />
|
|
111
|
+
${shown.length === 0
|
|
112
|
+
? html`<p class="ld-app-desc">${apps.length === 0
|
|
113
|
+
? tr('landing.wallEmpty', 'Be the first — copy the prompt above, build something, and it lands here.')
|
|
114
|
+
: tr('landing.wallNoMatch', 'No apps match your search.')}</p>`
|
|
115
|
+
: html`<div class="ld-gallery">
|
|
116
|
+
${shown.map((a) => {
|
|
117
|
+
const m = a.manifest || {};
|
|
118
|
+
// H-2: open published apps in a sandboxed (opaque-origin) iframe, never as a
|
|
119
|
+
// top-level apex document. Click-to-open instead of an apex href, so middle-/
|
|
120
|
+
// ctrl-click can't bypass it either.
|
|
121
|
+
const href = `/v1/apps/${encodeURIComponent(a.owner)}/${encodeURIComponent(a.filename)}?mode=inline`;
|
|
122
|
+
const desc = (m.description || '').length > 140 ? m.description.slice(0, 140) + '…' : (m.description || '');
|
|
123
|
+
const author = m.authorDisplay || a.owner || tr('landing.wallAnon', 'someone');
|
|
124
|
+
const when = a.created_at ? fmtPublished(a.created_at) : '';
|
|
125
|
+
const open = () => openAppSandboxed(href, m.name || a.filename);
|
|
126
|
+
return html`
|
|
127
|
+
<div key=${a.owner + '/' + a.filename} class="ld-app-card" role="button" tabindex="0"
|
|
128
|
+
onClick=${open} onKeyDown=${(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(); } }}>
|
|
129
|
+
${a.screenshot_url ? html`<img class="ld-app-shot" src=${a.screenshot_url} loading="lazy" alt="" />` : ''}
|
|
130
|
+
<div class="ld-app-name">${m.icon ? escHtml(m.icon) + ' ' : ''}${escHtml(m.name || a.filename)}</div>
|
|
131
|
+
${desc && html`<div class="ld-app-desc">${escHtml(desc)}</div>`}
|
|
132
|
+
<div class="ld-app-meta">${escHtml(author)}${when ? ' · ' + when : ''}</div>
|
|
133
|
+
</div>`;
|
|
134
|
+
})}
|
|
135
|
+
</div>`}
|
|
108
136
|
</div>
|
|
109
137
|
`;
|
|
110
138
|
}
|
|
@@ -232,6 +260,7 @@ function buildLandingAppPrompt(nodeUrl) {
|
|
|
232
260
|
p += 'Always include <meta name="viewport" content="width=device-width, initial-scale=1.0">. Mobile-first, single self-contained HTML file with embedded CSS + JS.\n\n';
|
|
233
261
|
p += '### Important Rules\n';
|
|
234
262
|
p += '- Return the COMPLETE HTML file, not fragments\n';
|
|
263
|
+
p += '- Also write a one-sentence description of what the app does - it is REQUIRED when publishing and shows in the catalogue and on the landing wall\n';
|
|
235
264
|
p += '- Never use literal closing script tags in JS comments or strings\n';
|
|
236
265
|
p += '- Keep it as a single self-contained HTML file\n';
|
|
237
266
|
p += '- Load only the libraries you actually use; load aimeat-auth before libs that need a session\n';
|
|
@@ -381,35 +410,23 @@ function AskYourAI() {
|
|
|
381
410
|
</section>`;
|
|
382
411
|
}
|
|
383
412
|
|
|
384
|
-
/* ── Hero
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
413
|
+
/* ── Hero — value first: AIMEAT is a safe place to build real apps with your AI in minutes, and
|
|
414
|
+
you own + publish them. One copyable prompt is the whole on-ramp; the live wall below is the
|
|
415
|
+
proof that your creation lands on the same shelf as everyone else's. ── */
|
|
416
|
+
function BuildHero() {
|
|
417
|
+
const [copied, setCopied] = useState(false);
|
|
418
|
+
const prompt = buildLandingAppPrompt(window.location.origin);
|
|
419
|
+
const copy = async () => {
|
|
420
|
+
try { await navigator.clipboard.writeText(prompt); setCopied(true); setTimeout(() => setCopied(false), 2000); }
|
|
421
|
+
catch { /* the full prompt is also visible lower on the page */ }
|
|
422
|
+
};
|
|
392
423
|
return html`
|
|
393
424
|
<section class="ld-hero2">
|
|
394
|
-
<p class="ld-hero2-kicker">${tr('landing.
|
|
395
|
-
<h1 class="ld-hero2-title">${tr('landing.
|
|
396
|
-
<
|
|
397
|
-
aria-label=${tr('landing.heroOpenPaper', 'Open tonight’s paper')}>
|
|
398
|
-
${shot ? html`
|
|
399
|
-
<img class="ld-hero2-img" src=${shot} loading="lazy"
|
|
400
|
-
alt=${tr('landing.heroShotAlt', 'AIMEAT Sanomat — tonight’s edition')} />
|
|
401
|
-
` : html`
|
|
402
|
-
<div class="ld-hero2-paper">
|
|
403
|
-
<span class="ld-hero2-masthead">AIMEAT Sanomat</span>
|
|
404
|
-
<span class="ld-hero2-paper-rule"></span>
|
|
405
|
-
<span class="ld-hero2-paper-line">${tr('landing.heroPaperLine', 'Tonight’s edition · written entirely by agents')}</span>
|
|
406
|
-
</div>
|
|
407
|
-
`}
|
|
408
|
-
<span class="ld-hero2-openbar">${tr('landing.cardACta', 'Read tonight’s paper →')}</span>
|
|
409
|
-
</a>
|
|
410
|
-
<p class="ld-hero2-sub">${tr('landing.heroSub2', 'A real app, built and run by AI on AIMEAT — look before you sign up.')}</p>
|
|
425
|
+
<p class="ld-hero2-kicker">${tr('landing.buildHeroKicker', 'A safe place to build with AI.')}</p>
|
|
426
|
+
<h1 class="ld-hero2-title">${tr('landing.buildHeroTitle', 'Build a real app with your AI in minutes, and it’s yours.')}</h1>
|
|
427
|
+
<p class="ld-hero2-sub">${tr('landing.buildHeroSub', 'Copy one prompt into Claude, ChatGPT or any AI. It builds you a working app on AIMEAT, published live and yours to keep. Then let your agents run it for you, the way AIMEAT Sanomat writes itself every evening.')}</p>
|
|
411
428
|
<div class="ld-hero2-cta">
|
|
412
|
-
<
|
|
429
|
+
<button class="btn-primary" type="button" onClick=${copy}>${copied ? tr('landing.buildHeroCopied', 'Copied ✓ — paste into your AI') : tr('landing.buildHeroCopy', 'Copy the build prompt →')}</button>
|
|
413
430
|
<a class="btn-outline" href="https://github.com/miikkij/aimeat-protocol/releases/latest" target="_blank" rel="noopener">${tr('landing.heroGetOwn', 'Get your own →')}</a>
|
|
414
431
|
</div>
|
|
415
432
|
</section>
|
|
@@ -436,24 +453,16 @@ export default function Landing({ navigate }) {
|
|
|
436
453
|
return () => window.removeEventListener('aimeat-auth-change', onAuth);
|
|
437
454
|
}, []);
|
|
438
455
|
|
|
439
|
-
const [apps, setApps] = useState([]);
|
|
440
|
-
// Card A sends the solo visitor to Sanomat — an instant experience alone, no login.
|
|
441
|
-
// (Battleship/Deep Six lives in the gallery with its "2 players" badge.)
|
|
442
|
-
const sanomat = apps.find(a => /sanomat/i.test((a.filename || '') + ' ' + (a.name || '')));
|
|
443
|
-
const tryHref = sanomat
|
|
444
|
-
? `/v1/apps/${encodeURIComponent(sanomat.owner)}/${encodeURIComponent(sanomat.filename)}?mode=inline`
|
|
445
|
-
: 'https://aimeat.io/v1/apps/happydude500001/laimeat-sanomat.html?mode=inline';
|
|
446
|
-
|
|
447
456
|
return html`
|
|
448
457
|
<div class="ld">
|
|
449
|
-
<!-- 1. Hero —
|
|
450
|
-
<${
|
|
458
|
+
<!-- 1. Hero — value first: build a real app with your AI; the live wall below is the proof. -->
|
|
459
|
+
<${BuildHero} />
|
|
451
460
|
|
|
452
|
-
<!-- 2.
|
|
453
|
-
<${Gallery}
|
|
461
|
+
<!-- 2. Live wall — the real apps people built with their AI and published here (yours goes here). -->
|
|
462
|
+
<${Gallery} />
|
|
454
463
|
|
|
455
|
-
<!-- 3.
|
|
456
|
-
<${
|
|
464
|
+
<!-- 3. Node totals — cumulative "this node has X" counters; never empty, always proof it's alive. -->
|
|
465
|
+
<${NodeTotals} />
|
|
457
466
|
|
|
458
467
|
<!-- 4. How building works: the 3-step loop + the copyable build prompt. -->
|
|
459
468
|
<div class="ld-loop">
|