aimeat 1.18.0 → 1.19.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 +22 -1
- package/dist/locales/en.json +32 -0
- package/dist/locales/fi.json +32 -0
- package/dist/public/css/views/agents-detail.css +145 -0
- package/dist/public/css/views/scheduler.css +16 -0
- package/dist/public/js/services/admin.js +3 -0
- package/dist/public/llms-template.txt +170 -2
- package/dist/public/oauth-consent.html +7 -1
- package/dist/public/views/admin/portal-tab.js +94 -9
- package/dist/public/views/profile/agents/agent-card.js +24 -4
- package/dist/public/views/profile/agents/tab-schedules.js +3 -32
- package/dist/public/views/profile/agents/tab-tasks.js +5 -2
- package/dist/public/views/profile/agents-tab.js +86 -3
- package/dist/public/views/profile/agents-tasks-subtab.js +82 -8
- package/dist/public/views/profile/schedule-item.js +191 -0
- package/dist/public/views/profile/scheduler-tab.js +5 -51
- package/dist/src/generated/api-types.d.ts +284 -5
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/mcp/catalog/scopes.js +1 -1
- package/dist/src/mcp/catalog/scopes.js.map +1 -1
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +12 -0
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/routes/agent-tasks.d.ts +5 -1
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +43 -3
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/apps.d.ts +7 -0
- package/dist/src/routes/apps.d.ts.map +1 -1
- package/dist/src/routes/apps.js +50 -36
- package/dist/src/routes/apps.js.map +1 -1
- package/dist/src/routes/bootstrap.d.ts +2 -1
- package/dist/src/routes/bootstrap.d.ts.map +1 -1
- package/dist/src/routes/bootstrap.js +14 -3
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/cortex.d.ts +13 -0
- package/dist/src/routes/cortex.d.ts.map +1 -1
- package/dist/src/routes/cortex.js +211 -0
- package/dist/src/routes/cortex.js.map +1 -1
- package/dist/src/routes/extensions.d.ts +14 -0
- package/dist/src/routes/extensions.d.ts.map +1 -1
- package/dist/src/routes/extensions.js +255 -109
- package/dist/src/routes/extensions.js.map +1 -1
- package/dist/src/routes/lib-header.d.ts +22 -0
- package/dist/src/routes/lib-header.d.ts.map +1 -0
- package/dist/src/routes/lib-header.js +244 -0
- package/dist/src/routes/lib-header.js.map +1 -0
- package/dist/src/routes/libs.d.ts.map +1 -1
- package/dist/src/routes/libs.js +13 -0
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/schedules.d.ts.map +1 -1
- package/dist/src/routes/schedules.js +29 -7
- package/dist/src/routes/schedules.js.map +1 -1
- package/dist/src/routes/site.d.ts.map +1 -1
- package/dist/src/routes/site.js +39 -1
- package/dist/src/routes/site.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +5 -2
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts +15 -0
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +8 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/services/prompt-defaults.d.ts.map +1 -1
- package/dist/src/services/prompt-defaults.js +35 -0
- package/dist/src/services/prompt-defaults.js.map +1 -1
- package/dist/src/services/prompt-seeder.d.ts.map +1 -1
- package/dist/src/services/prompt-seeder.js +6 -2
- package/dist/src/services/prompt-seeder.js.map +1 -1
- package/dist/src/services/scheduler.d.ts +19 -1
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +49 -17
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/site.d.ts +4 -0
- package/dist/src/services/site.d.ts.map +1 -1
- package/dist/src/services/site.js +29 -0
- package/dist/src/services/site.js.map +1 -1
- package/dist/src/storage/interface.d.ts +1 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +5 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +30 -1
- 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 +20 -0
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/agent-task.d.ts +1 -0
- package/dist/src/storage/providers/sqlite/repos/agent-task.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/agent-task.js +5 -1
- package/dist/src/storage/providers/sqlite/repos/agent-task.js.map +1 -1
- package/dist/src/storage/repositories/app.repository.d.ts +25 -0
- package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
- package/dist/src/utils/csp-nonce.d.ts +19 -0
- package/dist/src/utils/csp-nonce.d.ts.map +1 -0
- package/dist/src/utils/csp-nonce.js +25 -0
- package/dist/src/utils/csp-nonce.js.map +1 -0
- package/dist/static/app-catalog.html +936 -221
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,11 +15,16 @@ AIMEAT is an open protocol for AI agent infrastructure. It gives agents (Claude,
|
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<img src="assets/screenshots/portal-landing.png" alt="Portal landing page" width="24%" />
|
|
18
|
-
<img src="assets/screenshots/profile-overview.png" alt="User profile" width="24%" />
|
|
18
|
+
<img src="assets/screenshots/profile-overview.png" alt="User profile with the persistent grouped sidebar" width="24%" />
|
|
19
19
|
<img src="assets/screenshots/admin-dashboard.png" alt="Admin dashboard" width="24%" />
|
|
20
20
|
<img src="assets/screenshots/app-catalogue-aimeatio.png" alt="App catalogue" width="24%" />
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
|
+
<p align="center">
|
|
24
|
+
<img src="assets/screenshots/profile-mobile.png" alt="AIMEAT profile on mobile -- compact gold logged-in pill plus the grouped navigation as an off-canvas drawer" width="280" />
|
|
25
|
+
</p>
|
|
26
|
+
<p align="center"><em>The portal and profile are fully responsive: on mobile the grouped navigation collapses into an off-canvas drawer and the logged-in pill stays reachable.</em></p>
|
|
27
|
+
|
|
23
28
|
### See it in action (5:50)
|
|
24
29
|
|
|
25
30
|
An AI agent connects to an AIMEAT node and reaches full operational readiness autonomously:
|
|
@@ -153,6 +158,12 @@ For MCP-capable runtimes (Claude Desktop, MCP-aware IDEs), run `aimeat connect s
|
|
|
153
158
|
|
|
154
159
|
Claude Pro, ChatGPT Plus, and other MCP-capable AIs connect directly as MCP clients. OpenClaw, Hermes, Claude Code, and Cursor all work. Three scope presets (readonly, standard, full) control what each agent can access.
|
|
155
160
|
|
|
161
|
+
### Connect agent platforms (Dify, n8n, Open WebUI, ...)
|
|
162
|
+
|
|
163
|
+
AIMEAT also bridges agent platforms. A tool like Dify, n8n, or Open WebUI is its own island -- the agents and data you build there can't reach agents anywhere else. Pointing the platform at an AIMEAT node changes that, and it's a one-time **MCP** connection: add an MCP server for the node's `/v1/mcp` (or a scoped `/v2/mcp/agent` surface), authorize once in the browser, and the full AIMEAT toolset appears -- no token pasting, no per-tool wiring. The agent can even run Hello Integration on itself (paste the canonical instruction from your profile -> Agents), then immediately read/write shared memory, storage, and knowledge packages, discover other agents, and hire capabilities.
|
|
164
|
+
|
|
165
|
+
That's the federation play: each platform is an island, and through AIMEAT its agents move what they build onto the shared network -- where it spreads across federated nodes and other agents can find and use it. Walkthrough: [docs/integrations/dify-hello-integration.md](aimeat/docs/integrations/dify-hello-integration.md).
|
|
166
|
+
|
|
156
167
|
### Build apps with AI
|
|
157
168
|
|
|
158
169
|
Tell any AI what you want. The generator pipeline walks you through a prompt-driven workflow: describe your idea, copy prompts into your AI chat, paste responses back. The system validates each component and registers it on your node. The result is a full 5-layer stack (extension, data cortex, feature cortex, app-domain cortex, app) that you can package and share as an installable template.
|
|
@@ -299,6 +310,12 @@ With `AIMEAT_ANONYMOUS=true`, anyone can read and do limited writes without regi
|
|
|
299
310
|
|
|
300
311
|
Each node runs independently with its own identity and portal. Operators customize through CSS themes (`theme.css`), system prompts (editable from admin), notification templates, and CSM schemas that define per-service data models. Run your own node, your own branding.
|
|
301
312
|
|
|
313
|
+
### Custom portal templates
|
|
314
|
+
|
|
315
|
+
The public landing page (`/`) is editable live from the admin **Portal** tab — write the HTML template (with serve-time `{{config:*}}`, `{{memory:portal/*}}`, `{{kv:*}}`, `{{board:*}}` tags), manage portal memory keys and KV pairs, and watch the result render in the inline preview. Following AIMEAT's prompt-driven workflow, the **AI-Assisted Editor** hands you a ready prompt: paste it into any AI chat, paste the JSON bundle it returns back into **Import AI Result**, and your node's front page immediately looks custom. Drop in `<script src="/v1/libs/aimeat-header.js"></script>` and the page also carries the **exact same site header** as the rest of AIMEAT — brand, navigation, theme and language switchers, and the live gold login pill — so visitors can always sign in and reach their profile. Operator templates are trusted: their inline `<script>` runs under the node's CSP via a per-request nonce.
|
|
316
|
+
|
|
317
|
+
<img src="assets/screenshots/portal-admin-editor.png" alt="Admin Portal tab: live preview of the custom landing page, the HTML template editor with tag reference and active-source status, portal memory keys, KV pairs, and the AI-Assisted Editor (Load AI Prompt -> Import AI Result)" width="760" />
|
|
318
|
+
|
|
302
319
|
---
|
|
303
320
|
|
|
304
321
|
## Getting Started
|
|
@@ -375,6 +392,10 @@ cd aimeat && pnpm exec node --env-file=.env.test.sqlite --import tsx test/run-e2
|
|
|
375
392
|
|
|
376
393
|
> The legacy `pnpm test:e2e` (in-memory backend) is deprecated and produces stale failures. Use the SQLite or MongoDB commands instead.
|
|
377
394
|
|
|
395
|
+
### Synthetic agent traces (dev tool)
|
|
396
|
+
|
|
397
|
+
[`aimeat/tools/synthtraces/`](aimeat/tools/synthtraces/) is a small self-play harness that generates synthetic AIMEAT agent-session traces: a *persona* model plays the human owner while an *agent* model drives a real node over REST or MCP, producing task-driven traces you can use to benchmark or fine-tune models on the protocol. It runs on free cloud models (OpenRouter `owl-alpha`) or **fully local** via Ollama (`qwen2.5` + `llama3.2`), and ships an eval that scores protocol-correctness (no hallucinated tools, valid memory keys, task completion, token cost). See [tools/README.md](aimeat/tools/README.md).
|
|
398
|
+
|
|
378
399
|
---
|
|
379
400
|
|
|
380
401
|
## Documentation
|
package/dist/locales/en.json
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"profile.scheduler.newSchedule": "New schedule",
|
|
10
10
|
"profile.scheduler.close": "Close",
|
|
11
11
|
"profile.scheduler.triggered": "Schedule run started",
|
|
12
|
+
"profile.scheduler.runCreated": "Run started — a new task was created",
|
|
13
|
+
"profile.scheduler.runBusy": "No task created — a previous run is still active. Finish, fail, or delete it, then run again.",
|
|
14
|
+
"profile.scheduler.runLimited": "Not run — this schedule's run limit (daily or max) has been reached.",
|
|
15
|
+
"profile.scheduler.runError": "Run failed",
|
|
12
16
|
"profile.scheduler.cancelled": "Schedule cancelled",
|
|
13
17
|
"profile.scheduler.created": "Schedule created",
|
|
14
18
|
"profile.scheduler.saving": "Saving…",
|
|
@@ -80,6 +84,13 @@
|
|
|
80
84
|
"profile.scheduler.budgetDesc": "Default opt-in limits applied to schedules created for this agent.",
|
|
81
85
|
"profile.scheduler.budgetSave": "Save budget guards",
|
|
82
86
|
"profile.scheduler.budgetSaved": "Budget guards saved",
|
|
87
|
+
"profile.scheduler.edit": "Edit",
|
|
88
|
+
"profile.scheduler.save": "Save",
|
|
89
|
+
"profile.scheduler.saved": "Schedule updated",
|
|
90
|
+
"profile.scheduler.dispatches": "Creates each run",
|
|
91
|
+
"profile.scheduler.soon": "soon",
|
|
92
|
+
"profile.scheduler.paused": "paused",
|
|
93
|
+
"profile.scheduler.autoKey": "(auto)",
|
|
83
94
|
"common": {
|
|
84
95
|
"cancel": "Cancel",
|
|
85
96
|
"confirm": "Confirm",
|
|
@@ -347,6 +358,13 @@
|
|
|
347
358
|
},
|
|
348
359
|
"agents": {
|
|
349
360
|
"title": "Your Agents",
|
|
361
|
+
"active": {
|
|
362
|
+
"title": "Running now",
|
|
363
|
+
"empty": "No tasks running right now.",
|
|
364
|
+
"openHint": "Open this task",
|
|
365
|
+
"untitled": "Untitled task",
|
|
366
|
+
"more": "more running (open an agent to see all)"
|
|
367
|
+
},
|
|
350
368
|
"desc": "Agents are AI identities registered under your account, each with their own identity, scopes, tools, tasks, and memory access. Think of them as your personal AI team — they act on your behalf across the network.",
|
|
351
369
|
"connect": "Connect an Automation Agent",
|
|
352
370
|
"connectDesc": "Connect an AI agent to your account using the AIMEAT connector CLI or an agent-capable runtime. The connector is the easiest path because it gives the agent a persistent identity, approved scopes, local skill bundle, and MCP access to AIMEAT tools:",
|
|
@@ -3676,6 +3694,20 @@
|
|
|
3676
3694
|
"portalLbStatus": "Sync Status",
|
|
3677
3695
|
"portalLbSyncNow": "Sync Now",
|
|
3678
3696
|
"portalLbSyncDone": "Portal sync completed.",
|
|
3697
|
+
"portalActiveSource": "Active source",
|
|
3698
|
+
"portalCustomActive": "Custom template",
|
|
3699
|
+
"portalDefaultActive": "Default app (built-in SPA)",
|
|
3700
|
+
"portalDefaultExplain": "No custom template is stored, so visitors see the built-in app. The editor below starts empty. Click \"{action}\" to load the current page's HTML as a starting point, then edit and save it to override the default.",
|
|
3701
|
+
"portalCustomExplain": "A custom template is active and overrides the built-in app. Edit it below and save, or reset to default to bring the built-in app back.",
|
|
3702
|
+
"portalLoadCurrent": "Load current page",
|
|
3703
|
+
"portalLoadCurrentConfirm": "Replace the editor contents with the current page's HTML? Unsaved edits will be lost.",
|
|
3704
|
+
"portalLoadedCurrent": "Loaded current page into the editor — review and click Save Template to apply.",
|
|
3705
|
+
"portalAiBundleExplain": "Paste the JSON bundle the AI produced here. It can include the template, portal memory keys, and KV pairs — they are all applied at once.",
|
|
3706
|
+
"portalAiBundlePlaceholder": "Paste the AI's JSON result here: { \"template\": \"<!DOCTYPE html>...\", \"memory\": { \"portal/about\": \"...\" }, \"kv\": {} }",
|
|
3707
|
+
"portalImportDone": "Imported: {summary}",
|
|
3708
|
+
"portalImportInvalid": "That doesn't look like a valid AI bundle. Expected JSON with a \"template\", \"memory\", or \"kv\" field.",
|
|
3709
|
+
"portalSaveLooksLikeJson": "This looks like an AI JSON bundle, not an HTML template. Paste it into \"Import AI Result\" below instead.",
|
|
3710
|
+
"portalMemorySaved": "Portal memory key saved.",
|
|
3679
3711
|
"cors": "CORS",
|
|
3680
3712
|
"corsTitle": "CORS Origin Management",
|
|
3681
3713
|
"corsNodeDefault": "Node Default Origins",
|
package/dist/locales/fi.json
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
"profile.scheduler.newSchedule": "Uusi ajastus",
|
|
10
10
|
"profile.scheduler.close": "Sulje",
|
|
11
11
|
"profile.scheduler.triggered": "Ajastuksen suoritus käynnistetty",
|
|
12
|
+
"profile.scheduler.runCreated": "Suoritus aloitettu — uusi tehtävä luotiin",
|
|
13
|
+
"profile.scheduler.runBusy": "Tehtävää ei luotu — edellinen suoritus on yhä käynnissä. Päätä, merkitse epäonnistuneeksi tai poista se, ja suorita uudelleen.",
|
|
14
|
+
"profile.scheduler.runLimited": "Ei suoritettu — tämän ajastuksen suoritusraja (päivittäinen tai maksimi) on saavutettu.",
|
|
15
|
+
"profile.scheduler.runError": "Suoritus epäonnistui",
|
|
12
16
|
"profile.scheduler.cancelled": "Ajastus peruttu",
|
|
13
17
|
"profile.scheduler.created": "Ajastus luotu",
|
|
14
18
|
"profile.scheduler.saving": "Tallennetaan…",
|
|
@@ -80,6 +84,13 @@
|
|
|
80
84
|
"profile.scheduler.budgetDesc": "Oletusarvoiset valinnaiset rajat, jotka koskevat tälle agentille luotuja ajastuksia.",
|
|
81
85
|
"profile.scheduler.budgetSave": "Tallenna budjettirajat",
|
|
82
86
|
"profile.scheduler.budgetSaved": "Budjettirajat tallennettu",
|
|
87
|
+
"profile.scheduler.edit": "Muokkaa",
|
|
88
|
+
"profile.scheduler.save": "Tallenna",
|
|
89
|
+
"profile.scheduler.saved": "Ajastus päivitetty",
|
|
90
|
+
"profile.scheduler.dispatches": "Luo joka ajolla",
|
|
91
|
+
"profile.scheduler.soon": "pian",
|
|
92
|
+
"profile.scheduler.paused": "tauolla",
|
|
93
|
+
"profile.scheduler.autoKey": "(auto)",
|
|
83
94
|
"common": {
|
|
84
95
|
"cancel": "Peruuta",
|
|
85
96
|
"confirm": "Vahvista",
|
|
@@ -347,6 +358,13 @@
|
|
|
347
358
|
},
|
|
348
359
|
"agents": {
|
|
349
360
|
"title": "Agenttisi",
|
|
361
|
+
"active": {
|
|
362
|
+
"title": "Käynnissä nyt",
|
|
363
|
+
"empty": "Ei käynnissä olevia taskeja juuri nyt.",
|
|
364
|
+
"openHint": "Avaa tämä taski",
|
|
365
|
+
"untitled": "Nimetön taski",
|
|
366
|
+
"more": "lisää käynnissä (avaa agentti nähdäksesi kaikki)"
|
|
367
|
+
},
|
|
350
368
|
"desc": "Agentit ovat tilillesi rekisteröityjä AI-identiteettejä, joilla on oma identiteetti, rajaukset, työkalut, tehtävät ja muistipääsy. Ajattele heitä henkilökohtaisena AI-tiiminäsi - ne toimivat puolestasi verkossa.",
|
|
351
369
|
"connect": "Yhdistä automaatioagentti",
|
|
352
370
|
"connectDesc": "Yhdistä tekoälyagentti tilillesi AIMEAT-liittimellä tai agenttia ajavassa ympäristössä. Liitin on helpoin polku, koska se antaa agentille pysyvän identiteetin, hyväksytyt rajaukset, paikallisen taitopaketin ja MCP-pääsyn AIMEAT-työkaluihin:",
|
|
@@ -3676,6 +3694,20 @@
|
|
|
3676
3694
|
"portalLbStatus": "Synkronointitila",
|
|
3677
3695
|
"portalLbSyncNow": "Synkronoi nyt",
|
|
3678
3696
|
"portalLbSyncDone": "Portaalin synkronointi valmis.",
|
|
3697
|
+
"portalActiveSource": "Aktiivinen lähde",
|
|
3698
|
+
"portalCustomActive": "Mukautettu mallipohja",
|
|
3699
|
+
"portalDefaultActive": "Oletussovellus (sisäänrakennettu SPA)",
|
|
3700
|
+
"portalDefaultExplain": "Mukautettua mallipohjaa ei ole tallennettu, joten kävijät näkevät sisäänrakennetun sovelluksen. Alla oleva editori on tyhjä. Paina \"{action}\" ladataksesi nykyisen sivun HTML:n lähtökohdaksi, muokkaa sitä ja tallenna korvataksesi oletuksen.",
|
|
3701
|
+
"portalCustomExplain": "Mukautettu mallipohja on käytössä ja korvaa sisäänrakennetun sovelluksen. Muokkaa sitä alla ja tallenna, tai palauta oletukseksi tuodaksesi sisäänrakennetun sovelluksen takaisin.",
|
|
3702
|
+
"portalLoadCurrent": "Lataa nykyinen sivu pohjaksi",
|
|
3703
|
+
"portalLoadCurrentConfirm": "Korvataanko editorin sisältö nykyisen sivun HTML:llä? Tallentamattomat muutokset menetetään.",
|
|
3704
|
+
"portalLoadedCurrent": "Nykyinen sivu ladattu editoriin — tarkista ja paina Tallenna mallipohja ottaaksesi käyttöön.",
|
|
3705
|
+
"portalAiBundleExplain": "Liitä tähän tekoälyn tuottama JSON-paketti. Se voi sisältää mallipohjan, portaalin muistiavaimet ja KV-parit — ne otetaan käyttöön kaikki kerralla.",
|
|
3706
|
+
"portalAiBundlePlaceholder": "Liitä tekoälyn JSON-tulos tähän: { \"template\": \"<!DOCTYPE html>...\", \"memory\": { \"portal/about\": \"...\" }, \"kv\": {} }",
|
|
3707
|
+
"portalImportDone": "Tuotu: {summary}",
|
|
3708
|
+
"portalImportInvalid": "Tämä ei näytä kelvolliselta tekoälypaketilta. Odotettiin JSON, jossa on \"template\", \"memory\" tai \"kv\" -kenttä.",
|
|
3709
|
+
"portalSaveLooksLikeJson": "Tämä näyttää tekoälyn JSON-paketilta, ei HTML-mallipohjalta. Liitä se alla olevaan \"Tuo tekoälyn tulos\" -kenttään.",
|
|
3710
|
+
"portalMemorySaved": "Portaalin muistiavain tallennettu.",
|
|
3679
3711
|
"cors": "CORS",
|
|
3680
3712
|
"corsTitle": "CORS-alkuperähallinta",
|
|
3681
3713
|
"corsNodeDefault": "Solmun oletusalkuperät",
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Uses agd- prefix to avoid collisions with other profile styles.
|
|
5
5
|
* All colors use CSS variables from theme.css.
|
|
6
6
|
* @version-history
|
|
7
|
+
* v1.10.0 -- 2026-06-03 -- Add fleet "running now" panel styles (.pf-agd-active,
|
|
8
|
+
* -head, -empty, -list, -row, -dot, -agent, -title, -time) for the cross-agent
|
|
9
|
+
* active-tasks list between the board and the agent list. All colors tokenized
|
|
10
|
+
* (var(--success) running dot/hover, var(--card-bg)/-alt, --border, --text-dim/-muted).
|
|
7
11
|
* v1.9.0 -- 2026-06-02 -- Component unification (#24 search bar): tokenize the
|
|
8
12
|
* hardcoded hex fallbacks in .pf-agd-search-input (var(--border, #E5E7EB) ->
|
|
9
13
|
* var(--border); var(--card-bg, #FFFFFF) -> var(--card); var(--text, #1A1A2E)
|
|
@@ -49,6 +53,7 @@
|
|
|
49
53
|
* v2.22.0 -- 2026-06-02 -- Add divider line above the tab bar (border-top + spacing) so tabs don't blend with the status row above
|
|
50
54
|
* v2.23.0 -- 2026-06-02 -- Compact .md-body styles for agent message bubbles (Markdown-rendered outbound replies)
|
|
51
55
|
* v2.24.0 -- 2026-06-02 -- Compact .md-body styles for Markdown-rendered non-JSON task memory values (pf-agd-task-memory-md)
|
|
56
|
+
* v2.25.0 -- 2026-06-03 -- Compact .md-body styles for Markdown-rendered task descriptions (pf-agd-task-desc) so long agent-authored prompts render with structure
|
|
52
57
|
* v2.11.0 -- 2026-05-31 -- Widen the solo pop-out window (and pf-agd-solo max-width) so the agent card matches the list-view width and the full tab bar fits
|
|
53
58
|
* v2.12.0 -- 2026-05-31 -- Add completed-task deliverable styles (pf-agd-deliverable*) linking a task to its agent-memory output key
|
|
54
59
|
* v2.13.0 -- 2026-05-31 -- Add per-task blur-title toggle styles (pf-agd-blur-toggle, pf-agd-task-title--blurred) for hiding sensitive titles in screen recordings
|
|
@@ -1231,6 +1236,76 @@
|
|
|
1231
1236
|
padding: 1rem;
|
|
1232
1237
|
margin-bottom: 1rem;
|
|
1233
1238
|
}
|
|
1239
|
+
|
|
1240
|
+
/* Fleet "running now" panel (between the board and the agent list). */
|
|
1241
|
+
.pf .pf-agd-active {
|
|
1242
|
+
background: var(--card-bg, #FFFFFF);
|
|
1243
|
+
border: 1px solid var(--border, #E5E7EB);
|
|
1244
|
+
border-radius: var(--radius, 8px);
|
|
1245
|
+
padding: 0.75rem 1rem;
|
|
1246
|
+
margin-bottom: 1rem;
|
|
1247
|
+
}
|
|
1248
|
+
.pf .pf-agd-active-head {
|
|
1249
|
+
margin-bottom: 0.5rem;
|
|
1250
|
+
}
|
|
1251
|
+
.pf .pf-agd-active-empty {
|
|
1252
|
+
font-size: 0.8rem;
|
|
1253
|
+
color: var(--text-dim, #6B7280);
|
|
1254
|
+
}
|
|
1255
|
+
.pf .pf-agd-active-list {
|
|
1256
|
+
display: flex;
|
|
1257
|
+
flex-direction: column;
|
|
1258
|
+
gap: 0.25rem;
|
|
1259
|
+
}
|
|
1260
|
+
.pf .pf-agd-active-row {
|
|
1261
|
+
display: flex;
|
|
1262
|
+
align-items: center;
|
|
1263
|
+
gap: 0.5rem;
|
|
1264
|
+
width: 100%;
|
|
1265
|
+
text-align: left;
|
|
1266
|
+
background: var(--card-bg-alt, #F3F4F6);
|
|
1267
|
+
border: 1px solid var(--border, #E5E7EB);
|
|
1268
|
+
border-radius: var(--radius-xs, 6px);
|
|
1269
|
+
padding: 0.4rem 0.6rem;
|
|
1270
|
+
font: inherit;
|
|
1271
|
+
color: inherit;
|
|
1272
|
+
cursor: pointer;
|
|
1273
|
+
transition: border-color 0.15s, background 0.15s;
|
|
1274
|
+
}
|
|
1275
|
+
.pf .pf-agd-active-row:hover {
|
|
1276
|
+
background: var(--card-bg, #FFFFFF);
|
|
1277
|
+
border-color: var(--success, #10B981);
|
|
1278
|
+
}
|
|
1279
|
+
.pf .pf-agd-active-dot {
|
|
1280
|
+
flex: 0 0 auto;
|
|
1281
|
+
width: 8px;
|
|
1282
|
+
height: 8px;
|
|
1283
|
+
border-radius: 50%;
|
|
1284
|
+
background: var(--success, #10B981);
|
|
1285
|
+
}
|
|
1286
|
+
.pf .pf-agd-active-agent {
|
|
1287
|
+
flex: 0 0 auto;
|
|
1288
|
+
max-width: 30%;
|
|
1289
|
+
font-weight: 600;
|
|
1290
|
+
font-size: 0.8rem;
|
|
1291
|
+
white-space: nowrap;
|
|
1292
|
+
overflow: hidden;
|
|
1293
|
+
text-overflow: ellipsis;
|
|
1294
|
+
}
|
|
1295
|
+
.pf .pf-agd-active-title {
|
|
1296
|
+
flex: 1 1 auto;
|
|
1297
|
+
min-width: 0;
|
|
1298
|
+
font-size: 0.8rem;
|
|
1299
|
+
color: var(--text-dim, #6B7280);
|
|
1300
|
+
white-space: nowrap;
|
|
1301
|
+
overflow: hidden;
|
|
1302
|
+
text-overflow: ellipsis;
|
|
1303
|
+
}
|
|
1304
|
+
.pf .pf-agd-active-time {
|
|
1305
|
+
flex: 0 0 auto;
|
|
1306
|
+
font-size: 0.72rem;
|
|
1307
|
+
color: var(--text-muted, #9CA3AF);
|
|
1308
|
+
}
|
|
1234
1309
|
.pf .pf-agd-board-grid {
|
|
1235
1310
|
display: grid;
|
|
1236
1311
|
grid-template-columns: repeat(3, 1fr);
|
|
@@ -3295,6 +3370,76 @@
|
|
|
3295
3370
|
}
|
|
3296
3371
|
.pf .pf-agd-task-memory-md .md-body th { background: var(--card-bg, #FFFFFF); }
|
|
3297
3372
|
|
|
3373
|
+
/* Markdown-rendered task descriptions (agent-authored prompts are often long and
|
|
3374
|
+
structured). Compact, mirroring the task-memory variant. The expanded card
|
|
3375
|
+
background is grey (--card-bg-alt), so code chips get a white fill + border for
|
|
3376
|
+
contrast. Paragraphs keep the dim base colour; headings use the normal text
|
|
3377
|
+
colour to stand out. */
|
|
3378
|
+
.pf .pf-agd-task-desc .md-body > :first-child { margin-top: 0; }
|
|
3379
|
+
.pf .pf-agd-task-desc .md-body > :last-child { margin-bottom: 0; }
|
|
3380
|
+
.pf .pf-agd-task-desc .md-body p { margin: 0.3rem 0; }
|
|
3381
|
+
.pf .pf-agd-task-desc .md-body h1,
|
|
3382
|
+
.pf .pf-agd-task-desc .md-body h2,
|
|
3383
|
+
.pf .pf-agd-task-desc .md-body h3,
|
|
3384
|
+
.pf .pf-agd-task-desc .md-body h4 {
|
|
3385
|
+
margin: 0.5rem 0 0.2rem;
|
|
3386
|
+
line-height: 1.3;
|
|
3387
|
+
font-size: 0.85rem;
|
|
3388
|
+
color: var(--text, #1A1A2E);
|
|
3389
|
+
}
|
|
3390
|
+
.pf .pf-agd-task-desc .md-body h1 { font-size: 0.95rem; }
|
|
3391
|
+
.pf .pf-agd-task-desc .md-body h2 { font-size: 0.9rem; }
|
|
3392
|
+
.pf .pf-agd-task-desc .md-body ul,
|
|
3393
|
+
.pf .pf-agd-task-desc .md-body ol { margin: 0.3rem 0; padding-left: 1.3rem; }
|
|
3394
|
+
.pf .pf-agd-task-desc .md-body li { margin: 0.12rem 0; }
|
|
3395
|
+
.pf .pf-agd-task-desc .md-body a { color: var(--accent, #E8564A); }
|
|
3396
|
+
.pf .pf-agd-task-desc .md-body strong { color: var(--text, #1A1A2E); }
|
|
3397
|
+
.pf .pf-agd-task-desc .md-body blockquote {
|
|
3398
|
+
margin: 0.35rem 0;
|
|
3399
|
+
padding: 0.15rem 0.55rem;
|
|
3400
|
+
border-left: 3px solid var(--border, #E5E7EB);
|
|
3401
|
+
color: var(--text-dim, #6B7280);
|
|
3402
|
+
}
|
|
3403
|
+
.pf .pf-agd-task-desc .md-body .md-code {
|
|
3404
|
+
font-family: var(--font-mono, ui-monospace, Menlo, Consolas, monospace);
|
|
3405
|
+
font-size: 0.72rem;
|
|
3406
|
+
background: var(--card-bg, #FFFFFF);
|
|
3407
|
+
border: 1px solid var(--border, #E5E7EB);
|
|
3408
|
+
padding: 0.02rem 0.3rem;
|
|
3409
|
+
border-radius: 4px;
|
|
3410
|
+
}
|
|
3411
|
+
.pf .pf-agd-task-desc .md-body .md-pre {
|
|
3412
|
+
margin: 0.4rem 0;
|
|
3413
|
+
padding: 0.55rem;
|
|
3414
|
+
background: var(--card-bg, #FFFFFF);
|
|
3415
|
+
border: 1px solid var(--border, #E5E7EB);
|
|
3416
|
+
border-radius: 6px;
|
|
3417
|
+
overflow-x: auto;
|
|
3418
|
+
}
|
|
3419
|
+
.pf .pf-agd-task-desc .md-body .md-pre .md-code {
|
|
3420
|
+
display: block;
|
|
3421
|
+
white-space: pre;
|
|
3422
|
+
background: none;
|
|
3423
|
+
border: none;
|
|
3424
|
+
padding: 0;
|
|
3425
|
+
tab-size: 4;
|
|
3426
|
+
font-size: 0.72rem;
|
|
3427
|
+
line-height: 1.35;
|
|
3428
|
+
color: var(--text, #1A1A2E);
|
|
3429
|
+
}
|
|
3430
|
+
.pf .pf-agd-task-desc .md-body table {
|
|
3431
|
+
border-collapse: collapse;
|
|
3432
|
+
margin: 0.35rem 0;
|
|
3433
|
+
font-size: 0.72rem;
|
|
3434
|
+
}
|
|
3435
|
+
.pf .pf-agd-task-desc .md-body th,
|
|
3436
|
+
.pf .pf-agd-task-desc .md-body td {
|
|
3437
|
+
border: 1px solid var(--border, #E5E7EB);
|
|
3438
|
+
padding: 0.2rem 0.45rem;
|
|
3439
|
+
text-align: left;
|
|
3440
|
+
}
|
|
3441
|
+
.pf .pf-agd-task-desc .md-body th { background: var(--card-bg, #FFFFFF); }
|
|
3442
|
+
|
|
3298
3443
|
/* Structured JSON view: key/value rows, nested blocks indented with a guide line. */
|
|
3299
3444
|
.pf .pf-agd-json-block { display: flex; flex-direction: column; gap: 0.05rem; }
|
|
3300
3445
|
.pf .pf-agd-json-row {
|
|
@@ -98,3 +98,19 @@
|
|
|
98
98
|
.sch-form-actions { display: flex; justify-content: flex-end; }
|
|
99
99
|
|
|
100
100
|
.sch-budget-section { margin-bottom: 1rem; }
|
|
101
|
+
|
|
102
|
+
/* ── Editable schedule card (master + agent views) ── */
|
|
103
|
+
.sch-card-list { display: flex; flex-direction: column; gap: 0.6rem; }
|
|
104
|
+
.sch-card {
|
|
105
|
+
background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
|
|
106
|
+
padding: 0.75rem 0.9rem; display: flex; flex-direction: column; gap: 0.5rem;
|
|
107
|
+
}
|
|
108
|
+
.sch-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; }
|
|
109
|
+
.sch-card-title { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; min-width: 0; }
|
|
110
|
+
.sch-card-meta { display: flex; flex-wrap: wrap; gap: 0.3rem 1rem; font-size: 0.82rem; color: var(--text); }
|
|
111
|
+
.sch-meta-k { color: var(--text-dim); }
|
|
112
|
+
.sch-card-dispatch { background: var(--bg-dim); border-radius: var(--radius-sm); padding: 0.5rem 0.7rem; }
|
|
113
|
+
.sch-dispatch-label { font-size: 0.66rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 0.2rem; }
|
|
114
|
+
.sch-dispatch-title { font-weight: 600; font-size: 0.9rem; color: var(--text); white-space: pre-wrap; }
|
|
115
|
+
.sch-dispatch-body { font-size: 0.84rem; color: var(--text-dim); margin-top: 0.15rem; white-space: pre-wrap; word-break: break-word; }
|
|
116
|
+
.sch-edit { display: flex; flex-direction: column; gap: 0.55rem; border-top: 1px solid var(--border); padding-top: 0.6rem; }
|
|
@@ -152,6 +152,9 @@ export const saveSiteTemplate = (tmpl) => apiPost('/v1/site/template', { templa
|
|
|
152
152
|
export const deleteSiteTemplate = () => apiDelete('/v1/site/template');
|
|
153
153
|
export const getSiteChangelog = () => apiGet('/v1/site/changelog');
|
|
154
154
|
export const getSiteMemoryKeys = () => apiGet('/v1/site/memory-keys');
|
|
155
|
+
export const setSiteMemory = (key, value) => apiPost('/v1/site/memory', { key, value });
|
|
156
|
+
export const deleteSiteMemory = (key) => apiDelete(`/v1/site/memory/${encodeURIComponent(key)}`);
|
|
157
|
+
export const importSiteBundle = (bundle) => apiPost('/v1/site/import', bundle);
|
|
155
158
|
export const clearSiteCache = () => apiPost('/v1/site/cache-invalidate');
|
|
156
159
|
export const getSitePrompt = () => apiGet('/v1/site/prompt');
|
|
157
160
|
export const triggerLbSync = () => apiPost('/v1/admin/site/sync');
|
|
@@ -35,6 +35,16 @@ write the HTML using the starter template and SDK libraries below.
|
|
|
35
35
|
Do not ask about deployment, cortex, extensions, CSM, or
|
|
36
36
|
architecture. Just ask what the app should do and start building.
|
|
37
37
|
|
|
38
|
+
Two starter templates follow. Pick by who needs to read the content:
|
|
39
|
+
|
|
40
|
+
- **Starter Template** (default, login-gated) — the visitor signs in and
|
|
41
|
+
works with their own data (notes, trackers, multiplayer, galleries).
|
|
42
|
+
Nothing renders until there is a session.
|
|
43
|
+
- **Public viewer template** (readable without login) — the content is
|
|
44
|
+
shown to everyone, no account required; the owner logs in only to edit.
|
|
45
|
+
Use it for public-facing readers: a public newspaper, directory,
|
|
46
|
+
noticeboard, or gallery that pulls from PUBLIC memory.
|
|
47
|
+
|
|
38
48
|
### Starter Template (use this for every app)
|
|
39
49
|
|
|
40
50
|
```html
|
|
@@ -93,6 +103,164 @@ entry: index.html
|
|
|
93
103
|
</html>
|
|
94
104
|
```
|
|
95
105
|
|
|
106
|
+
### Public viewer template — readable without login
|
|
107
|
+
|
|
108
|
+
Use this when the content must be visible to anyone, with no account
|
|
109
|
+
(public newspaper, directory, noticeboard, gallery). It differs from the
|
|
110
|
+
default Starter Template in three ways:
|
|
111
|
+
|
|
112
|
+
1. **`startApp()` runs for everyone.** The login bar still mounts, but the
|
|
113
|
+
app never waits for a session — anonymous visitors render immediately.
|
|
114
|
+
2. **Reads use `getPublic(gaii, key)` only.** This is the single read that
|
|
115
|
+
works without a token (it hits `GET /v1/memory/:gaii/:key` and returns
|
|
116
|
+
PUBLIC entries). Do NOT use `AIMEAT.data.get/list/search` for the shown
|
|
117
|
+
content — those require a session and read the *caller's* namespace, not
|
|
118
|
+
the publisher's. There is no anonymous "list public keys" call.
|
|
119
|
+
3. **Content lives behind one public index key.** The publisher keeps a
|
|
120
|
+
single PUBLIC key (a "front page") whose value lists each item with its
|
|
121
|
+
own `gaii` + `key`. The viewer reads the index, then fans out to each
|
|
122
|
+
item. The bodies can sit under many different authors (e.g. several
|
|
123
|
+
writer agents) — only the index has a fixed home, and it carries every
|
|
124
|
+
item's full `gaii`, so the app never has to know each author up front.
|
|
125
|
+
|
|
126
|
+
```html
|
|
127
|
+
<!-- AIMEAT App Manifest
|
|
128
|
+
name: public-viewer
|
|
129
|
+
version: 1.0.0
|
|
130
|
+
description: Reads public memory and shows it to everyone — no login required
|
|
131
|
+
entry: index.html
|
|
132
|
+
-->
|
|
133
|
+
<!DOCTYPE html>
|
|
134
|
+
<html lang="en">
|
|
135
|
+
<head>
|
|
136
|
+
<meta charset="UTF-8">
|
|
137
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
138
|
+
<title>Public Viewer</title>
|
|
139
|
+
<link href="https://cdn.jsdelivr.net/npm/daisyui@5" rel="stylesheet" />
|
|
140
|
+
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
141
|
+
</head>
|
|
142
|
+
<body class="bg-base-100 min-h-screen flex flex-col">
|
|
143
|
+
<nav class="navbar bg-base-200 shadow-sm px-4">
|
|
144
|
+
<div class="flex-1"><span class="text-lg font-bold">Public Viewer</span></div>
|
|
145
|
+
<!-- Login bar mounts for everyone. Anonymous visitors just read; the
|
|
146
|
+
owner can sign in to edit. Reading never depends on it. -->
|
|
147
|
+
<div class="flex-none"><span id="header-auth"></span></div>
|
|
148
|
+
</nav>
|
|
149
|
+
<div id="app" class="flex-1 p-4">Loading…</div>
|
|
150
|
+
<script>
|
|
151
|
+
// ── Where the public index lives (fill these in) ──
|
|
152
|
+
// PUBLISHER is the gaii that owns INDEX_KEY:
|
|
153
|
+
// • owner-run public site → the owner GHII, e.g. 'alice@aimeat-fi-001-genesis'
|
|
154
|
+
// • agent/pipeline-fed feed → the publishing agent GAII,
|
|
155
|
+
// e.g. 'editor#alice@aimeat-fi-001-genesis'
|
|
156
|
+
// (To auto-derive the owner GHII from the app's /v1/apps/:owner/ URL, read
|
|
157
|
+
// it from location.pathname — but an explicit value is clearer and avoids
|
|
158
|
+
// guessing the node id.)
|
|
159
|
+
const PUBLISHER = 'OWNER_OR_AGENT@NODE_ID'; // ← set this
|
|
160
|
+
const INDEX_KEY = 'newspaper.frontpage'; // ← a PUBLIC key holding the item list
|
|
161
|
+
|
|
162
|
+
function loadScript(src) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const s = document.createElement('script');
|
|
165
|
+
s.src = src; s.onload = resolve; s.onerror = reject;
|
|
166
|
+
document.head.appendChild(s);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function boot() {
|
|
171
|
+
await loadScript('/v1/libs/aimeat-auth.js');
|
|
172
|
+
await loadScript('/v1/libs/aimeat-data.js');
|
|
173
|
+
|
|
174
|
+
// The login bar renders for everyone. Owner login unlocks editing;
|
|
175
|
+
// logout returns to read-only. Re-render on either so the UI matches.
|
|
176
|
+
AIMEAT.auth.mountLoginButton('#header-auth', {
|
|
177
|
+
onLogin: () => location.reload(),
|
|
178
|
+
onLogout: () => location.reload(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Restore an existing session if there is one — but DO NOT gate on it.
|
|
182
|
+
await AIMEAT.auth.login(); // owner session, or null for an anonymous visitor
|
|
183
|
+
startApp(); // ALWAYS render — anonymous visitors included
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function startApp() {
|
|
187
|
+
const session = AIMEAT.auth.getSession(); // null when nobody is logged in
|
|
188
|
+
// Editing is offered only when the logged-in owner actually owns the index.
|
|
189
|
+
// For a pipeline-fed feed (PUBLISHER is an agent), the app stays read-only
|
|
190
|
+
// and the pipeline maintains the front page.
|
|
191
|
+
const canEdit = !!session && session.ghii === PUBLISHER;
|
|
192
|
+
const app = document.getElementById('app');
|
|
193
|
+
|
|
194
|
+
// ── Anonymous-safe read: the public front-page index ──
|
|
195
|
+
const index = await AIMEAT.data.getPublic(PUBLISHER, INDEX_KEY) || [];
|
|
196
|
+
// index item shape (you choose it): { gaii, key, title, date, summary }
|
|
197
|
+
|
|
198
|
+
// Fan out to each item's public body. Items may live under different
|
|
199
|
+
// authors — the index carries each item's full gaii.
|
|
200
|
+
const items = (await Promise.all(index.map(async (item) => {
|
|
201
|
+
const body = await AIMEAT.data.getPublic(item.gaii, item.key);
|
|
202
|
+
return body ? { ...item, body } : null;
|
|
203
|
+
}))).filter(Boolean);
|
|
204
|
+
|
|
205
|
+
render(app, items, canEdit);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function render(app, items, canEdit) {
|
|
209
|
+
const header = canEdit
|
|
210
|
+
? '<div class="mb-4"><button id="new-btn" class="btn btn-primary btn-sm">New article</button></div>'
|
|
211
|
+
: '';
|
|
212
|
+
const body = items.length
|
|
213
|
+
? items.map(a =>
|
|
214
|
+
'<article class="card bg-base-200 mb-3"><div class="card-body">' +
|
|
215
|
+
'<h2 class="card-title">' + esc(a.title || a.key) + '</h2>' +
|
|
216
|
+
'<p class="text-xs opacity-60">' + esc(a.date || '') + '</p>' +
|
|
217
|
+
// Public content can come from anyone — ALWAYS escape before insert.
|
|
218
|
+
// Never assign a raw public value to innerHTML.
|
|
219
|
+
'<div>' + esc(typeof a.body === 'string' ? a.body : (a.body.text ?? JSON.stringify(a.body))) + '</div>' +
|
|
220
|
+
'</div></article>'
|
|
221
|
+
).join('')
|
|
222
|
+
: '<p class="opacity-70">No content published yet.</p>';
|
|
223
|
+
app.innerHTML = header + body;
|
|
224
|
+
if (canEdit) document.getElementById('new-btn').onclick = publishArticle;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Owner-only: write a PUBLIC article and prepend it to the PUBLIC index.
|
|
228
|
+
// Both writes land in the owner's GHII namespace (= PUBLISHER here), so the
|
|
229
|
+
// viewer reads them straight back via getPublic.
|
|
230
|
+
async function publishArticle() {
|
|
231
|
+
const title = prompt('Title?'); if (!title) return;
|
|
232
|
+
const text = prompt('Body?') || '';
|
|
233
|
+
const session = AIMEAT.auth.getSession();
|
|
234
|
+
const now = new Date().toISOString();
|
|
235
|
+
const key = 'newspaper.article.' + Date.now();
|
|
236
|
+
await AIMEAT.data.set(key, { title, text, date: now }, { visibility: 'public' });
|
|
237
|
+
const index = await AIMEAT.data.getPublic(PUBLISHER, INDEX_KEY) || [];
|
|
238
|
+
index.unshift({ gaii: session.ghii, key, title, date: now });
|
|
239
|
+
await AIMEAT.data.set(INDEX_KEY, index, { visibility: 'public' });
|
|
240
|
+
location.reload();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; }
|
|
244
|
+
boot();
|
|
245
|
+
</script>
|
|
246
|
+
</body>
|
|
247
|
+
</html>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Public viewer rules:**
|
|
251
|
+
- Call `startApp()` unconditionally. Never `if (session) startApp()` — that is
|
|
252
|
+
what leaves anonymous visitors stuck on "Loading…".
|
|
253
|
+
- `getPublic(gaii, key)` is the only anonymous read. `get/list/search/set`
|
|
254
|
+
all require a login and operate on the caller's own namespace.
|
|
255
|
+
- Everything the public sees must be written with `visibility: 'public'` —
|
|
256
|
+
the index key and every item body.
|
|
257
|
+
- The index is the single source the app must know; it carries each item's
|
|
258
|
+
full `gaii`, so bodies can be spread across many author agents.
|
|
259
|
+
- Public content is untrusted input. Escape it before inserting into the DOM
|
|
260
|
+
(the template's `esc()` does this). Never `innerHTML` a raw public value.
|
|
261
|
+
- Owner editing is a UX affordance gated on `session.ghii === PUBLISHER`; the
|
|
262
|
+
server is the real boundary (it rejects public writes without an owner session).
|
|
263
|
+
|
|
96
264
|
### SDK Libraries (add to boot() as needed)
|
|
97
265
|
|
|
98
266
|
| Library | Load with | Use for |
|
|
@@ -340,7 +508,7 @@ entry: index.html
|
|
|
340
508
|
// AIMEAT.data.delete(key) — delete entry
|
|
341
509
|
// AIMEAT.data.search(query) — search across keys and values
|
|
342
510
|
// AIMEAT.data.list() — list all keys
|
|
343
|
-
// AIMEAT.data.getPublic(gaii, key) — read another user's public data
|
|
511
|
+
// AIMEAT.data.getPublic(gaii, key) — read another user's public data (no login required)
|
|
344
512
|
|
|
345
513
|
// Example: load saved data or initialize
|
|
346
514
|
let notes = await AIMEAT.data.get('my-app.notes') || [];
|
|
@@ -576,7 +744,7 @@ await AIMEAT.data.update(key, value, version) // optimistic locking update
|
|
|
576
744
|
await AIMEAT.data.delete(key) // delete entry
|
|
577
745
|
await AIMEAT.data.list() // list all keys
|
|
578
746
|
await AIMEAT.data.search(query) // full-text search
|
|
579
|
-
await AIMEAT.data.getPublic(gaii, key) // read another user's public data
|
|
747
|
+
await AIMEAT.data.getPublic(gaii, key) // read another user's public data (no login — the only anonymous read; see "Public viewer template")
|
|
580
748
|
```
|
|
581
749
|
|
|
582
750
|
**AIMEAT.storage** (`/v1/libs/aimeat-storage.js`):
|
|
@@ -363,7 +363,13 @@
|
|
|
363
363
|
});
|
|
364
364
|
}
|
|
365
365
|
} else {
|
|
366
|
-
|
|
366
|
+
// d.error may be an OAuth string ("invalid_request") OR an AIMEAT
|
|
367
|
+
// envelope object ({code,message}); never stringify the object as
|
|
368
|
+
// "[object Object]" — surface the real message.
|
|
369
|
+
const msg = d.error_description
|
|
370
|
+
|| (typeof d.error === 'string' ? d.error : d.error && d.error.message)
|
|
371
|
+
|| t('errorGeneric');
|
|
372
|
+
throw new Error(msg);
|
|
367
373
|
}
|
|
368
374
|
} catch (err) {
|
|
369
375
|
errEl.textContent = err.message;
|