aimeat 1.32.1 → 1.33.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/locales/en.json +359 -2
- package/dist/locales/fi.json +359 -2
- package/dist/public/css/views/admin.css +10 -0
- package/dist/public/css/views/agents-detail.css +8 -0
- package/dist/public/css/views/my-company.css +41 -0
- package/dist/public/css/views/profile.css +7 -1
- package/dist/public/css/views/secretary.css +482 -1
- package/dist/public/js/services/admin.js +7 -0
- package/dist/public/js/services/agents.js +18 -3
- package/dist/public/js/services/generator-prompts-build.js +5 -1
- package/dist/public/js/services/organisms.js +16 -0
- package/dist/public/js/services/secretary-helpers.js +197 -0
- package/dist/public/js/services/secretary-policy.js +63 -0
- package/dist/public/js/services/secretary-routing.js +69 -0
- package/dist/public/spa.html +14 -0
- package/dist/public/views/admin/apps-tab.js +208 -0
- package/dist/public/views/admin.js +3 -0
- package/dist/public/views/my-company.js +247 -3
- package/dist/public/views/profile/agents/agent-card.js +15 -0
- package/dist/public/views/profile/apps-tab.js +4 -0
- package/dist/public/views/profile/extensions-tab.js +30 -1
- package/dist/public/views/profile/generator-settings.js +28 -1
- package/dist/public/views/secretary/cards-reach.js +235 -0
- package/dist/public/views/secretary/cards.js +419 -0
- package/dist/public/views/secretary/use-access.js +95 -0
- package/dist/public/views/secretary/use-autonomy.js +92 -0
- package/dist/public/views/secretary/use-create-resource.js +86 -0
- package/dist/public/views/secretary/use-crew.js +173 -0
- package/dist/public/views/secretary/use-guided-plan.js +112 -0
- package/dist/public/views/secretary/use-intake.js +332 -0
- package/dist/public/views/secretary/use-knowledge.js +104 -0
- package/dist/public/views/secretary/use-learning.js +140 -0
- package/dist/public/views/secretary.js +327 -34
- package/dist/src/cli/connect/mcp/tools/agent-management.d.ts +8 -8
- package/dist/src/cli/connect/mcp/tools/agent-management.js +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-management.js.map +1 -1
- package/dist/src/enterprise/loader.d.ts.map +1 -1
- package/dist/src/enterprise/loader.js +2 -0
- package/dist/src/enterprise/loader.js.map +1 -1
- package/dist/src/enterprise/provider.d.ts +67 -1
- package/dist/src/enterprise/provider.d.ts.map +1 -1
- package/dist/src/enterprise/provider.js +17 -0
- package/dist/src/enterprise/provider.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +744 -49
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +7 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +4 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +117 -1
- package/dist/src/generated/prisma-postgres/index.js +7 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +16 -12
- package/dist/src/generated/prisma-postgres/wasm.js +7 -3
- package/dist/src/mcp/agent-management.d.ts +2 -0
- package/dist/src/mcp/agent-management.d.ts.map +1 -1
- package/dist/src/mcp/agent-management.js +5 -1
- package/dist/src/mcp/agent-management.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +3 -3
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/scopes.d.ts +46 -0
- package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
- package/dist/src/mcp/catalog/scopes.js +81 -0
- package/dist/src/mcp/catalog/scopes.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +2 -2
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/routes/agent-directives.d.ts +12 -1
- package/dist/src/routes/agent-directives.d.ts.map +1 -1
- package/dist/src/routes/agent-directives.js +30 -4
- package/dist/src/routes/agent-directives.js.map +1 -1
- package/dist/src/routes/agents.d.ts +3 -0
- package/dist/src/routes/agents.d.ts.map +1 -1
- package/dist/src/routes/agents.js +11 -4
- package/dist/src/routes/agents.js.map +1 -1
- package/dist/src/routes/ai.d.ts +2 -0
- package/dist/src/routes/ai.d.ts.map +1 -1
- package/dist/src/routes/ai.js +15 -2
- package/dist/src/routes/ai.js.map +1 -1
- package/dist/src/routes/apps.d.ts +14 -0
- package/dist/src/routes/apps.d.ts.map +1 -1
- package/dist/src/routes/apps.js +168 -3
- package/dist/src/routes/apps.js.map +1 -1
- package/dist/src/routes/extensions.d.ts +4 -0
- package/dist/src/routes/extensions.d.ts.map +1 -1
- package/dist/src/routes/extensions.js +75 -11
- package/dist/src/routes/extensions.js.map +1 -1
- package/dist/src/routes/libs.d.ts +6 -0
- package/dist/src/routes/libs.d.ts.map +1 -1
- package/dist/src/routes/libs.js +163 -0
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/oauth-login.d.ts +16 -6
- package/dist/src/routes/oauth-login.d.ts.map +1 -1
- package/dist/src/routes/oauth-login.js +290 -51
- package/dist/src/routes/oauth-login.js.map +1 -1
- package/dist/src/routes/openrouter.d.ts +2 -0
- package/dist/src/routes/openrouter.d.ts.map +1 -1
- package/dist/src/routes/openrouter.js +8 -1
- package/dist/src/routes/openrouter.js.map +1 -1
- package/dist/src/routes/organism-templates.d.ts +22 -0
- package/dist/src/routes/organism-templates.d.ts.map +1 -0
- package/dist/src/routes/organism-templates.js +110 -0
- package/dist/src/routes/organism-templates.js.map +1 -0
- package/dist/src/routes/schedules.d.ts +2 -0
- package/dist/src/routes/schedules.d.ts.map +1 -1
- package/dist/src/routes/schedules.js +3 -1
- package/dist/src/routes/schedules.js.map +1 -1
- package/dist/src/routes/specialists.d.ts +28 -0
- package/dist/src/routes/specialists.d.ts.map +1 -0
- package/dist/src/routes/specialists.js +227 -0
- package/dist/src/routes/specialists.js.map +1 -0
- package/dist/src/routes/subdomains.d.ts +3 -0
- package/dist/src/routes/subdomains.d.ts.map +1 -1
- package/dist/src/routes/subdomains.js +12 -2
- package/dist/src/routes/subdomains.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +11 -3
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/ai-completion.d.ts +8 -0
- package/dist/src/services/ai-completion.d.ts.map +1 -1
- package/dist/src/services/ai-completion.js +7 -1
- package/dist/src/services/ai-completion.js.map +1 -1
- package/dist/src/services/discovery/setup.d.ts +2 -1
- package/dist/src/services/discovery/setup.d.ts.map +1 -1
- package/dist/src/services/discovery/setup.js +2 -0
- package/dist/src/services/discovery/setup.js.map +1 -1
- package/dist/src/services/discovery/sources/memory-source.d.ts +2 -0
- package/dist/src/services/discovery/sources/memory-source.d.ts.map +1 -1
- package/dist/src/services/discovery/sources/memory-source.js +8 -1
- package/dist/src/services/discovery/sources/memory-source.js.map +1 -1
- package/dist/src/services/discovery/sources/template-source.d.ts +17 -0
- package/dist/src/services/discovery/sources/template-source.d.ts.map +1 -0
- package/dist/src/services/discovery/sources/template-source.js +51 -0
- package/dist/src/services/discovery/sources/template-source.js.map +1 -0
- package/dist/src/services/discovery/types.d.ts +1 -1
- package/dist/src/services/discovery/types.d.ts.map +1 -1
- package/dist/src/services/extension-secrets.d.ts +63 -0
- package/dist/src/services/extension-secrets.d.ts.map +1 -0
- package/dist/src/services/extension-secrets.js +122 -0
- package/dist/src/services/extension-secrets.js.map +1 -0
- package/dist/src/services/handbooks/appdev.d.ts +1 -1
- package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
- package/dist/src/services/handbooks/appdev.js +1 -1
- package/dist/src/services/openrouter.d.ts +1 -1
- package/dist/src/services/openrouter.d.ts.map +1 -1
- package/dist/src/services/openrouter.js +18 -2
- package/dist/src/services/openrouter.js.map +1 -1
- package/dist/src/services/organism-export.d.ts +2 -0
- package/dist/src/services/organism-export.d.ts.map +1 -1
- package/dist/src/services/organism-export.js +24 -5
- package/dist/src/services/organism-export.js.map +1 -1
- package/dist/src/services/organism-import.d.ts +11 -0
- package/dist/src/services/organism-import.d.ts.map +1 -1
- package/dist/src/services/organism-import.js +16 -1
- package/dist/src/services/organism-import.js.map +1 -1
- package/dist/src/services/organism-template.d.ts +160 -0
- package/dist/src/services/organism-template.d.ts.map +1 -0
- package/dist/src/services/organism-template.js +202 -0
- package/dist/src/services/organism-template.js.map +1 -0
- package/dist/src/services/scheduler.d.ts +45 -0
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +358 -5
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/secretary-tick.d.ts +127 -0
- package/dist/src/services/secretary-tick.d.ts.map +1 -0
- package/dist/src/services/secretary-tick.js +169 -0
- package/dist/src/services/secretary-tick.js.map +1 -0
- package/dist/src/services/secretary.d.ts +98 -2
- package/dist/src/services/secretary.d.ts.map +1 -1
- package/dist/src/services/secretary.js +130 -12
- package/dist/src/services/secretary.js.map +1 -1
- package/dist/src/services/specialist-policy.d.ts +39 -0
- package/dist/src/services/specialist-policy.d.ts.map +1 -0
- package/dist/src/services/specialist-policy.js +59 -0
- package/dist/src/services/specialist-policy.js.map +1 -0
- package/dist/src/services/specialist.d.ts +77 -0
- package/dist/src/services/specialist.d.ts.map +1 -0
- package/dist/src/services/specialist.js +137 -0
- package/dist/src/services/specialist.js.map +1 -0
- package/dist/src/services/structure-overview.d.ts +3 -0
- package/dist/src/services/structure-overview.d.ts.map +1 -1
- package/dist/src/services/structure-overview.js +20 -10
- package/dist/src/services/structure-overview.js.map +1 -1
- package/dist/src/services/system-agent.d.ts +39 -0
- package/dist/src/services/system-agent.d.ts.map +1 -0
- package/dist/src/services/system-agent.js +43 -0
- package/dist/src/services/system-agent.js.map +1 -0
- package/dist/src/storage/interface.d.ts +7 -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 +5 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +31 -4
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +5 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +26 -6
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +8 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/app.repository.d.ts +17 -0
- package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
- package/dist/src/utils/app-badge.d.ts +26 -0
- package/dist/src/utils/app-badge.d.ts.map +1 -0
- package/dist/src/utils/app-badge.js +56 -0
- package/dist/src/utils/app-badge.js.map +1 -0
- package/dist/static/app-catalog.html +315 -71
- package/package.json +1 -1
- package/prisma/schema.postgres.prisma +4 -0
- package/prisma/schema.prisma +4 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file secretary-helpers.js
|
|
3
|
+
* @description Pure helpers for the Secretary view (kept out of the view module so it stays small):
|
|
4
|
+
* the interview/design prompt builders + their shared JSON output contract, robust JSON extraction
|
|
5
|
+
* from AI text, a brain version-snapshot, a context id generator, and the secretary.config migration
|
|
6
|
+
* from the old single-context layout to the multi-context `{contexts[], activeContextId}` shape.
|
|
7
|
+
* See docs/plans/2026-06-23-secretary-feature.md (§5 hire, §22 multi-context).
|
|
8
|
+
* @structure CONTRACT · buildInterviewPrompt · buildDesignPrompt · extractJson · snapshotOf · genCtxId · migrateConfig · buildDecisionRecord
|
|
9
|
+
* @usage import { buildInterviewPrompt, extractJson, migrateConfig } from '/js/services/secretary-helpers.js';
|
|
10
|
+
* @version-history
|
|
11
|
+
* v0.2.0 — 2026-06-24 — P3-B: buildDecisionRecord — the single source of the decision-log contract
|
|
12
|
+
* shape, shared by the manual log form (use-learning), answered Ask cards (use-intake), and
|
|
13
|
+
* approved guided plans (use-guided-plan) so real choices feed the learning loop.
|
|
14
|
+
* v0.1.0 — 2026-06-23 — Extracted from views/secretary.js (Phase 2, keep view < 500 lines).
|
|
15
|
+
*/
|
|
16
|
+
import { defaultPolicy } from '/js/services/secretary-policy.js';
|
|
17
|
+
|
|
18
|
+
/** Concise, accurate AIMEAT primer fed to the chat so the Secretary can teach/guide the user
|
|
19
|
+
* ("tutustuta AIMEATiin"). Concept-level (avoids brittle exact UI paths so it doesn't drift). */
|
|
20
|
+
export const SECRETARY_AIMEAT_PRIMER = `AIMEAT is the user's own AI-agent platform. Key ideas you can explain:
|
|
21
|
+
- The user owns everything under their identity (GHII). "Morsels" (❤️) are the usage/credit meter — they're spent on actions, not cashable.
|
|
22
|
+
- Memory: the user's private key-value knowledge store.
|
|
23
|
+
- Organisms = spaces (personal or shared) that contain Workspaces (like folders) holding structured records. The user's Secretary keeps each context's info in its own organism.
|
|
24
|
+
- Agents: AI helpers the user connects (each scoped); the Secretary is one of them.
|
|
25
|
+
- Discover (the network search): finds what already exists across the node — agents, services, companies' offerings, knowledge, apps — so you never rebuild what's there. Always suggest discovering before building.
|
|
26
|
+
- Apps, extensions, capabilities: ways to extend AIMEAT; companies can publish offerings others order.
|
|
27
|
+
When the user asks how to do something in AIMEAT, explain clearly and concretely from these concepts, point them to the right area of their Profile, and offer to walk them through it step by step. If unsure, say so rather than inventing exact button names.`;
|
|
28
|
+
|
|
29
|
+
/** Shared JSON output contract both interview modes ask the AI to produce. */
|
|
30
|
+
export const CONTRACT = `\`\`\`json
|
|
31
|
+
{
|
|
32
|
+
"brain": {
|
|
33
|
+
"purpose": "1-3 sentences: who my Secretary is and how it helps me in this context",
|
|
34
|
+
"rules": [ { "id": "r1", "description": "a concrete operating rule" } ]
|
|
35
|
+
},
|
|
36
|
+
"organism": {
|
|
37
|
+
"name": "a short name for this space",
|
|
38
|
+
"description": "one line describing it",
|
|
39
|
+
"workspaces": [ { "name": "workspace name", "purpose": "what it holds and why" } ]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
\`\`\``;
|
|
43
|
+
|
|
44
|
+
/** Conversational interview the user runs in their own external AI chat. */
|
|
45
|
+
export function buildInterviewPrompt(owner) {
|
|
46
|
+
return `I want to set up a context for my personal Secretary inside AIMEAT${owner ? ` (my username is "${owner}")` : ''}.
|
|
47
|
+
|
|
48
|
+
Act as that Secretary getting to know me for ONE area of my life or work. Interview me with a few focused questions about: what I'm trying to achieve in this area, the kinds of tasks I'd want help with, and how I like to communicate (tone, language, level of detail). Do NOT ask me about "organisms", "workspaces", folders or any technical structure — figure the right structure out yourself from what I tell you. Keep it conversational; ask, wait for my answers, then continue.
|
|
49
|
+
|
|
50
|
+
When you have enough, design two things and output them as ONE JSON object inside a single code block with EXACTLY this shape and nothing else:
|
|
51
|
+
|
|
52
|
+
${CONTRACT}
|
|
53
|
+
|
|
54
|
+
Constraints: 3–7 brain rules; 2–6 workspaces, each designed from my actual needs. Output ONLY the JSON code block.`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Single-shot design prompt for the in-app mode (runs on the owner's OpenRouter key). */
|
|
58
|
+
export function buildDesignPrompt(owner, needs) {
|
|
59
|
+
return `You are setting up one context of ${owner || 'a user'}'s personal Secretary inside AIMEAT. Based ONLY on the needs described below, design two things and output them as ONE JSON object inside a single code block with EXACTLY this shape and nothing else:
|
|
60
|
+
|
|
61
|
+
${CONTRACT}
|
|
62
|
+
|
|
63
|
+
The user's needs:
|
|
64
|
+
"""
|
|
65
|
+
${needs}
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
Constraints: 3–7 brain rules; 2–6 workspaces (you choose names + purposes from the needs). Output ONLY the JSON code block — no commentary.`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Pull a JSON object out of an AI's text (may be fenced / surrounded by prose). */
|
|
72
|
+
export function extractJson(text) {
|
|
73
|
+
let s = String(text || '').trim();
|
|
74
|
+
const fence = s.match(/```(?:json)?\s*\n?([\s\S]*?)```/i);
|
|
75
|
+
if (fence) s = fence[1].trim();
|
|
76
|
+
if (s[0] !== '{') {
|
|
77
|
+
const i = s.indexOf('{');
|
|
78
|
+
const j = s.lastIndexOf('}');
|
|
79
|
+
if (i >= 0 && j > i) s = s.slice(i, j + 1);
|
|
80
|
+
}
|
|
81
|
+
return JSON.parse(s);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A version snapshot of a context brain (purpose + rules), or null if unset. */
|
|
85
|
+
export function snapshotOf(brain) {
|
|
86
|
+
if (!brain || !brain.purpose) return null;
|
|
87
|
+
const rules = Array.isArray(brain.rules) ? brain.rules.map((r) => ({ id: r.id, description: r.description })) : [];
|
|
88
|
+
return { ts: new Date().toISOString(), purpose: brain.purpose, rules };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function genCtxId() {
|
|
92
|
+
return 'ctx-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Cheap, AI-free context routing (plan §22 step 2): score `text` against each context's name +
|
|
96
|
+
* purpose + workspaces by word overlap. Returns {id,name} of a NON-active context only when it
|
|
97
|
+
* clearly beats the active one — so the view can SUGGEST a switch. Null otherwise (no nagging). */
|
|
98
|
+
export function suggestContextId(text, contexts, activeId) {
|
|
99
|
+
const t = String(text || '').toLowerCase();
|
|
100
|
+
if (t.trim().length < 8 || !Array.isArray(contexts) || contexts.length < 2) return null;
|
|
101
|
+
const words = new Set(t.split(/[^a-z0-9äöå]+/i).filter((w) => w.length >= 4));
|
|
102
|
+
if (words.size === 0) return null;
|
|
103
|
+
const score = (ctx) => {
|
|
104
|
+
const hay = [ctx.name, ctx.brain && ctx.brain.purpose, ...((ctx.workspaces || []).map((w) => w.name + ' ' + (w.purpose || '')))].join(' ').toLowerCase();
|
|
105
|
+
let s = 0;
|
|
106
|
+
for (const w of words) if (hay.includes(w)) s++;
|
|
107
|
+
return s;
|
|
108
|
+
};
|
|
109
|
+
let best = null;
|
|
110
|
+
let bestScore = 0;
|
|
111
|
+
for (const c of contexts) {
|
|
112
|
+
const s = score(c);
|
|
113
|
+
if (s > bestScore) { bestScore = s; best = c; }
|
|
114
|
+
}
|
|
115
|
+
const activeCtx = contexts.find((c) => c.id === activeId);
|
|
116
|
+
const activeScore = activeCtx ? score(activeCtx) : 0;
|
|
117
|
+
if (best && best.id !== activeId && bestScore >= 2 && bestScore > activeScore) return { id: best.id, name: best.name };
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** P1-C: remaining autonomous morsel budget for a context today, or null when no limit is set.
|
|
122
|
+
* `budget` is the context's policy.dailyMorselBudget (number|null); reads the per-day spend ledger
|
|
123
|
+
* the tick maintains on `config.autonomousLedger[contextId]`. */
|
|
124
|
+
export function computeBudgetInfo(budget, config, contextId) {
|
|
125
|
+
if (budget == null) return null;
|
|
126
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
127
|
+
const led = (config && config.autonomousLedger && config.autonomousLedger[contextId]) || null;
|
|
128
|
+
const spent = (led && led.date === today && typeof led.morsels === 'number') ? led.morsels : 0;
|
|
129
|
+
return { budget, spent, remaining: Math.max(0, budget - spent) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** P1-D: self-facing reliability = mean score over REVIEWED decisions (not the marketplace trustScore).
|
|
133
|
+
* Returns { count, score } where score is null until at least one decision has been reviewed. */
|
|
134
|
+
export function computeReliability(decisions) {
|
|
135
|
+
const reviewed = (decisions || []).filter((d) => d && d.status === 'reviewed' && typeof d.score === 'number');
|
|
136
|
+
if (reviewed.length === 0) return { count: 0, score: null };
|
|
137
|
+
return { count: reviewed.length, score: Math.round(reviewed.reduce((s, d) => s + d.score, 0) / reviewed.length) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Normalize secretary.config to the multi-context shape, migrating the old single-context layout.
|
|
141
|
+
* `directives` (merged) seeds the brain for a migrated legacy context. Returns {config, changed}. */
|
|
142
|
+
export function migrateConfig(cfg, directives) {
|
|
143
|
+
if (cfg && Array.isArray(cfg.contexts)) return { config: cfg, changed: false };
|
|
144
|
+
const dirRules = (directives && Array.isArray(directives.rules))
|
|
145
|
+
? directives.rules.filter((r) => r.source !== 'system').map((r) => ({ id: r.id, description: r.description }))
|
|
146
|
+
: [];
|
|
147
|
+
const dirPurpose = (directives && directives.purpose) || '';
|
|
148
|
+
if (cfg && (cfg.selfOrganismId || cfg.organismName)) {
|
|
149
|
+
const ctx = {
|
|
150
|
+
id: 'ctx-legacy',
|
|
151
|
+
name: cfg.organismName || 'My space',
|
|
152
|
+
brain: { purpose: dirPurpose, rules: dirRules },
|
|
153
|
+
organismId: cfg.selfOrganismId || null,
|
|
154
|
+
organismName: cfg.organismName || null,
|
|
155
|
+
workspaces: cfg.workspaces || [],
|
|
156
|
+
policy: cfg.policy || defaultPolicy(),
|
|
157
|
+
brainHistory: cfg.brainHistory || [],
|
|
158
|
+
};
|
|
159
|
+
return { config: { contexts: [ctx], activeContextId: ctx.id }, changed: true };
|
|
160
|
+
}
|
|
161
|
+
if (!cfg && dirPurpose) {
|
|
162
|
+
const ctx = { id: 'ctx-legacy', name: 'My space', brain: { purpose: dirPurpose, rules: dirRules }, organismId: null, organismName: null, workspaces: [], policy: defaultPolicy(), brainHistory: [] };
|
|
163
|
+
return { config: { contexts: [ctx], activeContextId: ctx.id }, changed: true };
|
|
164
|
+
}
|
|
165
|
+
return { config: { contexts: [], activeContextId: null }, changed: false };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Path to the decision-log Memory Contract spec (mirrors use-learning.js). */
|
|
169
|
+
export const DECISION_SPEC = 'docs/specs/secretary-decision-contract.md';
|
|
170
|
+
|
|
171
|
+
/** Build one `secretary.decision.{id}` Memory Contract value (see docs/specs/secretary-decision-contract.md).
|
|
172
|
+
* The SINGLE source of the decision shape — used by the manual log form, answered Ask cards, and
|
|
173
|
+
* approved guided plans, so every real choice enters the learning loop the same way. Stays `open`
|
|
174
|
+
* (revisitWhen = now + revisitDays) until the tick's review sweep scores it. `active` is the current
|
|
175
|
+
* context ({id,name}) for tagging; `options` is an array; goalRef is optional.
|
|
176
|
+
* @param {{ decision?: string, options?: string[], chosen?: string, rationale?: string, expectedOutcome?: string, goalRef?: string|null, revisitDays?: number|string, active?: { id?: string, name?: string }|null }} [opts]
|
|
177
|
+
*/
|
|
178
|
+
export function buildDecisionRecord(opts = {}) {
|
|
179
|
+
const { decision, options, chosen, rationale, expectedOutcome, goalRef, revisitDays, active } = opts;
|
|
180
|
+
const id = 'd' + Date.now().toString(36) + Math.floor(Math.random() * 1e6).toString(36);
|
|
181
|
+
const days = Number(revisitDays) || 7;
|
|
182
|
+
const revisitWhen = new Date(Date.now() + days * 86400000).toISOString();
|
|
183
|
+
return {
|
|
184
|
+
type: 'secretary.decision', spec: DECISION_SPEC, id,
|
|
185
|
+
decision: String(decision || '').trim(),
|
|
186
|
+
goalRef: goalRef || null,
|
|
187
|
+
options: Array.isArray(options) ? options.filter(Boolean) : [],
|
|
188
|
+
chosen: String(chosen || '').trim(),
|
|
189
|
+
rationale: String(rationale || '').trim(),
|
|
190
|
+
expectedOutcome: String(expectedOutcome || '').trim(),
|
|
191
|
+
revisitWhen,
|
|
192
|
+
actualOutcome: null, score: null, verdict: null, status: 'open',
|
|
193
|
+
reviewedAt: null, attempts: 0, lastError: null,
|
|
194
|
+
contextId: (active && active.id) || '', contextName: (active && active.name) || '',
|
|
195
|
+
createdAt: new Date().toISOString(),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file secretary-policy.js
|
|
3
|
+
* @description The Secretary's operating model (Phase 1): the capability taxonomy + autonomy bands
|
|
4
|
+
* that define what the Secretary may do on its own. Stored on the owner memory key `secretary.config`
|
|
5
|
+
* under `.policy` (NOT the generic agent-directives model — keeps the operating model in one
|
|
6
|
+
* secretary-specific place). This is the CONTRACT later phases enforce (Phase 3 inbox bands, Phase 4
|
|
7
|
+
* autonomous tick read it); for now it is authored + persisted here. Bands: act (do it + log) /
|
|
8
|
+
* draft (prepare, ask to approve) / ask (decision card) / off. `discover` is locked to act + free
|
|
9
|
+
* (read-only, never blocked by stop-spending). Full design: docs/plans/2026-06-23-secretary-feature.md §6/§9.
|
|
10
|
+
* @structure BANDS · SECRETARY_CAPABILITIES · defaultPolicy() · mergePolicy()
|
|
11
|
+
* @usage import { SECRETARY_CAPABILITIES, BANDS, defaultPolicy } from '/js/services/secretary-policy.js';
|
|
12
|
+
* @version-history
|
|
13
|
+
* v0.2.0 — 2026-06-24 — Phase 3d: mark Enterprise-only capabilities (third-party messaging, transacting,
|
|
14
|
+
* spending) with `enterprise: true` — gated/locked in the Community Secretary (which lacks the
|
|
15
|
+
* messages:send / work:request / wallet scopes). The explicit third-party-send gate.
|
|
16
|
+
* v0.1.0 — 2026-06-23 — Phase 1 completion: operating-model taxonomy + bands.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Autonomy bands, least → most restrictive in display order. */
|
|
20
|
+
export const BANDS = ['act', 'draft', 'ask', 'off'];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The Secretary's operating surface. `defaultBand` is the conservative Community default; `locked`
|
|
24
|
+
* capabilities cannot be changed by the user; `free` ones cost no morsels and are never blocked by the
|
|
25
|
+
* stop-spending switch; `costs` flags money/outbound actions (shown with a cost hint in the UI);
|
|
26
|
+
* `enterprise` capabilities require scopes the Community Secretary doesn't have (messages:send /
|
|
27
|
+
* work:request / wallet) — they're gated/locked here and only unlocked by the company `ee/` edition.
|
|
28
|
+
*/
|
|
29
|
+
export const SECRETARY_CAPABILITIES = [
|
|
30
|
+
{ id: 'discover', defaultBand: 'act', locked: true, free: true },
|
|
31
|
+
{ id: 'file_intake', defaultBand: 'act' },
|
|
32
|
+
{ id: 'briefing', defaultBand: 'act' },
|
|
33
|
+
{ id: 'reminders', defaultBand: 'act' },
|
|
34
|
+
{ id: 'curate_knowledge', defaultBand: 'draft' },
|
|
35
|
+
{ id: 'draft_replies', defaultBand: 'draft' },
|
|
36
|
+
{ id: 'create_resource', defaultBand: 'ask' },
|
|
37
|
+
{ id: 'delegate', defaultBand: 'ask', costs: true },
|
|
38
|
+
{ id: 'resource_invoke', defaultBand: 'ask', costs: true, enterprise: true },
|
|
39
|
+
{ id: 'third_party_message', defaultBand: 'ask', costs: true, enterprise: true },
|
|
40
|
+
{ id: 'spend', defaultBand: 'ask', costs: true, enterprise: true },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** The conservative default operating model seeded when the Secretary is hired. */
|
|
44
|
+
export function defaultPolicy() {
|
|
45
|
+
const bands = {};
|
|
46
|
+
for (const c of SECRETARY_CAPABILITIES) bands[c.id] = c.defaultBand;
|
|
47
|
+
return { stopSpending: false, dailyMorselBudget: null, bands };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Merge a stored (possibly partial / older) policy onto the current defaults so new capabilities
|
|
51
|
+
* always have a band and locked ones stay correct. */
|
|
52
|
+
export function mergePolicy(stored) {
|
|
53
|
+
const base = defaultPolicy();
|
|
54
|
+
const p = stored && typeof stored === 'object' ? stored : {};
|
|
55
|
+
const bands = { ...base.bands, ...(p.bands || {}) };
|
|
56
|
+
// Locked capabilities are always forced to their default band.
|
|
57
|
+
for (const c of SECRETARY_CAPABILITIES) if (c.locked) bands[c.id] = c.defaultBand;
|
|
58
|
+
return {
|
|
59
|
+
stopSpending: !!p.stopSpending,
|
|
60
|
+
dailyMorselBudget: (typeof p.dailyMorselBudget === 'number' && p.dailyMorselBudget >= 0) ? p.dailyMorselBudget : null,
|
|
61
|
+
bands,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file secretary-routing.js
|
|
3
|
+
* @description Pure, AI-free cross-context routing for the Secretary's intake (plan §22 Phase-4, P2-E).
|
|
4
|
+
* Classifies a piece of intake text against ALL of the user's Secretary contexts by cheap word
|
|
5
|
+
* overlap (context name + brain purpose + workspace names/purposes), biased by recorded CORRECTIONS
|
|
6
|
+
* (words the owner has previously associated with a context by moving a note there). The result drives
|
|
7
|
+
* the intake hook: a HIGH-confidence match to a NON-active context auto-routes the note there; a
|
|
8
|
+
* LOW-confidence match surfaces an Ask card; an item that belongs to the active context (or has no
|
|
9
|
+
* signal) returns null and is filed normally. Mirrors the backend helpers in
|
|
10
|
+
* src/services/secretary-tick.ts — the node behavior wins on any mismatch.
|
|
11
|
+
* @structure tokenize · scoreContexts · routeIntake · learnCorrection
|
|
12
|
+
* @usage import { routeIntake, learnCorrection } from '/js/services/secretary-routing.js';
|
|
13
|
+
* @version-history v0.1.0 — 2026-06-24 — P2-E: cheap cross-context router + corrections-teach.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Significant words (≥4 chars) from text, lowercased. */
|
|
17
|
+
export function tokenize(text) {
|
|
18
|
+
return Array.from(new Set(String(text || '').toLowerCase().split(/[^a-z0-9äöå]+/i).filter((w) => w.length >= 4)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CORRECTION_BONUS = 5;
|
|
22
|
+
|
|
23
|
+
/** Score each context by word overlap (+ correction bias). Returns [{id,name,organismId,score}] sorted desc. */
|
|
24
|
+
export function scoreContexts(words, contexts, corrections) {
|
|
25
|
+
const corr = corrections || {};
|
|
26
|
+
return (contexts || []).map((ctx) => {
|
|
27
|
+
const hay = [ctx.name, ctx.brain && ctx.brain.purpose, ...((ctx.workspaces || []).map((w) => w.name + ' ' + (w.purpose || '')))]
|
|
28
|
+
.join(' ').toLowerCase();
|
|
29
|
+
let score = 0;
|
|
30
|
+
for (const w of words) {
|
|
31
|
+
if (hay.includes(w)) score += 1;
|
|
32
|
+
if (corr[w] === ctx.id) score += CORRECTION_BONUS;
|
|
33
|
+
}
|
|
34
|
+
return { id: ctx.id, name: ctx.name, organismId: ctx.organismId || null, score };
|
|
35
|
+
}).sort((a, b) => b.score - a.score);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Route intake `text` across all `contexts`. Returns null when the text belongs to the active context
|
|
40
|
+
* or carries no signal (caller files it normally); otherwise `{ contextId, confidence, score }` where
|
|
41
|
+
* confidence is 'high' (auto-route into that non-active context) or 'low' (surface an Ask card).
|
|
42
|
+
*/
|
|
43
|
+
export function routeIntake(text, contexts, activeId, corrections) {
|
|
44
|
+
const t = String(text || '').trim();
|
|
45
|
+
if (t.length < 8 || !Array.isArray(contexts) || contexts.length < 2) return null;
|
|
46
|
+
const words = tokenize(t);
|
|
47
|
+
if (words.length === 0) return null;
|
|
48
|
+
const scored = scoreContexts(words, contexts, corrections);
|
|
49
|
+
const best = scored[0];
|
|
50
|
+
const second = scored[1] || { score: 0 };
|
|
51
|
+
if (!best || best.score === 0) return null;
|
|
52
|
+
const activeScore = (scored.find((s) => s.id === activeId) || { score: 0 }).score;
|
|
53
|
+
if (best.id === activeId) return null; // belongs to the active context → no routing needed
|
|
54
|
+
const margin = best.score - second.score;
|
|
55
|
+
const beatsActive = best.score - activeScore;
|
|
56
|
+
if (best.score >= 2 && beatsActive >= 2 && margin >= 2) {
|
|
57
|
+
return { contextId: best.id, confidence: 'high', score: best.score };
|
|
58
|
+
}
|
|
59
|
+
return { contextId: best.id, confidence: 'low', score: best.score };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Record a correction: associate the text's significant words with `toContextId` (bounded map). */
|
|
63
|
+
export function learnCorrection(corrections, text, toContextId) {
|
|
64
|
+
const next = { ...(corrections || {}) };
|
|
65
|
+
for (const w of tokenize(text)) next[w] = toContextId;
|
|
66
|
+
const keys = Object.keys(next);
|
|
67
|
+
if (keys.length > 200) for (const k of keys.slice(0, keys.length - 200)) delete next[k];
|
|
68
|
+
return next;
|
|
69
|
+
}
|
package/dist/public/spa.html
CHANGED
|
@@ -174,6 +174,19 @@
|
|
|
174
174
|
"/js/services/notebook-plan.js": "/js/services/notebook-plan.js",
|
|
175
175
|
"/js/services/nodes.js": "/js/services/nodes.js",
|
|
176
176
|
"/js/services/organisms.js": "/js/services/organisms.js",
|
|
177
|
+
"/js/services/secretary-policy.js": "/js/services/secretary-policy.js",
|
|
178
|
+
"/js/services/secretary-helpers.js": "/js/services/secretary-helpers.js",
|
|
179
|
+
"/js/services/secretary-routing.js": "/js/services/secretary-routing.js",
|
|
180
|
+
"/views/secretary/cards.js": "/views/secretary/cards.js",
|
|
181
|
+
"/views/secretary/cards-reach.js": "/views/secretary/cards-reach.js",
|
|
182
|
+
"/views/secretary/use-intake.js": "/views/secretary/use-intake.js",
|
|
183
|
+
"/views/secretary/use-guided-plan.js": "/views/secretary/use-guided-plan.js",
|
|
184
|
+
"/views/secretary/use-autonomy.js": "/views/secretary/use-autonomy.js",
|
|
185
|
+
"/views/secretary/use-learning.js": "/views/secretary/use-learning.js",
|
|
186
|
+
"/views/secretary/use-create-resource.js": "/views/secretary/use-create-resource.js",
|
|
187
|
+
"/views/secretary/use-knowledge.js": "/views/secretary/use-knowledge.js",
|
|
188
|
+
"/views/secretary/use-access.js": "/views/secretary/use-access.js",
|
|
189
|
+
"/views/secretary/use-crew.js": "/views/secretary/use-crew.js",
|
|
177
190
|
"/js/services/offers.js": "/js/services/offers.js",
|
|
178
191
|
"/js/services/offers-grouping.js": "/js/services/offers-grouping.js",
|
|
179
192
|
"/js/services/packages.js": "/js/services/packages.js",
|
|
@@ -198,6 +211,7 @@
|
|
|
198
211
|
"/components/Mermaid.js": "/components/Mermaid.js",
|
|
199
212
|
"/components/NotificationBell.js": "/components/NotificationBell.js",
|
|
200
213
|
"/components/CopyButton.js": "/components/CopyButton.js",
|
|
214
|
+
"/components/Toast.js": "/components/Toast.js",
|
|
201
215
|
"/components/ContactCard.js": "/components/ContactCard.js",
|
|
202
216
|
"/components/Spinner.js": "/components/Spinner.js",
|
|
203
217
|
"/components/EmptyState.js": "/components/EmptyState.js",
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file apps-tab.js
|
|
3
|
+
* @description Admin dashboard Applications tab — operator moderation surface for
|
|
4
|
+
* every published app on the node. Lists all apps (across all owners, including
|
|
5
|
+
* parked + operator-hidden), with a hide/restore control. Hiding removes the app
|
|
6
|
+
* from every public surface (catalogue/gallery/search + direct download); the
|
|
7
|
+
* owner still sees it in their "My Apps" with a "moderated by operator: hidden"
|
|
8
|
+
* badge but cannot lift it — only an operator can.
|
|
9
|
+
* @structure AppsAdminTab (default) — load all apps, filter, table, hide-reason +
|
|
10
|
+
* type-to-confirm delete modals.
|
|
11
|
+
* @usage Mounted by the admin dashboard tab router (views/admin.js).
|
|
12
|
+
* @version-history
|
|
13
|
+
* v1.0.0 — 2026-06-24 — Initial: list-all + hide/restore moderation tool.
|
|
14
|
+
* v1.1.0 — 2026-06-25 — Add operator hard-delete (type-to-confirm) — removes an
|
|
15
|
+
* app permanently from the node.
|
|
16
|
+
*/
|
|
17
|
+
import { h } from 'preact';
|
|
18
|
+
import { useState, useEffect, useCallback, useMemo } from 'preact/hooks';
|
|
19
|
+
import htm from 'htm';
|
|
20
|
+
const html = htm.bind(h);
|
|
21
|
+
import { t } from '/js/i18n.js';
|
|
22
|
+
import { escHtml } from '/js/utils.js';
|
|
23
|
+
import { dt, fmtBytes, Spinner, Empty, ErrorBox, useToast, Toast } from './shared.js';
|
|
24
|
+
import { Modal } from '/components/Modal.js';
|
|
25
|
+
import * as adminService from '/js/services/admin.js';
|
|
26
|
+
|
|
27
|
+
export default function AppsAdminTab() {
|
|
28
|
+
const [apps, setApps] = useState(null);
|
|
29
|
+
const [error, setError] = useState(null);
|
|
30
|
+
const [query, setQuery] = useState('');
|
|
31
|
+
const [hiddenOnly, setHiddenOnly] = useState(false);
|
|
32
|
+
// { owner, filename, name, reason } while the hide-reason modal is open
|
|
33
|
+
const [hiding, setHiding] = useState(null);
|
|
34
|
+
// { owner, filename, name, typed } while the type-to-confirm delete modal is open
|
|
35
|
+
const [deleting, setDeleting] = useState(null);
|
|
36
|
+
const [busy, setBusy] = useState(false);
|
|
37
|
+
const [msg, showError, showSuccess, clearMsg] = useToast();
|
|
38
|
+
|
|
39
|
+
const load = useCallback(async () => {
|
|
40
|
+
setError(null);
|
|
41
|
+
try {
|
|
42
|
+
const resp = await adminService.getAdminApps();
|
|
43
|
+
setApps(resp?.data?.apps || []);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
setError(err?.message || String(err));
|
|
46
|
+
setApps([]);
|
|
47
|
+
}
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
useEffect(() => { load(); }, [load]);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const handler = () => { load(); };
|
|
54
|
+
window.addEventListener('aimeat-live-update', handler);
|
|
55
|
+
return () => window.removeEventListener('aimeat-live-update', handler);
|
|
56
|
+
}, [load]);
|
|
57
|
+
|
|
58
|
+
const filtered = useMemo(() => {
|
|
59
|
+
if (!apps) return [];
|
|
60
|
+
const q = query.trim().toLowerCase();
|
|
61
|
+
return apps.filter(a => {
|
|
62
|
+
if (hiddenOnly && !a.operator_hidden) return false;
|
|
63
|
+
if (!q) return true;
|
|
64
|
+
return (a.filename || '').toLowerCase().includes(q)
|
|
65
|
+
|| (a.owner || '').toLowerCase().includes(q)
|
|
66
|
+
|| (a.manifest?.name || '').toLowerCase().includes(q);
|
|
67
|
+
});
|
|
68
|
+
}, [apps, query, hiddenOnly]);
|
|
69
|
+
|
|
70
|
+
const hiddenCount = useMemo(() => (apps || []).filter(a => a.operator_hidden).length, [apps]);
|
|
71
|
+
|
|
72
|
+
const doHide = useCallback(async () => {
|
|
73
|
+
if (!hiding) return;
|
|
74
|
+
setBusy(true);
|
|
75
|
+
try {
|
|
76
|
+
await adminService.moderateApp(hiding.owner, hiding.filename, true, hiding.reason?.trim() || undefined);
|
|
77
|
+
showSuccess(t('admin.apps.hiddenOk'));
|
|
78
|
+
setHiding(null);
|
|
79
|
+
await load();
|
|
80
|
+
} catch (err) {
|
|
81
|
+
showError(err?.message || String(err));
|
|
82
|
+
} finally {
|
|
83
|
+
setBusy(false);
|
|
84
|
+
}
|
|
85
|
+
}, [hiding, load, showSuccess, showError]);
|
|
86
|
+
|
|
87
|
+
const doRestore = useCallback(async (app) => {
|
|
88
|
+
try {
|
|
89
|
+
await adminService.moderateApp(app.owner, app.filename, false);
|
|
90
|
+
showSuccess(t('admin.apps.restoredOk'));
|
|
91
|
+
await load();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
showError(err?.message || String(err));
|
|
94
|
+
}
|
|
95
|
+
}, [load, showSuccess, showError]);
|
|
96
|
+
|
|
97
|
+
const doDelete = useCallback(async () => {
|
|
98
|
+
if (!deleting || deleting.typed !== deleting.filename) return;
|
|
99
|
+
setBusy(true);
|
|
100
|
+
try {
|
|
101
|
+
await adminService.deleteAppAdmin(deleting.owner, deleting.filename);
|
|
102
|
+
showSuccess(t('admin.apps.deletedOk'));
|
|
103
|
+
setDeleting(null);
|
|
104
|
+
await load();
|
|
105
|
+
} catch (err) {
|
|
106
|
+
showError(err?.message || String(err));
|
|
107
|
+
} finally {
|
|
108
|
+
setBusy(false);
|
|
109
|
+
}
|
|
110
|
+
}, [deleting, load, showSuccess, showError]);
|
|
111
|
+
|
|
112
|
+
return html`
|
|
113
|
+
${msg && html`<${Toast} type=${msg.type} text=${msg.text} onDismiss=${clearMsg} />`}
|
|
114
|
+
<p class="adm-text-sm adm-text-dim adm-mb-md">${t('admin.apps.desc')}</p>
|
|
115
|
+
|
|
116
|
+
<div class="adm-card">
|
|
117
|
+
<div class="adm-flex-between adm-mb-md">
|
|
118
|
+
<h2>${t('admin.apps.title')} ${hiddenCount > 0 ? html`<span class="badge badge-warn">${hiddenCount} ${t('admin.apps.hiddenChip')}</span>` : ''}</h2>
|
|
119
|
+
<div class="adm-flex adm-apps-filters">
|
|
120
|
+
<input class="adm-input" placeholder=${t('admin.apps.searchPlaceholder')}
|
|
121
|
+
value=${query} onInput=${e => setQuery(e.target.value)} />
|
|
122
|
+
<label class="adm-text-sm adm-apps-toggle">
|
|
123
|
+
<input type="checkbox" checked=${hiddenOnly} onChange=${e => setHiddenOnly(e.target.checked)} />
|
|
124
|
+
${t('admin.apps.hiddenOnly')}
|
|
125
|
+
</label>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
${error && html`<${ErrorBox} message=${error} />`}
|
|
130
|
+
${apps === null ? html`<${Spinner} />`
|
|
131
|
+
: filtered.length === 0 ? html`<${Empty} text=${t('admin.apps.empty')} />`
|
|
132
|
+
: html`
|
|
133
|
+
<div class="scrollable">
|
|
134
|
+
<table>
|
|
135
|
+
<thead><tr>
|
|
136
|
+
<th>${t('admin.apps.colApp')}</th>
|
|
137
|
+
<th>${t('admin.apps.colOwner')}</th>
|
|
138
|
+
<th>${t('admin.apps.colSize')}</th>
|
|
139
|
+
<th>${t('admin.apps.colDownloads')}</th>
|
|
140
|
+
<th>${t('admin.apps.colStatus')}</th>
|
|
141
|
+
<th></th>
|
|
142
|
+
</tr></thead>
|
|
143
|
+
<tbody>
|
|
144
|
+
${filtered.map(a => html`
|
|
145
|
+
<tr class=${a.operator_hidden ? 'adm-apps-row-hidden' : ''}>
|
|
146
|
+
<td>
|
|
147
|
+
<div>${escHtml(a.manifest?.name || a.filename)}</div>
|
|
148
|
+
<div class="adm-text-sm adm-text-dim mono">${escHtml(a.filename)}</div>
|
|
149
|
+
</td>
|
|
150
|
+
<td class="mono">${escHtml(a.owner)}</td>
|
|
151
|
+
<td>${fmtBytes(a.size || 0)}</td>
|
|
152
|
+
<td>${a.downloads || 0}</td>
|
|
153
|
+
<td>
|
|
154
|
+
${a.operator_hidden
|
|
155
|
+
? html`<span class="badge badge-danger">\u{1F6AB} ${t('admin.apps.statusHidden')}</span>
|
|
156
|
+
${a.operator_hide_reason ? html`<div class="adm-text-sm adm-text-dim">${escHtml(a.operator_hide_reason)}</div>` : ''}
|
|
157
|
+
${a.operator_hidden_by ? html`<div class="adm-text-sm adm-text-dim">${t('admin.apps.byAt', { by: a.operator_hidden_by, at: dt(a.operator_hidden_at) })}</div>` : ''}`
|
|
158
|
+
: a.parked
|
|
159
|
+
? html`<span class="badge badge-dim">\u{1F17F}️ ${t('admin.apps.statusParked')}</span>`
|
|
160
|
+
: html`<span class="badge badge-success">${t('admin.apps.statusLive')}</span>`}
|
|
161
|
+
</td>
|
|
162
|
+
<td>
|
|
163
|
+
<div class="adm-flex adm-apps-rowactions">
|
|
164
|
+
${a.operator_hidden
|
|
165
|
+
? html`<button class="adm-btn-sm adm-btn-success" onClick=${() => doRestore(a)}>${t('admin.apps.restore')}</button>`
|
|
166
|
+
: html`<button class="adm-btn-sm adm-btn-danger" onClick=${() => setHiding({ owner: a.owner, filename: a.filename, name: a.manifest?.name || a.filename, reason: '' })}>${t('admin.apps.hide')}</button>`}
|
|
167
|
+
<button class="adm-btn-sm adm-btn-danger-solid" onClick=${() => setDeleting({ owner: a.owner, filename: a.filename, name: a.manifest?.name || a.filename, typed: '' })}>${t('admin.apps.delete')}</button>
|
|
168
|
+
</div>
|
|
169
|
+
</td>
|
|
170
|
+
</tr>
|
|
171
|
+
`)}
|
|
172
|
+
</tbody>
|
|
173
|
+
</table>
|
|
174
|
+
</div>
|
|
175
|
+
`}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<${Modal} open=${!!hiding} onClose=${() => setHiding(null)} title=${t('admin.apps.hideTitle')}>
|
|
179
|
+
${hiding && html`
|
|
180
|
+
<p>${t('admin.apps.hideConfirm')} <strong>${escHtml(hiding.name)}</strong> (<span class="mono">${escHtml(hiding.owner)}</span>)?</p>
|
|
181
|
+
<p class="adm-text-sm adm-text-dim">${t('admin.apps.hideExplain')}</p>
|
|
182
|
+
<label class="adm-text-sm adm-mb-half">${t('admin.apps.reasonLabel')}</label>
|
|
183
|
+
<input class="adm-input adm-input-full" value=${hiding.reason}
|
|
184
|
+
placeholder=${t('admin.apps.reasonPlaceholder')}
|
|
185
|
+
onInput=${e => setHiding({ ...hiding, reason: e.target.value })} />
|
|
186
|
+
<div class="adm-flex adm-mt-md adm-apps-modal-actions">
|
|
187
|
+
<button class="btn-ghost" onClick=${() => setHiding(null)}>${t('common.cancel')}</button>
|
|
188
|
+
<button class="btn-danger-solid" disabled=${busy} onClick=${doHide}>${t('admin.apps.hide')}</button>
|
|
189
|
+
</div>
|
|
190
|
+
`}
|
|
191
|
+
<//>
|
|
192
|
+
|
|
193
|
+
<${Modal} open=${!!deleting} onClose=${() => setDeleting(null)} title=${t('admin.apps.deleteTitle')}>
|
|
194
|
+
${deleting && html`
|
|
195
|
+
<p>${t('admin.apps.deleteConfirm')} <strong>${escHtml(deleting.name)}</strong> (<span class="mono">${escHtml(deleting.owner)}</span>)?</p>
|
|
196
|
+
<p class="adm-text-sm adm-text-error">${t('admin.apps.deleteWarn')}</p>
|
|
197
|
+
<label class="adm-text-sm adm-mb-half">${t('admin.apps.deleteTypeLabel', { filename: deleting.filename })}</label>
|
|
198
|
+
<input class="adm-input adm-input-full mono" value=${deleting.typed}
|
|
199
|
+
placeholder=${deleting.filename}
|
|
200
|
+
onInput=${e => setDeleting({ ...deleting, typed: e.target.value })} />
|
|
201
|
+
<div class="adm-flex adm-mt-md adm-apps-modal-actions">
|
|
202
|
+
<button class="btn-ghost" onClick=${() => setDeleting(null)}>${t('common.cancel')}</button>
|
|
203
|
+
<button class="btn-danger-solid" disabled=${busy || deleting.typed !== deleting.filename} onClick=${doDelete}>${t('admin.apps.delete')}</button>
|
|
204
|
+
</div>
|
|
205
|
+
`}
|
|
206
|
+
<//>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* @version-history
|
|
7
7
|
* v1.1.0 — 2026-06-18 — Debounce + silence SSE-driven refresh so busy nodes don't flicker "Loading…".
|
|
8
8
|
* v1.1.1 — 2026-06-19 — JSDoc type annotations for frontend type-checking
|
|
9
|
+
* v1.2.0 — 2026-06-24 — Add Applications moderation tab (operator hide/restore apps).
|
|
9
10
|
*/
|
|
10
11
|
import { h } from 'preact';
|
|
11
12
|
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
@@ -59,6 +60,7 @@ import AgentTasksAdminTab from './admin/agent-tasks-tab.js';
|
|
|
59
60
|
import SharingGroupsAdminTab from './admin/sharing-groups-tab.js';
|
|
60
61
|
import AgentIntegrationAdminTab from './admin/agent-integration-tab.js';
|
|
61
62
|
import SubdomainsAdminTab from './admin/subdomains-tab.js';
|
|
63
|
+
import AppsAdminTab from './admin/apps-tab.js';
|
|
62
64
|
|
|
63
65
|
// ── Sidebar nav structure ──
|
|
64
66
|
const NAV_GROUPS = [
|
|
@@ -92,6 +94,7 @@ const NAV_GROUPS = [
|
|
|
92
94
|
{ id: 'agent-tasks', icon: '\u{1F4CB}', key: 'dashboard.agentTasksTab', component: AgentTasksAdminTab },
|
|
93
95
|
{ id: 'sharing-groups', icon: '\u{1F465}', key: 'dashboard.sharingGroupsTab', component: SharingGroupsAdminTab },
|
|
94
96
|
{ id: 'capabilities', icon: '⚡', key: 'capabilities.adminTitle', component: CapabilitiesAdminTab },
|
|
97
|
+
{ id: 'apps', icon: '\u{1F4F1}', key: 'admin.apps.title', component: AppsAdminTab },
|
|
95
98
|
]},
|
|
96
99
|
{ key: 'dashboard.navInfrastructure', items: [
|
|
97
100
|
{ id: 'email', icon: '\u2709', key: 'dashboard.email', component: EmailTab },
|