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.
Files changed (208) hide show
  1. package/dist/locales/en.json +359 -2
  2. package/dist/locales/fi.json +359 -2
  3. package/dist/public/css/views/admin.css +10 -0
  4. package/dist/public/css/views/agents-detail.css +8 -0
  5. package/dist/public/css/views/my-company.css +41 -0
  6. package/dist/public/css/views/profile.css +7 -1
  7. package/dist/public/css/views/secretary.css +482 -1
  8. package/dist/public/js/services/admin.js +7 -0
  9. package/dist/public/js/services/agents.js +18 -3
  10. package/dist/public/js/services/generator-prompts-build.js +5 -1
  11. package/dist/public/js/services/organisms.js +16 -0
  12. package/dist/public/js/services/secretary-helpers.js +197 -0
  13. package/dist/public/js/services/secretary-policy.js +63 -0
  14. package/dist/public/js/services/secretary-routing.js +69 -0
  15. package/dist/public/spa.html +14 -0
  16. package/dist/public/views/admin/apps-tab.js +208 -0
  17. package/dist/public/views/admin.js +3 -0
  18. package/dist/public/views/my-company.js +247 -3
  19. package/dist/public/views/profile/agents/agent-card.js +15 -0
  20. package/dist/public/views/profile/apps-tab.js +4 -0
  21. package/dist/public/views/profile/extensions-tab.js +30 -1
  22. package/dist/public/views/profile/generator-settings.js +28 -1
  23. package/dist/public/views/secretary/cards-reach.js +235 -0
  24. package/dist/public/views/secretary/cards.js +419 -0
  25. package/dist/public/views/secretary/use-access.js +95 -0
  26. package/dist/public/views/secretary/use-autonomy.js +92 -0
  27. package/dist/public/views/secretary/use-create-resource.js +86 -0
  28. package/dist/public/views/secretary/use-crew.js +173 -0
  29. package/dist/public/views/secretary/use-guided-plan.js +112 -0
  30. package/dist/public/views/secretary/use-intake.js +332 -0
  31. package/dist/public/views/secretary/use-knowledge.js +104 -0
  32. package/dist/public/views/secretary/use-learning.js +140 -0
  33. package/dist/public/views/secretary.js +327 -34
  34. package/dist/src/cli/connect/mcp/tools/agent-management.d.ts +8 -8
  35. package/dist/src/cli/connect/mcp/tools/agent-management.js +1 -1
  36. package/dist/src/cli/connect/mcp/tools/agent-management.js.map +1 -1
  37. package/dist/src/enterprise/loader.d.ts.map +1 -1
  38. package/dist/src/enterprise/loader.js +2 -0
  39. package/dist/src/enterprise/loader.js.map +1 -1
  40. package/dist/src/enterprise/provider.d.ts +67 -1
  41. package/dist/src/enterprise/provider.d.ts.map +1 -1
  42. package/dist/src/enterprise/provider.js +17 -0
  43. package/dist/src/enterprise/provider.js.map +1 -1
  44. package/dist/src/generated/api-types.d.ts +744 -49
  45. package/dist/src/generated/api-types.d.ts.map +1 -1
  46. package/dist/src/generated/prisma-postgres/edge.js +7 -3
  47. package/dist/src/generated/prisma-postgres/index-browser.js +4 -0
  48. package/dist/src/generated/prisma-postgres/index.d.ts +117 -1
  49. package/dist/src/generated/prisma-postgres/index.js +7 -3
  50. package/dist/src/generated/prisma-postgres/package.json +1 -1
  51. package/dist/src/generated/prisma-postgres/schema.prisma +16 -12
  52. package/dist/src/generated/prisma-postgres/wasm.js +7 -3
  53. package/dist/src/mcp/agent-management.d.ts +2 -0
  54. package/dist/src/mcp/agent-management.d.ts.map +1 -1
  55. package/dist/src/mcp/agent-management.js +5 -1
  56. package/dist/src/mcp/agent-management.js.map +1 -1
  57. package/dist/src/mcp/catalog/definitions.js +3 -3
  58. package/dist/src/mcp/catalog/definitions.js.map +1 -1
  59. package/dist/src/mcp/catalog/scopes.d.ts +46 -0
  60. package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
  61. package/dist/src/mcp/catalog/scopes.js +81 -0
  62. package/dist/src/mcp/catalog/scopes.js.map +1 -1
  63. package/dist/src/mcp/catalog/surfaces.js +2 -2
  64. package/dist/src/mcp/catalog/surfaces.js.map +1 -1
  65. package/dist/src/routes/agent-directives.d.ts +12 -1
  66. package/dist/src/routes/agent-directives.d.ts.map +1 -1
  67. package/dist/src/routes/agent-directives.js +30 -4
  68. package/dist/src/routes/agent-directives.js.map +1 -1
  69. package/dist/src/routes/agents.d.ts +3 -0
  70. package/dist/src/routes/agents.d.ts.map +1 -1
  71. package/dist/src/routes/agents.js +11 -4
  72. package/dist/src/routes/agents.js.map +1 -1
  73. package/dist/src/routes/ai.d.ts +2 -0
  74. package/dist/src/routes/ai.d.ts.map +1 -1
  75. package/dist/src/routes/ai.js +15 -2
  76. package/dist/src/routes/ai.js.map +1 -1
  77. package/dist/src/routes/apps.d.ts +14 -0
  78. package/dist/src/routes/apps.d.ts.map +1 -1
  79. package/dist/src/routes/apps.js +168 -3
  80. package/dist/src/routes/apps.js.map +1 -1
  81. package/dist/src/routes/extensions.d.ts +4 -0
  82. package/dist/src/routes/extensions.d.ts.map +1 -1
  83. package/dist/src/routes/extensions.js +75 -11
  84. package/dist/src/routes/extensions.js.map +1 -1
  85. package/dist/src/routes/libs.d.ts +6 -0
  86. package/dist/src/routes/libs.d.ts.map +1 -1
  87. package/dist/src/routes/libs.js +163 -0
  88. package/dist/src/routes/libs.js.map +1 -1
  89. package/dist/src/routes/oauth-login.d.ts +16 -6
  90. package/dist/src/routes/oauth-login.d.ts.map +1 -1
  91. package/dist/src/routes/oauth-login.js +290 -51
  92. package/dist/src/routes/oauth-login.js.map +1 -1
  93. package/dist/src/routes/openrouter.d.ts +2 -0
  94. package/dist/src/routes/openrouter.d.ts.map +1 -1
  95. package/dist/src/routes/openrouter.js +8 -1
  96. package/dist/src/routes/openrouter.js.map +1 -1
  97. package/dist/src/routes/organism-templates.d.ts +22 -0
  98. package/dist/src/routes/organism-templates.d.ts.map +1 -0
  99. package/dist/src/routes/organism-templates.js +110 -0
  100. package/dist/src/routes/organism-templates.js.map +1 -0
  101. package/dist/src/routes/schedules.d.ts +2 -0
  102. package/dist/src/routes/schedules.d.ts.map +1 -1
  103. package/dist/src/routes/schedules.js +3 -1
  104. package/dist/src/routes/schedules.js.map +1 -1
  105. package/dist/src/routes/specialists.d.ts +28 -0
  106. package/dist/src/routes/specialists.d.ts.map +1 -0
  107. package/dist/src/routes/specialists.js +227 -0
  108. package/dist/src/routes/specialists.js.map +1 -0
  109. package/dist/src/routes/subdomains.d.ts +3 -0
  110. package/dist/src/routes/subdomains.d.ts.map +1 -1
  111. package/dist/src/routes/subdomains.js +12 -2
  112. package/dist/src/routes/subdomains.js.map +1 -1
  113. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  114. package/dist/src/server-bootstrap/routes-loader.js +11 -3
  115. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  116. package/dist/src/services/ai-completion.d.ts +8 -0
  117. package/dist/src/services/ai-completion.d.ts.map +1 -1
  118. package/dist/src/services/ai-completion.js +7 -1
  119. package/dist/src/services/ai-completion.js.map +1 -1
  120. package/dist/src/services/discovery/setup.d.ts +2 -1
  121. package/dist/src/services/discovery/setup.d.ts.map +1 -1
  122. package/dist/src/services/discovery/setup.js +2 -0
  123. package/dist/src/services/discovery/setup.js.map +1 -1
  124. package/dist/src/services/discovery/sources/memory-source.d.ts +2 -0
  125. package/dist/src/services/discovery/sources/memory-source.d.ts.map +1 -1
  126. package/dist/src/services/discovery/sources/memory-source.js +8 -1
  127. package/dist/src/services/discovery/sources/memory-source.js.map +1 -1
  128. package/dist/src/services/discovery/sources/template-source.d.ts +17 -0
  129. package/dist/src/services/discovery/sources/template-source.d.ts.map +1 -0
  130. package/dist/src/services/discovery/sources/template-source.js +51 -0
  131. package/dist/src/services/discovery/sources/template-source.js.map +1 -0
  132. package/dist/src/services/discovery/types.d.ts +1 -1
  133. package/dist/src/services/discovery/types.d.ts.map +1 -1
  134. package/dist/src/services/extension-secrets.d.ts +63 -0
  135. package/dist/src/services/extension-secrets.d.ts.map +1 -0
  136. package/dist/src/services/extension-secrets.js +122 -0
  137. package/dist/src/services/extension-secrets.js.map +1 -0
  138. package/dist/src/services/handbooks/appdev.d.ts +1 -1
  139. package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
  140. package/dist/src/services/handbooks/appdev.js +1 -1
  141. package/dist/src/services/openrouter.d.ts +1 -1
  142. package/dist/src/services/openrouter.d.ts.map +1 -1
  143. package/dist/src/services/openrouter.js +18 -2
  144. package/dist/src/services/openrouter.js.map +1 -1
  145. package/dist/src/services/organism-export.d.ts +2 -0
  146. package/dist/src/services/organism-export.d.ts.map +1 -1
  147. package/dist/src/services/organism-export.js +24 -5
  148. package/dist/src/services/organism-export.js.map +1 -1
  149. package/dist/src/services/organism-import.d.ts +11 -0
  150. package/dist/src/services/organism-import.d.ts.map +1 -1
  151. package/dist/src/services/organism-import.js +16 -1
  152. package/dist/src/services/organism-import.js.map +1 -1
  153. package/dist/src/services/organism-template.d.ts +160 -0
  154. package/dist/src/services/organism-template.d.ts.map +1 -0
  155. package/dist/src/services/organism-template.js +202 -0
  156. package/dist/src/services/organism-template.js.map +1 -0
  157. package/dist/src/services/scheduler.d.ts +45 -0
  158. package/dist/src/services/scheduler.d.ts.map +1 -1
  159. package/dist/src/services/scheduler.js +358 -5
  160. package/dist/src/services/scheduler.js.map +1 -1
  161. package/dist/src/services/secretary-tick.d.ts +127 -0
  162. package/dist/src/services/secretary-tick.d.ts.map +1 -0
  163. package/dist/src/services/secretary-tick.js +169 -0
  164. package/dist/src/services/secretary-tick.js.map +1 -0
  165. package/dist/src/services/secretary.d.ts +98 -2
  166. package/dist/src/services/secretary.d.ts.map +1 -1
  167. package/dist/src/services/secretary.js +130 -12
  168. package/dist/src/services/secretary.js.map +1 -1
  169. package/dist/src/services/specialist-policy.d.ts +39 -0
  170. package/dist/src/services/specialist-policy.d.ts.map +1 -0
  171. package/dist/src/services/specialist-policy.js +59 -0
  172. package/dist/src/services/specialist-policy.js.map +1 -0
  173. package/dist/src/services/specialist.d.ts +77 -0
  174. package/dist/src/services/specialist.d.ts.map +1 -0
  175. package/dist/src/services/specialist.js +137 -0
  176. package/dist/src/services/specialist.js.map +1 -0
  177. package/dist/src/services/structure-overview.d.ts +3 -0
  178. package/dist/src/services/structure-overview.d.ts.map +1 -1
  179. package/dist/src/services/structure-overview.js +20 -10
  180. package/dist/src/services/structure-overview.js.map +1 -1
  181. package/dist/src/services/system-agent.d.ts +39 -0
  182. package/dist/src/services/system-agent.d.ts.map +1 -0
  183. package/dist/src/services/system-agent.js +43 -0
  184. package/dist/src/services/system-agent.js.map +1 -0
  185. package/dist/src/storage/interface.d.ts +7 -2
  186. package/dist/src/storage/interface.d.ts.map +1 -1
  187. package/dist/src/storage/interface.js.map +1 -1
  188. package/dist/src/storage/providers/mongodb/index.d.ts +5 -0
  189. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  190. package/dist/src/storage/providers/mongodb/index.js +31 -4
  191. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  192. package/dist/src/storage/providers/sqlite/index.d.ts +5 -0
  193. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  194. package/dist/src/storage/providers/sqlite/index.js +26 -6
  195. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  196. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  197. package/dist/src/storage/providers/sqlite/schema.js +8 -0
  198. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  199. package/dist/src/storage/repositories/app.repository.d.ts +17 -0
  200. package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
  201. package/dist/src/utils/app-badge.d.ts +26 -0
  202. package/dist/src/utils/app-badge.d.ts.map +1 -0
  203. package/dist/src/utils/app-badge.js +56 -0
  204. package/dist/src/utils/app-badge.js.map +1 -0
  205. package/dist/static/app-catalog.html +315 -71
  206. package/package.json +1 -1
  207. package/prisma/schema.postgres.prisma +4 -0
  208. package/prisma/schema.prisma +4 -0
@@ -0,0 +1,419 @@
1
+ /**
2
+ * @file secretary/cards.js
3
+ * @description Presentational cards for the Secretary view, split out of views/secretary.js to keep the
4
+ * main component focused on state + logic (and under the file-size limit). Each export is a PURE render
5
+ * function taking a props bag — no hooks, no state — so the view owns all state/handlers and passes
6
+ * them in. Cards: contextSwitcher · hirePanel · chatCard · findCard · noteCard · decisionsCard ·
7
+ * brainCard · operatingCard · historyCard · metaCard. See views/secretary.js + docs/plans/2026-06-23-secretary-feature.md.
8
+ * @structure one exported render function per card (props in → htm out)
9
+ * @usage import { chatCard, findCard, ... } from '/views/secretary/cards.js'; ... ${chatCard({...})}
10
+ * @version-history
11
+ * v0.6.0 — 2026-06-24 — P3-A: chatCard gains a doc/image attach control (📎) + an in-flight /
12
+ * filed status line, driven by the intake hook's handleAttach.
13
+ * v0.5.0 — 2026-06-24 — P2-E: noteCard shows the cross-context route banner (auto-route to a
14
+ * non-active context on high confidence; "unsure" hint on low) driven by the intake hook.
15
+ * v0.4.0 — 2026-06-24 — P1: automationCard shows remaining daily morsel budget; metaCard shows a
16
+ * self-facing reliability chip (mean reviewed-decision score); decisionsCard handles tick `text`.
17
+ * v0.3.0 — 2026-06-24 — Phase 5: goalsCard + decisionLogCard (learning loop).
18
+ * v0.2.0 — 2026-06-24 — Phase 4: feedCard + automationCard.
19
+ * v0.1.0 — 2026-06-24 — Extract cards from views/secretary.js (Phase 3 cleanup).
20
+ */
21
+ import { h } from 'preact';
22
+ import htm from 'htm';
23
+ const html = htm.bind(h);
24
+ import { t } from '/js/i18n.js';
25
+ import { escHtml } from '/js/utils.js';
26
+ import { CopyButton } from '/components/CopyButton.js';
27
+ import { SECRETARY_CAPABILITIES, BANDS } from '/js/services/secretary-policy.js';
28
+ import { buildInterviewPrompt } from '/js/services/secretary-helpers.js';
29
+
30
+ /** Context "hats" switcher chips + add. */
31
+ export function contextSwitcher(p) {
32
+ return html`
33
+ <div class="sec-ctx-bar">
34
+ ${p.contexts.map((c) => html`
35
+ <button class="sec-ctx-chip ${c.id === p.activeId ? 'active' : ''}" key=${c.id} onClick=${() => p.switchContext(c.id)}>${escHtml(c.name)}</button>`)}
36
+ <button class="sec-ctx-add" title=${t('secretary.addContext')} onClick=${p.openAdd}>+</button>
37
+ </div>`;
38
+ }
39
+
40
+ /** Hire / re-run onboarding: external prompt-driven interview + in-app generate + paste→apply. */
41
+ export function hirePanel(p) {
42
+ return html`
43
+ <section class="sec-card sec-hire">
44
+ <div class="sec-card-head">
45
+ <h2 class="sec-h2">${p.firstEver ? t('secretary.hireTitle') : (p.hireMode === 'edit' ? t('secretary.rerun') : t('secretary.addContextTitle'))}</h2>
46
+ ${!p.firstEver && html`<button class="btn-ghost btn-sm" onClick=${p.onCancel}>${t('secretary.cancel')}</button>`}
47
+ </div>
48
+ <p class="sec-desc">${t('secretary.hireIntro')}</p>
49
+ <div class="sec-opt">
50
+ <div class="sec-opt-h">${t('secretary.optionA')}</div>
51
+ <div class="sec-prompt-box">${buildInterviewPrompt(p.owner)}</div>
52
+ <${CopyButton} text=${buildInterviewPrompt(p.owner)} className="btn-outline btn-sm" label=${t('secretary.copyPrompt')} copiedLabel=${'✓ ' + t('secretary.copied')} />
53
+ </div>
54
+ <div class="sec-opt">
55
+ <div class="sec-opt-h">${t('secretary.optionB')}</div>
56
+ <textarea class="sec-paste" rows="3" placeholder=${t('secretary.needsPlaceholder')} value=${p.needs} onInput=${(e) => p.setNeeds(e.target.value)}></textarea>
57
+ <button class="btn-outline btn-sm" disabled=${!p.needs.trim() || p.generating} onClick=${p.generateInApp}>${p.generating ? t('secretary.generating') : t('secretary.generate')}</button>
58
+ </div>
59
+ <div class="sec-step"><span class="sec-step-n">→</span> ${t('secretary.hireStep2')}</div>
60
+ <textarea class="sec-paste" rows="7" placeholder=${t('secretary.pastePlaceholder')} value=${p.result} onInput=${(e) => p.setResult(e.target.value)}></textarea>
61
+ <div class="sec-actions">
62
+ <button class="btn-primary" disabled=${!p.result.trim() || p.applying} onClick=${p.applyResult}>${p.applying ? t('secretary.applying') : (p.firstEver || p.hireMode === 'new' ? t('secretary.apply') : t('secretary.applyEdit'))}</button>
63
+ </div>
64
+ </section>`;
65
+ }
66
+
67
+ /** Context-aware chat (per active context) + the cheap routing suggestion + doc/image attach (P3-A). */
68
+ export function chatCard(p) {
69
+ const canAttach = typeof p.onAttach === 'function' && p.canAttach !== false;
70
+ return html`
71
+ <section class="sec-card sec-chat">
72
+ <h2 class="sec-h2">${t('secretary.chatTitle')} · ${escHtml(p.activeName)}</h2>
73
+ <div class="sec-chat-log">
74
+ ${p.chat.length === 0
75
+ ? html`<div class="sec-hint">${t('secretary.chatEmpty')}</div>`
76
+ : p.chat.map((m, i) => html`<div class="sec-msg sec-msg-${m.role}" key=${i}>${escHtml(m.content)}</div>`)}
77
+ ${p.chatSending ? html`<div class="sec-msg sec-msg-assistant sec-msg-pending">…</div>` : null}
78
+ </div>
79
+ ${p.routeSuggestion ? html`
80
+ <div class="sec-route-hint">
81
+ <span>${t('secretary.routeSuggest')} <strong>${escHtml(p.routeSuggestion.name)}</strong></span>
82
+ <button class="btn-ghost btn-sm" onClick=${() => p.switchContext(p.routeSuggestion.id)}>${t('secretary.routeSwitch')}</button>
83
+ </div>` : null}
84
+ ${canAttach && p.attaching ? html`<div class="sec-hint sec-attach-status">${t('secretary.attaching')}</div>` : null}
85
+ ${canAttach && !p.attaching && p.attachResult ? html`
86
+ <div class="sec-route-hint"><span>${t('secretary.attachFiled')} <strong>${escHtml(p.attachResult.wsName)}</strong>${p.attachResult.name ? html` — ${escHtml(p.attachResult.name)}` : null}</span></div>` : null}
87
+ <div class="sec-chat-input">
88
+ <textarea rows="2" placeholder=${t('secretary.chatPlaceholder')} value=${p.chatInput}
89
+ onInput=${(e) => p.setChatInput(e.target.value)}
90
+ onKeyDown=${(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); p.sendChat(); } }}></textarea>
91
+ ${canAttach ? html`
92
+ <label class="btn-outline btn-sm sec-attach-btn ${p.attaching ? 'disabled' : ''}" title=${t('secretary.attachHint')}>
93
+ 📎 ${t('secretary.attach')}
94
+ <input type="file" accept="image/*,.pdf,.txt,.md,.doc,.docx,.csv" hidden disabled=${p.attaching}
95
+ onChange=${(e) => { const f = e.target.files && e.target.files[0]; e.target.value = ''; if (f) p.onAttach(f); }} />
96
+ </label>` : null}
97
+ <button class="btn-primary" disabled=${!p.chatInput.trim() || p.chatSending} onClick=${p.sendChat}>
98
+ ${p.chatSending ? t('secretary.chatSending') : t('secretary.chatSend')}
99
+ </button>
100
+ </div>
101
+ </section>`;
102
+ }
103
+
104
+ /** Resource finder via aimeat_discover. */
105
+ export function findCard(p) {
106
+ return html`
107
+ <section class="sec-card sec-find">
108
+ <h2 class="sec-h2">${t('secretary.findTitle')}</h2>
109
+ <p class="sec-hint">${t('secretary.findHint')}</p>
110
+ <div class="sec-find-bar">
111
+ <input class="sec-find-in" placeholder=${t('secretary.findPlaceholder')} value=${p.findQ}
112
+ onInput=${(e) => p.setFindQ(e.target.value)}
113
+ onKeyDown=${(e) => { if (e.key === 'Enter') { e.preventDefault(); p.doFind(); } }} />
114
+ <select class="sec-band" value=${p.findScope} onChange=${(e) => p.setFindScope(e.target.value)}>
115
+ <option value="public" selected=${p.findScope === 'public'}>${t('secretary.findScopePublic')}</option>
116
+ <option value="own" selected=${p.findScope === 'own'}>${t('secretary.findScopeMine')}</option>
117
+ </select>
118
+ <button class="btn-primary" disabled=${p.finding} onClick=${p.doFind}>${p.finding ? t('secretary.findSearching') : t('secretary.findSearch')}</button>
119
+ </div>
120
+ ${p.findResults !== null && html`
121
+ <div class="sec-find-results">
122
+ ${p.findResults.length === 0
123
+ ? html`<div class="sec-hint">${t('secretary.findEmpty')}</div>`
124
+ : p.findResults.map((e, i) => html`
125
+ <div class="sec-find-row" key=${i}>
126
+ <span class="sec-find-type">${escHtml(e.type)}</span>
127
+ <div class="sec-find-main">
128
+ <div class="sec-find-title">${e.url ? html`<a href=${e.url} target="_blank" rel="noreferrer">${escHtml(e.title)}</a>` : escHtml(e.title)}</div>
129
+ ${e.description ? html`<div class="sec-find-desc">${escHtml(String(e.description).slice(0, 160))}</div>` : null}
130
+ </div>
131
+ </div>`)}
132
+ </div>`}
133
+ </section>`;
134
+ }
135
+
136
+ /** Save-a-note composer (file into a workspace) + Ask-in-inbox (Ask band). */
137
+ export function noteCard(p) {
138
+ return html`
139
+ <section class="sec-card sec-note">
140
+ <h2 class="sec-h2">${t('secretary.noteTitle')}</h2>
141
+ <p class="sec-hint">${t('secretary.noteHint')}</p>
142
+ <textarea class="sec-paste" rows="3" placeholder=${t('secretary.notePlaceholder')} value=${p.noteText} onInput=${(e) => p.setNoteText(e.target.value)}></textarea>
143
+ ${p.route && p.route.confidence === 'high' ? html`
144
+ <div class="sec-route-hint">
145
+ <span>${t('secretary.routeBelongs')} <strong>${escHtml(p.route.name)}</strong></span>
146
+ <button class="btn-outline btn-sm" disabled=${p.noteSaving} onClick=${p.autoRouteNote}>${t('secretary.routeFileThere')}</button>
147
+ </div>` : p.route && p.route.confidence === 'low' ? html`
148
+ <div class="sec-route-hint"><span class="sec-hint">${t('secretary.routeUnsure')}</span></div>` : null}
149
+ <div class="sec-note-bar">
150
+ <label class="sec-hint">${t('secretary.noteInto')}</label>
151
+ <select class="sec-band" value=${p.effectiveWsId} onChange=${(e) => p.setNoteWsId(e.target.value)}>
152
+ ${p.wsList.map((w) => html`<option value=${w.id} selected=${p.effectiveWsId === w.id}>${escHtml(w.name)}</option>`)}
153
+ </select>
154
+ <button class="btn-primary" disabled=${!p.noteText.trim() || p.noteSaving} onClick=${p.saveNote}>${p.noteSaving ? t('secretary.noteSaving') : t('secretary.noteSave')}</button>
155
+ ${p.wsList.length >= 2 ? html`<button class="btn-outline btn-sm" disabled=${!p.noteText.trim() || p.noteSaving} onClick=${p.askDecision} title=${t('secretary.askInInboxHint')}>${t('secretary.askInInbox')}</button>` : null}
156
+ </div>
157
+ </section>`;
158
+ }
159
+
160
+ /** Pending Ask-band decisions awaiting the owner's inbox answer (+ Apply / dismiss). */
161
+ export function decisionsCard(p) {
162
+ return html`
163
+ <section class="sec-card">
164
+ <h2 class="sec-h2">${t('secretary.decisionsTitle')}</h2>
165
+ <ul class="sec-hist">
166
+ ${p.pendingIds.map((pid) => {
167
+ const dec = p.pendingDecisions[pid];
168
+ const answer = p.decisionAnswers[pid];
169
+ return html`<li class="sec-hist-row" key=${pid}>
170
+ <span class="sec-hist-purpose">${escHtml((dec.body || dec.text || dec.question || '').slice(0, 60))}</span>
171
+ ${answer
172
+ ? html`<button class="btn-primary btn-sm" onClick=${() => p.applyDecision(pid)}>${t('secretary.decisionApply')}: ${escHtml(answer)}</button>`
173
+ : html`<span class="sec-hint">${t('secretary.decisionAwaiting')}</span>`}
174
+ <button class="btn-ghost btn-sm" onClick=${() => p.dismissDecision(pid)}>${t('secretary.cancel')}</button>
175
+ </li>`;
176
+ })}
177
+ </ul>
178
+ </section>`;
179
+ }
180
+
181
+ /** Status + brain (purpose + rules) + self-organism summary, with Re-run. */
182
+ export function brainCard(p) {
183
+ const brain = p.brain;
184
+ const active = p.active;
185
+ return html`
186
+ <section class="sec-card">
187
+ <div class="sec-card-head">
188
+ <div class="sec-status"><span class="sec-dot"></span> ${t('secretary.hiredStatus')}</div>
189
+ <button class="btn-ghost btn-sm" onClick=${p.openEdit}>${t('secretary.rerun')}</button>
190
+ </div>
191
+ <h2 class="sec-h2">${t('secretary.brain')}</h2>
192
+ <p class="sec-purpose">${escHtml(brain.purpose)}</p>
193
+ ${Array.isArray(brain.rules) && brain.rules.length > 0 && html`<ul class="sec-rules">${brain.rules.map((r) => html`<li key=${r.id}>${escHtml(r.description)}</li>`)}</ul>`}
194
+ ${active.organismName && html`
195
+ <h2 class="sec-h2">${t('secretary.selfOrganism')}</h2>
196
+ <p class="sec-orgname">🗂 ${escHtml(active.organismName)}</p>
197
+ ${Array.isArray(active.workspaces) && html`<ul class="sec-ws">${active.workspaces.map((w, i) => html`<li key=${i}><strong>${escHtml(w.name)}</strong>${w.purpose ? html` — ${escHtml(w.purpose)}` : null}</li>`)}</ul>`}`}
198
+ </section>`;
199
+ }
200
+
201
+ /** Operating model: stop-spending + daily budget + per-capability autonomy bands. */
202
+ export function operatingCard(p) {
203
+ const policy = p.policy;
204
+ return html`
205
+ <section class="sec-card">
206
+ <h2 class="sec-h2">${t('secretary.howIWork')}</h2>
207
+ <label class="sec-stop ${policy.stopSpending ? 'on' : ''}">
208
+ <input type="checkbox" checked=${policy.stopSpending} onChange=${p.toggleStop} />
209
+ <span><strong>${t('secretary.stopSpending')}</strong><br/><span class="sec-hint">${t('secretary.stopSpendingHint')}</span></span>
210
+ </label>
211
+ <div class="sec-budget">
212
+ <label>${t('secretary.dailyBudget')}</label>
213
+ <input type="number" min="0" class="sec-budget-in" value=${policy.dailyMorselBudget == null ? '' : policy.dailyMorselBudget} placeholder=${t('secretary.noLimit')} onChange=${(e) => p.setBudget(e.target.value)} />
214
+ <span class="sec-hint">${t('secretary.dailyBudgetHint')}</span>
215
+ </div>
216
+ <p class="sec-hint sec-bands-note">${t('secretary.bandsNote')}</p>
217
+ <ul class="sec-caps">
218
+ ${SECRETARY_CAPABILITIES.map((c) => html`
219
+ <li class="sec-cap" key=${c.id}>
220
+ <span class="sec-cap-label">${t('secretary.cap.' + c.id)}${c.costs ? html`<span class="sec-cap-tag">${t('secretary.capCosts')}</span>` : null}</span>
221
+ ${c.enterprise
222
+ ? html`<span class="sec-cap-locked sec-cap-ent">${t('secretary.enterprise')}</span>`
223
+ : c.locked
224
+ ? html`<span class="sec-cap-locked">${t('secretary.band.' + policy.bands[c.id])} · ${t('secretary.locked')}</span>`
225
+ : html`<select class="sec-band" value=${policy.bands[c.id]} onChange=${(e) => p.setBand(c.id, e.target.value)}>${BANDS.map((b) => html`<option value=${b} selected=${policy.bands[c.id] === b}>${t('secretary.band.' + b)}</option>`)}</select>`}
226
+ </li>`)}
227
+ </ul>
228
+ </section>`;
229
+ }
230
+
231
+ /** Brain version history + restore. */
232
+ export function historyCard(p) {
233
+ return html`
234
+ <section class="sec-card">
235
+ <h2 class="sec-h2">${t('secretary.history')}</h2>
236
+ <ul class="sec-hist">
237
+ ${p.brainHistory.map((snap, i) => html`
238
+ <li class="sec-hist-row" key=${i}>
239
+ <span class="sec-hist-purpose">${escHtml((snap.purpose || '').slice(0, 80))}</span>
240
+ <button class="btn-ghost btn-sm" disabled=${p.applying} onClick=${() => p.restore(snap)}>${t('secretary.restore')}</button>
241
+ </li>`)}
242
+ </ul>
243
+ </section>`;
244
+ }
245
+
246
+ /** Guided playbook (Phase 3c): propose steps → approve → execute (file + discover) → result. */
247
+ export function guidedPlanCard(p) {
248
+ return html`
249
+ <section class="sec-card sec-plan">
250
+ <h2 class="sec-h2">${t('secretary.plan.title')}</h2>
251
+ <p class="sec-hint">${t('secretary.plan.hint')}</p>
252
+ ${!p.steps && !p.result ? html`
253
+ <textarea class="sec-paste" rows="2" placeholder=${t('secretary.plan.placeholder')} value=${p.goal} onInput=${(e) => p.setGoal(e.target.value)}></textarea>
254
+ <div class="sec-actions">
255
+ <button class="btn-primary" disabled=${!p.goal.trim() || p.planning} onClick=${p.planIt}>${p.planning ? t('secretary.plan.planning') : t('secretary.plan.go')}</button>
256
+ </div>` : null}
257
+ ${p.steps && !p.result ? html`
258
+ <div class="sec-plan-steps">
259
+ ${p.steps.summary ? html`<p class="sec-purpose">${escHtml(p.steps.summary)}</p>` : null}
260
+ <ol class="sec-rules">
261
+ ${p.steps.steps.map((s, i) => html`<li key=${i}><strong>${escHtml(s.title)}</strong>${s.detail ? html` — ${escHtml(s.detail)}` : null}</li>`)}
262
+ </ol>
263
+ <div class="sec-actions">
264
+ <button class="btn-ghost btn-sm" onClick=${p.cancelPlan}>${t('secretary.cancel')}</button>
265
+ <button class="btn-primary" disabled=${p.running} onClick=${p.runPlan}>${p.running ? t('secretary.plan.running') : t('secretary.plan.approve')}</button>
266
+ </div>
267
+ </div>` : null}
268
+ ${p.result ? html`
269
+ <div class="sec-plan-result">
270
+ <div class="sec-status"><span class="sec-dot"></span> ${t('secretary.plan.ran')}</div>
271
+ ${p.result.wsName ? html`<p class="sec-hint">${t('secretary.plan.filed')} <strong>${escHtml(p.result.wsName)}</strong></p>` : null}
272
+ <p class="sec-hint">${t('secretary.plan.found')} ${p.result.found}</p>
273
+ <button class="btn-ghost btn-sm" onClick=${p.resetPlan}>${t('secretary.plan.new')}</button>
274
+ </div>` : null}
275
+ </section>`;
276
+ }
277
+
278
+ /** Home feed (Phase 4): what the Secretary did on its own (autonomous briefings). */
279
+ export function feedCard(p) {
280
+ return html`
281
+ <section class="sec-card sec-feed">
282
+ <h2 class="sec-h2">${t('secretary.feed.title')}</h2>
283
+ ${p.feed.length === 0
284
+ ? html`<div class="sec-hint">${t('secretary.feed.empty')}</div>`
285
+ : html`<ul class="sec-feed-list">
286
+ ${p.feed.map((it, i) => html`<li class="sec-feed-item" key=${it.id || i}>
287
+ <div class="sec-feed-meta">${it.contextName ? escHtml(it.contextName) + ' · ' : ''}${it.ts ? new Date(it.ts).toLocaleString() : ''}</div>
288
+ <div class="sec-feed-text">${escHtml(it.text || '')}</div>
289
+ </li>`)}
290
+ </ul>`}
291
+ </section>`;
292
+ }
293
+
294
+ /** Autonomous tick + calendar (Phase 4): enable / run-now / pause the daily check-in. */
295
+ export function automationCard(p) {
296
+ const s = p.schedule;
297
+ return html`
298
+ <section class="sec-card sec-auto">
299
+ <h2 class="sec-h2">${t('secretary.auto.title')}</h2>
300
+ ${!s ? html`
301
+ <p class="sec-hint">${t('secretary.auto.hint')}</p>
302
+ <button class="btn-primary" onClick=${p.enableTick}>${t('secretary.auto.enable')}</button>`
303
+ : html`
304
+ <div class="sec-auto-row">
305
+ <div>
306
+ <div><strong>${escHtml(s.displayName || t('secretary.auto.scheduleName'))}</strong>${s.enabled ? null : html` <span class="sec-hint">(${t('secretary.auto.paused')})</span>`}</div>
307
+ <div class="sec-hint">${t('secretary.auto.nextRun')}: ${s.nextRunAt ? new Date(s.nextRunAt).toLocaleString() : '—'}${s.cron ? ' · ' + escHtml(s.cron) : ''}</div>
308
+ ${p.budgetInfo ? html`<div class="sec-hint">${t('secretary.auto.budgetLeft')}: ${p.budgetInfo.remaining} / ${p.budgetInfo.budget}</div>` : null}
309
+ </div>
310
+ <div class="sec-auto-actions">
311
+ <button class="btn-ghost btn-sm" onClick=${p.toggleTick}>${s.enabled ? t('secretary.auto.pause') : t('secretary.auto.resume')}</button>
312
+ <button class="btn-primary btn-sm" disabled=${p.running} onClick=${p.runTick}>${p.running ? t('secretary.auto.running') : t('secretary.auto.runNow')}</button>
313
+ </div>
314
+ </div>`}
315
+ </section>`;
316
+ }
317
+
318
+ /** Goals (Phase 5): lightweight standalone goal records the Secretary works toward. */
319
+ export function goalsCard(p) {
320
+ const f = p.goalForm;
321
+ return html`
322
+ <section class="sec-card sec-goals">
323
+ <div class="sec-card-head">
324
+ <h2 class="sec-h2">${t('secretary.learn.goalsTitle')}</h2>
325
+ <button class="btn-ghost btn-sm" onClick=${() => p.setGoalForm({ ...f, open: !f.open })}>${f.open ? t('secretary.cancel') : t('secretary.learn.addGoal')}</button>
326
+ </div>
327
+ ${f.open ? html`
328
+ <div class="sec-form">
329
+ <input class="sec-input" placeholder=${t('secretary.learn.goalTitle')} value=${f.title} onInput=${(e) => p.setGoalForm({ ...f, title: e.target.value })} />
330
+ <textarea class="sec-paste" rows="2" placeholder=${t('secretary.learn.goalWhy')} value=${f.why} onInput=${(e) => p.setGoalForm({ ...f, why: e.target.value })}></textarea>
331
+ <button class="btn-primary" disabled=${!f.title.trim() || f.saving} onClick=${p.addGoal}>${f.saving ? t('secretary.noteSaving') : t('secretary.learn.saveGoal')}</button>
332
+ </div>` : null}
333
+ ${p.goals.length === 0
334
+ ? html`<div class="sec-hint">${t('secretary.learn.goalsEmpty')}</div>`
335
+ : html`<ul class="sec-goal-list">
336
+ ${p.goals.map((g) => html`<li class="sec-goal-row ${g.status === 'done' ? 'done' : ''}" key=${g.id}>
337
+ <div class="sec-goal-main">
338
+ <div class="sec-goal-title">${escHtml(g.title)}</div>
339
+ ${g.why ? html`<div class="sec-hint">${escHtml(g.why)}</div>` : null}
340
+ </div>
341
+ <button class="btn-ghost btn-sm" onClick=${() => p.completeGoal(g)}>${g.status === 'done' ? t('secretary.learn.reopen') : t('secretary.learn.done')}</button>
342
+ <button class="btn-ghost btn-sm" onClick=${() => p.deleteGoal(g)}>✕</button>
343
+ </li>`)}
344
+ </ul>`}
345
+ </section>`;
346
+ }
347
+
348
+ /** Decision log (Phase 5): self-describing decision contracts that stay open until the review scores them. */
349
+ export function decisionLogCard(p) {
350
+ const f = p.decForm;
351
+ const scoreChip = (d) => {
352
+ if (d.status === 'reviewed' && typeof d.score === 'number') {
353
+ const cls = d.score >= 70 ? 'good' : d.score >= 40 ? 'mid' : 'bad';
354
+ return html`<span class="sec-score ${cls}">${d.score}/100</span>`;
355
+ }
356
+ return html`<span class="sec-hint">${t('secretary.learn.revisit')}: ${d.revisitWhen ? new Date(d.revisitWhen).toLocaleDateString() : '—'}</span>`;
357
+ };
358
+ return html`
359
+ <section class="sec-card sec-decisions-log">
360
+ <div class="sec-card-head">
361
+ <h2 class="sec-h2">${t('secretary.learn.decisionsTitle')}</h2>
362
+ <div class="sec-auto-actions">
363
+ <button class="btn-ghost btn-sm" disabled=${p.reviewing} onClick=${p.reviewNow}>${p.reviewing ? t('secretary.auto.running') : t('secretary.learn.reviewNow')}</button>
364
+ <button class="btn-ghost btn-sm" onClick=${() => p.setDecForm({ ...f, open: !f.open })}>${f.open ? t('secretary.cancel') : t('secretary.learn.logDecision')}</button>
365
+ </div>
366
+ </div>
367
+ ${f.open ? html`
368
+ <div class="sec-form">
369
+ <input class="sec-input" placeholder=${t('secretary.learn.decision')} value=${f.decision} onInput=${(e) => p.setDecForm({ ...f, decision: e.target.value })} />
370
+ <textarea class="sec-paste" rows="2" placeholder=${t('secretary.learn.options')} value=${f.options} onInput=${(e) => p.setDecForm({ ...f, options: e.target.value })}></textarea>
371
+ <input class="sec-input" placeholder=${t('secretary.learn.chosen')} value=${f.chosen} onInput=${(e) => p.setDecForm({ ...f, chosen: e.target.value })} />
372
+ <input class="sec-input" placeholder=${t('secretary.learn.rationale')} value=${f.rationale} onInput=${(e) => p.setDecForm({ ...f, rationale: e.target.value })} />
373
+ <input class="sec-input" placeholder=${t('secretary.learn.expected')} value=${f.expectedOutcome} onInput=${(e) => p.setDecForm({ ...f, expectedOutcome: e.target.value })} />
374
+ <div class="sec-note-bar">
375
+ <label class="sec-hint">${t('secretary.learn.goalRef')}</label>
376
+ <select class="sec-band" value=${f.goalRef} onChange=${(e) => p.setDecForm({ ...f, goalRef: e.target.value })}>
377
+ <option value="">—</option>
378
+ ${p.goals.filter((g) => g.status !== 'done').map((g) => html`<option value=${'secretary.goal.' + g.id} selected=${f.goalRef === 'secretary.goal.' + g.id}>${escHtml(g.title)}</option>`)}
379
+ </select>
380
+ <label class="sec-hint">${t('secretary.learn.revisitIn')}</label>
381
+ <input class="sec-input sec-input-sm" type="number" min="0" value=${f.revisitDays} onInput=${(e) => p.setDecForm({ ...f, revisitDays: e.target.value })} />
382
+ <button class="btn-primary" disabled=${!f.decision.trim() || f.saving} onClick=${p.addDecision}>${f.saving ? t('secretary.noteSaving') : t('secretary.learn.saveDecision')}</button>
383
+ </div>
384
+ </div>` : null}
385
+ ${p.decisions.length === 0
386
+ ? html`<div class="sec-hint">${t('secretary.learn.decisionsEmpty')}</div>`
387
+ : html`<ul class="sec-goal-list">
388
+ ${p.decisions.map((d) => html`<li class="sec-goal-row" key=${d.id}>
389
+ <div class="sec-goal-main">
390
+ <div class="sec-goal-title">${escHtml(d.decision)}${d.chosen ? html` <span class="sec-hint">→ ${escHtml(d.chosen)}</span>` : null}</div>
391
+ ${d.status === 'reviewed' && d.verdict
392
+ ? html`<div class="sec-hint">${escHtml(d.verdict)}</div>`
393
+ : (d.expectedOutcome ? html`<div class="sec-hint">${t('secretary.learn.expected')}: ${escHtml(d.expectedOutcome)}</div>` : null)}
394
+ </div>
395
+ ${scoreChip(d)}
396
+ <button class="btn-ghost btn-sm" onClick=${() => p.deleteDecision(d)}>✕</button>
397
+ </li>`)}
398
+ </ul>`}
399
+ </section>`;
400
+ }
401
+
402
+ /** Identity + granted scopes + self-facing reliability (mean reviewed-decision score). */
403
+ export function metaCard(p) {
404
+ const rel = p.reliability;
405
+ const relChip = (rel && typeof rel.score === 'number')
406
+ ? html`<span class="sec-score ${rel.score >= 70 ? 'good' : rel.score >= 40 ? 'mid' : 'bad'}">${rel.score}/100</span> <span class="sec-hint">(${rel.count})</span>`
407
+ : html`<span class="sec-hint">${t('secretary.reliabilityBuilding')}</span>`;
408
+ return html`
409
+ <section class="sec-card sec-meta-card">
410
+ <dl class="sec-meta">
411
+ <dt>${t('secretary.identity')}</dt>
412
+ <dd><code>${escHtml(p.secretary.gaii)}</code></dd>
413
+ <dt title=${t('secretary.reliabilityHint')}>${t('secretary.reliability')}</dt>
414
+ <dd>${relChip}</dd>
415
+ <dt>${t('secretary.scopes')}</dt>
416
+ <dd class="sec-scopes">${(p.secretary.default_scopes || []).map((s) => html`<span class="sec-scope" key=${s}>${s}</span>`)}</dd>
417
+ </dl>
418
+ </section>`;
419
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @file secretary/use-access.js
3
+ * @description P2-C — access gatekeeper. A surface for the Secretary to help the owner manage their OWN
4
+ * sharing groups + consent grants (Draft/Ask: the form IS the draft, an explicit Approve commits). It
5
+ * lists the owner's active consent grants (GET /v1/consent) and sharing groups (GET /v1/groups), can
6
+ * propose + create a new consent grant (POST /v1/consent) or sharing group (POST /v1/groups), and can
7
+ * revoke a grant (DELETE /v1/consent/:id). PERSONAL admin only — owner sessions bypass the
8
+ * `consent:manage` scope; this never touches org / Enterprise `consent:manage`. Re-fetches on the
9
+ * consent/groups live-update domains. See docs/plans/2026-06-24-secretary-p2-fix-prompt.md (P2-C) + §21.
10
+ * @structure useAccess({ showToast }) -> { consents, groups, loading, grantForm, groupForm, ...handlers }
11
+ * @usage const access = useAccess({ showToast }); accessCard(access)
12
+ * @version-history v0.1.0 — 2026-06-24 — P2-C: list/grant/revoke consent + create sharing groups.
13
+ */
14
+ import { useState, useEffect, useCallback } from 'preact/hooks';
15
+ import { apiGet, apiPost, apiDelete } from '/js/api.js';
16
+ import { t } from '/js/i18n.js';
17
+
18
+ const EMPTY_GRANT = { open: false, dataPattern: '', recipient: '', purpose: '', saving: false };
19
+ const EMPTY_GROUP = { open: false, name: '', saving: false };
20
+
21
+ export function useAccess({ showToast }) {
22
+ const [consents, setConsents] = useState([]);
23
+ const [groups, setGroups] = useState([]);
24
+ const [loading, setLoading] = useState(true);
25
+ const [grantForm, setGrantForm] = useState(EMPTY_GRANT);
26
+ const [groupForm, setGroupForm] = useState(EMPTY_GROUP);
27
+
28
+ const refresh = useCallback(async () => {
29
+ setLoading(true);
30
+ try {
31
+ const [c, g] = await Promise.all([
32
+ apiGet(`/v1/consent?status=active&_=${Date.now()}`).catch(() => null),
33
+ apiGet(`/v1/groups?_=${Date.now()}`).catch(() => null),
34
+ ]);
35
+ setConsents((c && c.data && c.data.consents) || []);
36
+ setGroups((g && g.data && g.data.groups) || []);
37
+ } finally {
38
+ setLoading(false);
39
+ }
40
+ }, []);
41
+
42
+ useEffect(() => { refresh(); }, [refresh]);
43
+ useEffect(() => {
44
+ const handler = (e) => {
45
+ const d = e.detail && e.detail.domains;
46
+ if (!d || d.has('consent') || d.has('groups')) refresh();
47
+ };
48
+ window.addEventListener('aimeat-live-update', handler);
49
+ return () => window.removeEventListener('aimeat-live-update', handler);
50
+ }, [refresh]);
51
+
52
+ const approveGrant = useCallback(async () => {
53
+ const data_pattern = grantForm.dataPattern.trim();
54
+ const recipient = grantForm.recipient.trim();
55
+ const purpose = grantForm.purpose.trim();
56
+ if (!data_pattern || !recipient || !purpose) return;
57
+ setGrantForm((f) => ({ ...f, saving: true }));
58
+ try {
59
+ await apiPost('/v1/consent', { data_pattern, recipient, purpose, scope: 'private' });
60
+ setGrantForm(EMPTY_GRANT);
61
+ showToast(t('secretary.access.granted'));
62
+ await refresh();
63
+ } catch (e) {
64
+ showToast(`${t('secretary.access.error')}: ${e.message}`, true);
65
+ setGrantForm((f) => ({ ...f, saving: false }));
66
+ }
67
+ }, [grantForm, refresh, showToast]);
68
+
69
+ const revokeGrant = useCallback(async (id) => {
70
+ try {
71
+ await apiDelete(`/v1/consent/${encodeURIComponent(id)}`);
72
+ showToast(t('secretary.access.revoked'));
73
+ await refresh();
74
+ } catch (e) {
75
+ showToast(`${t('secretary.access.error')}: ${e.message}`, true);
76
+ }
77
+ }, [refresh, showToast]);
78
+
79
+ const createGroup = useCallback(async () => {
80
+ const name = groupForm.name.trim();
81
+ if (!name) return;
82
+ setGroupForm((f) => ({ ...f, saving: true }));
83
+ try {
84
+ await apiPost('/v1/groups', { name });
85
+ setGroupForm(EMPTY_GROUP);
86
+ showToast(t('secretary.access.groupCreated'));
87
+ await refresh();
88
+ } catch (e) {
89
+ showToast(`${t('secretary.access.error')}: ${e.message}`, true);
90
+ setGroupForm((f) => ({ ...f, saving: false }));
91
+ }
92
+ }, [groupForm, refresh, showToast]);
93
+
94
+ return { consents, groups, loading, grantForm, setGrantForm, approveGrant, revokeGrant, groupForm, setGroupForm, createGroup };
95
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @file secretary/use-autonomy.js
3
+ * @description Phase 4 — autonomous tick + Home feed + calendar. A hook that loads the Secretary's
4
+ * activity feed (`secretary.feed`) and its autonomous schedule (a `secretary`-kind scheduled job),
5
+ * and lets the owner enable / run-now / pause the daily check-in. The backend `secretary` scheduler
6
+ * kind (services/scheduler.ts) runs the active context's brain on the owner's key each fire and
7
+ * appends a briefing to the feed — unless stop-spending is on. See docs/plans/2026-06-23-secretary-feature.md (Phase 4).
8
+ * @structure useAutonomy({ showToast }) -> { feed, schedule, loading, refresh, enableTick, runTick, toggleTick, running }
9
+ * @usage const auto = useAutonomy({ showToast }); ... feedCard(auto) / automationCard(auto)
10
+ * @version-history v0.1.0 — 2026-06-24 — Phase 4: autonomous tick, feed, calendar controls.
11
+ */
12
+ import { useState, useEffect, useCallback } from 'preact/hooks';
13
+ import { api, apiGet, apiPost, apiPatch } from '/js/api.js';
14
+ import { t } from '/js/i18n.js';
15
+
16
+ const DAILY_CRON = '0 8 * * *'; // every day at 08:00 (server-local)
17
+
18
+ export function useAutonomy({ showToast }) {
19
+ const [feed, setFeed] = useState([]);
20
+ const [schedule, setSchedule] = useState(null); // the secretary-kind ScheduledJobRecord, or null
21
+ const [loading, setLoading] = useState(true);
22
+ const [running, setRunning] = useState(false);
23
+
24
+ const refresh = useCallback(async () => {
25
+ setLoading(true);
26
+ try {
27
+ const [feedR, schedR] = await Promise.all([
28
+ apiGet('/v1/memory/secretary.feed').catch(() => null),
29
+ apiGet('/v1/schedules').catch(() => null),
30
+ ]);
31
+ const items = (feedR && feedR.data && feedR.data.value && Array.isArray(feedR.data.value.items)) ? feedR.data.value.items : [];
32
+ setFeed(items);
33
+ const managed = (schedR && schedR.data && Array.isArray(schedR.data.managed)) ? schedR.data.managed : [];
34
+ setSchedule(managed.find((j) => j.type === 'secretary') || null);
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ }, []);
39
+
40
+ useEffect(() => { refresh(); }, [refresh]);
41
+
42
+ // Re-fetch on server-reported changes (the tick writes the feed + schedule state server-side).
43
+ useEffect(() => {
44
+ const handler = () => refresh();
45
+ window.addEventListener('aimeat-live-update', handler);
46
+ return () => window.removeEventListener('aimeat-live-update', handler);
47
+ }, [refresh]);
48
+
49
+ const enableTick = useCallback(async () => {
50
+ try {
51
+ await apiPost('/v1/schedules', { kind: 'secretary', cron: DAILY_CRON, display_name: t('secretary.auto.scheduleName') });
52
+ showToast(t('secretary.auto.enabled'));
53
+ await refresh();
54
+ } catch (e) {
55
+ showToast(`${t('secretary.auto.error')}: ${e.message}`, true);
56
+ }
57
+ }, [refresh, showToast]);
58
+
59
+ const runTick = useCallback(async () => {
60
+ if (!schedule) return;
61
+ setRunning(true);
62
+ try {
63
+ // The tick makes a (possibly slow) AI call server-side — use api() with a long timeout + no
64
+ // retries so it isn't aborted/re-fired by apiPost's 30s default. (See long-ai-call gotcha.)
65
+ const r = await api(`/v1/schedules/${schedule.id}/trigger`, { method: 'POST', body: '{}', timeoutMs: 1_800_000, retries: 0 });
66
+ const outcome = r && r.data && r.data.outcome;
67
+ // 'busy'/'limited'/'error' come back with a reason; 'ran' means a feed entry was produced.
68
+ if (outcome && outcome !== 'ran' && outcome !== 'created') {
69
+ showToast(`${t('secretary.auto.skipped')}: ${(r.data && r.data.reason) || outcome}`);
70
+ } else {
71
+ showToast(t('secretary.auto.ran'));
72
+ }
73
+ await refresh();
74
+ } catch (e) {
75
+ showToast(`${t('secretary.auto.error')}: ${e.message}`, true);
76
+ } finally {
77
+ setRunning(false);
78
+ }
79
+ }, [schedule, refresh, showToast]);
80
+
81
+ const toggleTick = useCallback(async () => {
82
+ if (!schedule) return;
83
+ try {
84
+ await apiPatch(`/v1/schedules/${schedule.id}`, { enabled: !schedule.enabled });
85
+ await refresh();
86
+ } catch (e) {
87
+ showToast(`${t('secretary.auto.error')}: ${e.message}`, true);
88
+ }
89
+ }, [schedule, refresh, showToast]);
90
+
91
+ return { feed, schedule, loading, running, refresh, enableTick, runTick, toggleTick };
92
+ }