aimeat 1.16.2 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.env.example +6 -1
- package/dist/locales/en.json +166 -4
- package/dist/locales/fi.json +166 -4
- package/dist/public/components/Card.js +17 -3
- package/dist/public/components/Collapsible.js +39 -0
- package/dist/public/components/CopyButton.js +55 -24
- package/dist/public/components/DataTable.js +57 -0
- package/dist/public/components/EmptyState.js +28 -0
- package/dist/public/components/KeyValueRow.js +37 -0
- package/dist/public/components/Modal.js +22 -5
- package/dist/public/components/Pagination.js +70 -0
- package/dist/public/components/SearchBar.js +44 -0
- package/dist/public/components/StatusDot.js +29 -0
- package/dist/public/components/TagList.js +53 -0
- package/dist/public/components/ToggleSwitch.js +28 -0
- package/dist/public/components/index.js +41 -1
- package/dist/public/css/components/tags.css +24 -0
- package/dist/public/css/style-testbed.css +135 -0
- package/dist/public/css/theme.css +556 -34
- package/dist/public/css/views/admin-agent-integration.css +2 -33
- package/dist/public/css/views/admin.css +730 -686
- package/dist/public/css/views/agents-detail.css +402 -14
- package/dist/public/css/views/help.css +2 -1
- package/dist/public/css/views/hobbies.css +53 -33
- package/dist/public/css/views/marketplace.css +73 -60
- package/dist/public/css/views/openclaw.css +12 -6
- package/dist/public/css/views/portal-classic.css +34 -26
- package/dist/public/css/views/portal-dev.css +41 -32
- package/dist/public/css/views/portal.css +14 -18
- package/dist/public/css/views/profile.css +252 -164
- package/dist/public/css/views/public-knowledge-viewer.css +18 -6
- package/dist/public/css/views/scheduler.css +100 -0
- package/dist/public/js/format.js +41 -0
- package/dist/public/js/services/agent-tasks.js +14 -1
- package/dist/public/js/services/generator-spec-validate.js +5 -1
- package/dist/public/js/services/schedules.js +53 -0
- package/dist/public/js/style-testbed.js +170 -0
- package/dist/public/js/theme.js +114 -0
- package/dist/public/spa.html +74 -16
- package/dist/public/style-testbed.html +420 -0
- package/dist/public/views/admin/agent-integration-tab.js +23 -38
- package/dist/public/views/admin/agent-tasks-tab.js +14 -21
- package/dist/public/views/admin/chat-instances-tab.js +1 -1
- package/dist/public/views/admin/config-tab.js +7 -2
- package/dist/public/views/admin/cortex-tab.js +20 -14
- package/dist/public/views/admin/csm-tab.js +22 -18
- package/dist/public/views/admin/directory-tab.js +3 -3
- package/dist/public/views/admin/email-tab.js +23 -28
- package/dist/public/views/admin/federation-tab.js +15 -5
- package/dist/public/views/admin/generator-debug-tab.js +10 -8
- package/dist/public/views/admin/genesis-tab.js +4 -4
- package/dist/public/views/admin/ghii-tab.js +3 -3
- package/dist/public/views/admin/knowledge-tab.js +15 -5
- package/dist/public/views/admin/matching-tab.js +3 -3
- package/dist/public/views/admin/memory-tab.js +16 -20
- package/dist/public/views/admin/msm-tab.js +30 -17
- package/dist/public/views/admin/overview-tab.js +7 -7
- package/dist/public/views/admin/packages-tab.js +10 -9
- package/dist/public/views/admin/portal-tab.js +14 -4
- package/dist/public/views/admin/prompts-tab.js +5 -5
- package/dist/public/views/admin/push-tab.js +15 -4
- package/dist/public/views/admin/realtime-tab.js +3 -3
- package/dist/public/views/admin/services-tab.js +18 -27
- package/dist/public/views/admin/shared.js +48 -58
- package/dist/public/views/admin/sharing-groups-tab.js +5 -10
- package/dist/public/views/admin/stats-tab.js +3 -3
- package/dist/public/views/guides.js +9 -13
- package/dist/public/views/help.js +3 -24
- package/dist/public/views/hobbies.js +47 -41
- package/dist/public/views/marketplace.js +42 -34
- package/dist/public/views/openclaw.js +0 -1
- package/dist/public/views/portal-classic.js +4 -15
- package/dist/public/views/portal-dev.js +28 -30
- package/dist/public/views/portal.js +16 -21
- package/dist/public/views/portfolio.js +11 -12
- package/dist/public/views/profile/access-tab.js +250 -1
- package/dist/public/views/profile/agents/agent-card.js +75 -4
- package/dist/public/views/profile/agents/tab-activity.js +15 -13
- package/dist/public/views/profile/agents/tab-agent-config.js +60 -6
- package/dist/public/views/profile/agents/tab-integration.js +37 -60
- package/dist/public/views/profile/agents/tab-messages.js +10 -1
- package/dist/public/views/profile/agents/tab-quality.js +25 -19
- package/dist/public/views/profile/agents/tab-schedules.js +110 -0
- package/dist/public/views/profile/agents-activity-subtab.js +4 -4
- package/dist/public/views/profile/agents-services-subtab.js +5 -4
- package/dist/public/views/profile/agents-tab.js +141 -37
- package/dist/public/views/profile/agents-tasks-subtab.js +221 -26
- package/dist/public/views/profile/calibrator-batch.js +5 -11
- package/dist/public/views/profile/calibrator-tab.js +4 -9
- package/dist/public/views/profile/chat-sessions-tab.js +9 -5
- package/dist/public/views/profile/email-tab.js +2 -2
- package/dist/public/views/profile/extensions-tab.js +27 -22
- package/dist/public/views/profile/federation-tab.js +4 -1
- package/dist/public/views/profile/foundry-detail.js +22 -23
- package/dist/public/views/profile/foundry-tab.js +12 -13
- package/dist/public/views/profile/generator-dashboard/DebugPanel.js +4 -7
- package/dist/public/views/profile/generator-dashboard/use-dashboard-core.js +4 -1
- package/dist/public/views/profile/generator-dashboard/use-edit-mode.js +3 -2
- package/dist/public/views/profile/generator-detail.js +26 -30
- package/dist/public/views/profile/generator-tab.js +5 -5
- package/dist/public/views/profile/knowledge-tab.js +12 -12
- package/dist/public/views/profile/landing-page.js +100 -149
- package/dist/public/views/profile/memory-tab.js +15 -11
- package/dist/public/views/profile/node-stats-tab.js +3 -3
- package/dist/public/views/profile/nodes-tab.js +10 -3
- package/dist/public/views/profile/notifications-tab.js +3 -3
- package/dist/public/views/profile/packages-tab.js +14 -14
- package/dist/public/views/profile/scheduler-tab.js +340 -0
- package/dist/public/views/profile/services-tab.js +1 -1
- package/dist/public/views/profile/shared.js +16 -12
- package/dist/public/views/profile/wallet-tab.js +27 -50
- package/dist/public/views/profile/work-tab.js +29 -31
- package/dist/public/views/profile.js +24 -0
- package/dist/public/views/public-knowledge-viewer.js +9 -18
- package/dist/src/auth/middleware.d.ts +3 -2
- package/dist/src/auth/middleware.d.ts.map +1 -1
- package/dist/src/auth/middleware.js +78 -3
- package/dist/src/auth/middleware.js.map +1 -1
- package/dist/src/cli/init-wizard.d.ts.map +1 -1
- package/dist/src/cli/init-wizard.js +13 -1
- package/dist/src/cli/init-wizard.js.map +1 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +12 -1
- package/dist/src/config.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +708 -8
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/mcp/agent-schedules.d.ts +23 -0
- package/dist/src/mcp/agent-schedules.d.ts.map +1 -0
- package/dist/src/mcp/agent-schedules.js +175 -0
- package/dist/src/mcp/agent-schedules.js.map +1 -0
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +6 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +55 -0
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/scopes.d.ts.map +1 -1
- package/dist/src/mcp/catalog/scopes.js +12 -0
- package/dist/src/mcp/catalog/scopes.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.d.ts.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +2 -0
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +2 -0
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/models/agent-task-schemas.d.ts +6 -0
- package/dist/src/models/agent-task-schemas.d.ts.map +1 -1
- package/dist/src/models/agent-task-schemas.js +6 -0
- package/dist/src/models/agent-task-schemas.js.map +1 -1
- package/dist/src/models/schemas.d.ts +1 -0
- package/dist/src/models/schemas.d.ts.map +1 -1
- package/dist/src/models/schemas.js +4 -0
- package/dist/src/models/schemas.js.map +1 -1
- package/dist/src/routes/access-tokens.d.ts +17 -0
- package/dist/src/routes/access-tokens.d.ts.map +1 -0
- package/dist/src/routes/access-tokens.js +164 -0
- package/dist/src/routes/access-tokens.js.map +1 -0
- package/dist/src/routes/agent-tasks.d.ts +2 -0
- package/dist/src/routes/agent-tasks.d.ts.map +1 -1
- package/dist/src/routes/agent-tasks.js +118 -23
- package/dist/src/routes/agent-tasks.js.map +1 -1
- package/dist/src/routes/agents.d.ts.map +1 -1
- package/dist/src/routes/agents.js +46 -0
- package/dist/src/routes/agents.js.map +1 -1
- package/dist/src/routes/ai.d.ts +6 -9
- package/dist/src/routes/ai.d.ts.map +1 -1
- package/dist/src/routes/ai.js +26 -175
- package/dist/src/routes/ai.js.map +1 -1
- package/dist/src/routes/apps.d.ts.map +1 -1
- package/dist/src/routes/apps.js +104 -34
- package/dist/src/routes/apps.js.map +1 -1
- package/dist/src/routes/auth.d.ts.map +1 -1
- package/dist/src/routes/auth.js +77 -4
- package/dist/src/routes/auth.js.map +1 -1
- package/dist/src/routes/cortex.d.ts +2 -1
- package/dist/src/routes/cortex.d.ts.map +1 -1
- package/dist/src/routes/cortex.js +13 -8
- package/dist/src/routes/cortex.js.map +1 -1
- package/dist/src/routes/extensions.d.ts.map +1 -1
- package/dist/src/routes/extensions.js +85 -51
- package/dist/src/routes/extensions.js.map +1 -1
- package/dist/src/routes/generator.d.ts.map +1 -1
- package/dist/src/routes/generator.js +109 -43
- package/dist/src/routes/generator.js.map +1 -1
- package/dist/src/routes/ghii.d.ts.map +1 -1
- package/dist/src/routes/ghii.js +34 -13
- package/dist/src/routes/ghii.js.map +1 -1
- package/dist/src/routes/libs.d.ts +15 -0
- package/dist/src/routes/libs.d.ts.map +1 -1
- package/dist/src/routes/libs.js +178 -55
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +1 -0
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/schedules.d.ts +33 -0
- package/dist/src/routes/schedules.d.ts.map +1 -0
- package/dist/src/routes/schedules.js +380 -0
- package/dist/src/routes/schedules.js.map +1 -0
- package/dist/src/server-bootstrap/config-init.js +2 -2
- package/dist/src/server-bootstrap/config-init.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +7 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +2 -1
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/services/access-token.d.ts +28 -0
- package/dist/src/services/access-token.d.ts.map +1 -0
- package/dist/src/services/access-token.js +37 -0
- package/dist/src/services/access-token.js.map +1 -0
- package/dist/src/services/ai-completion.d.ts +83 -0
- package/dist/src/services/ai-completion.d.ts.map +1 -0
- package/dist/src/services/ai-completion.js +187 -0
- package/dist/src/services/ai-completion.js.map +1 -0
- package/dist/src/services/config-schema.d.ts.map +1 -1
- package/dist/src/services/config-schema.js +3 -0
- package/dist/src/services/config-schema.js.map +1 -1
- package/dist/src/services/generator-prompt-seeds.d.ts.map +1 -1
- package/dist/src/services/generator-prompt-seeds.js +6 -3
- package/dist/src/services/generator-prompt-seeds.js.map +1 -1
- package/dist/src/services/generator-prompts/spec-validate.d.ts.map +1 -1
- package/dist/src/services/generator-prompts/spec-validate.js +6 -2
- package/dist/src/services/generator-prompts/spec-validate.js.map +1 -1
- package/dist/src/services/generator-registration.d.ts +12 -4
- package/dist/src/services/generator-registration.d.ts.map +1 -1
- package/dist/src/services/generator-registration.js +71 -0
- package/dist/src/services/generator-registration.js.map +1 -1
- package/dist/src/services/generator-validate.d.ts +10 -0
- package/dist/src/services/generator-validate.d.ts.map +1 -1
- package/dist/src/services/generator-validate.js +180 -36
- package/dist/src/services/generator-validate.js.map +1 -1
- package/dist/src/services/owner-session.d.ts +43 -0
- package/dist/src/services/owner-session.d.ts.map +1 -0
- package/dist/src/services/owner-session.js +175 -0
- package/dist/src/services/owner-session.js.map +1 -0
- package/dist/src/services/schedule-constraints.d.ts +65 -0
- package/dist/src/services/schedule-constraints.d.ts.map +1 -0
- package/dist/src/services/schedule-constraints.js +106 -0
- package/dist/src/services/schedule-constraints.js.map +1 -0
- package/dist/src/services/scheduler.d.ts +34 -0
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +266 -22
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/skill-bundle/generator.js +19 -0
- package/dist/src/services/skill-bundle/generator.js.map +1 -1
- package/dist/src/storage/interface.d.ts +48 -4
- 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 +31 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +187 -9
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +31 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +131 -8
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/agent-task.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/agent-task.js +6 -4
- package/dist/src/storage/providers/sqlite/repos/agent-task.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +79 -8
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/pat.repository.d.ts +40 -0
- package/dist/src/storage/repositories/pat.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/pat.repository.js +14 -0
- package/dist/src/storage/repositories/pat.repository.js.map +1 -0
- package/dist/src/storage/repositories/scheduler.repository.d.ts +2 -0
- package/dist/src/storage/repositories/scheduler.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/session.repository.d.ts +51 -0
- package/dist/src/storage/repositories/session.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/session.repository.js +12 -0
- package/dist/src/storage/repositories/session.repository.js.map +1 -1
- package/dist/src/utils/env-config.d.ts.map +1 -1
- package/dist/src/utils/env-config.js +25 -1
- package/dist/src/utils/env-config.js.map +1 -1
- package/dist/src/utils/env-validator.d.ts.map +1 -1
- package/dist/src/utils/env-validator.js +4 -0
- package/dist/src/utils/env-validator.js.map +1 -1
- package/package.json +1 -1
- package/prisma/schema.prisma +48 -0
|
@@ -41,8 +41,8 @@ export default function PackagesTab({ session, showToast, navigate, locale }) {
|
|
|
41
41
|
const [search, setSearch] = useState('');
|
|
42
42
|
const [category, setCategory] = useState('');
|
|
43
43
|
|
|
44
|
-
const loadData = useCallback(async () => {
|
|
45
|
-
setLoading(true);
|
|
44
|
+
const loadData = useCallback(async ({ showSpinner = true } = {}) => {
|
|
45
|
+
if (showSpinner) setLoading(true);
|
|
46
46
|
try {
|
|
47
47
|
const [instRes, pkgRes, tplRes] = await Promise.all([
|
|
48
48
|
pkgService.listInstances({ status: 'installed' }),
|
|
@@ -65,7 +65,7 @@ export default function PackagesTab({ session, showToast, navigate, locale }) {
|
|
|
65
65
|
|
|
66
66
|
// Live updates
|
|
67
67
|
useEffect(() => {
|
|
68
|
-
const handler = () => { loadData(); };
|
|
68
|
+
const handler = () => { loadData({ showSpinner: false }); }; // silent: don't flash blank
|
|
69
69
|
window.addEventListener('aimeat-live-update', handler);
|
|
70
70
|
return () => window.removeEventListener('aimeat-live-update', handler);
|
|
71
71
|
}, [loadData]);
|
|
@@ -268,8 +268,8 @@ export default function PackagesTab({ session, showToast, navigate, locale }) {
|
|
|
268
268
|
</div>
|
|
269
269
|
<div class="pkg-card-footer">
|
|
270
270
|
<div class="pkg-card-actions">
|
|
271
|
-
<button onClick=${() => handleCheckUpdate(inst.id)}>${t('packages.checkUpdate') || 'Check Update'}</button>
|
|
272
|
-
<button class="danger" onClick=${() => handleRemove(inst.id)}>${t('packages.remove') || 'Remove'}</button>
|
|
271
|
+
<button class="btn-outline btn-sm" onClick=${() => handleCheckUpdate(inst.id)}>${t('packages.checkUpdate') || 'Check Update'}</button>
|
|
272
|
+
<button class="btn-danger btn-sm" onClick=${() => handleRemove(inst.id)}>${t('packages.remove') || 'Remove'}</button>
|
|
273
273
|
</div>
|
|
274
274
|
</div>
|
|
275
275
|
</div>
|
|
@@ -325,19 +325,19 @@ export default function PackagesTab({ session, showToast, navigate, locale }) {
|
|
|
325
325
|
`}
|
|
326
326
|
<div class="pkg-card-footer">
|
|
327
327
|
<div class="pkg-card-actions">
|
|
328
|
-
<button class="primary" onClick=${() => handleInstall(pkg.packageGroupId)}>
|
|
328
|
+
<button class="btn-primary btn-sm" onClick=${() => handleInstall(pkg.packageGroupId)}>
|
|
329
329
|
${t('packages.install') || 'Install'}
|
|
330
330
|
</button>
|
|
331
|
-
<button onClick=${() => handleDownloadZip(pkg.packageGroupId, pkg.name)}>
|
|
331
|
+
<button class="btn-outline btn-sm" onClick=${() => handleDownloadZip(pkg.packageGroupId, pkg.name)}>
|
|
332
332
|
${t('packages.downloadZip') || 'Download ZIP'}
|
|
333
333
|
</button>
|
|
334
|
-
<button onClick=${() => handleOpenInGenerator(pkg.packageGroupId, pkg.author)}>
|
|
334
|
+
<button class="btn-outline btn-sm" onClick=${() => handleOpenInGenerator(pkg.packageGroupId, pkg.author)}>
|
|
335
335
|
${pkg.author === session?.owner
|
|
336
336
|
? (t('packages.editInGenerator') || 'Edit in Generator')
|
|
337
337
|
: (t('packages.openInGenerator') || 'Open in Generator')}
|
|
338
338
|
</button>
|
|
339
339
|
${pkg.status === 'published' && !pkg.templateStatus && pkg.author === session?.owner && html`
|
|
340
|
-
<button class="btn-outline" onClick=${() => handleProposeAsTemplate(pkg.packageGroupId)}>
|
|
340
|
+
<button class="btn-outline btn-sm" onClick=${() => handleProposeAsTemplate(pkg.packageGroupId)}>
|
|
341
341
|
${t('packages.proposeAsTemplate') || 'Propose as Template'}
|
|
342
342
|
</button>
|
|
343
343
|
`}
|
|
@@ -371,13 +371,13 @@ export default function PackagesTab({ session, showToast, navigate, locale }) {
|
|
|
371
371
|
</div>
|
|
372
372
|
<div class="pkg-card-footer">
|
|
373
373
|
<div class="pkg-card-actions">
|
|
374
|
-
<button class="primary" onClick=${() => handleInstall(tpl.packageGroupId)}>
|
|
374
|
+
<button class="btn-primary btn-sm" onClick=${() => handleInstall(tpl.packageGroupId)}>
|
|
375
375
|
${t('packages.install') || 'Install'}
|
|
376
376
|
</button>
|
|
377
|
-
<button onClick=${() => handleDownloadZip(tpl.packageGroupId, tpl.title)}>
|
|
377
|
+
<button class="btn-outline btn-sm" onClick=${() => handleDownloadZip(tpl.packageGroupId, tpl.title)}>
|
|
378
378
|
${t('packages.downloadZip') || 'Download ZIP'}
|
|
379
379
|
</button>
|
|
380
|
-
<button onClick=${() => handleOpenInGenerator(tpl.packageGroupId, tpl.packageAuthor)}>
|
|
380
|
+
<button class="btn-outline btn-sm" onClick=${() => handleOpenInGenerator(tpl.packageGroupId, tpl.packageAuthor)}>
|
|
381
381
|
${tpl.packageAuthor === session?.owner
|
|
382
382
|
? (t('packages.editInGenerator') || 'Edit in Generator')
|
|
383
383
|
: (t('packages.openInGenerator') || 'Open in Generator')}
|
|
@@ -416,10 +416,10 @@ export default function PackagesTab({ session, showToast, navigate, locale }) {
|
|
|
416
416
|
<div class="pkg-card-footer">
|
|
417
417
|
<div class="pkg-card-actions">
|
|
418
418
|
${rt.packageGroupId ? html`
|
|
419
|
-
<button class="primary" onClick=${() => handleInstall(rt.packageGroupId)}>
|
|
419
|
+
<button class="btn-primary btn-sm" onClick=${() => handleInstall(rt.packageGroupId)}>
|
|
420
420
|
${t('packages.federation.installFromRemote') || 'Install'}
|
|
421
421
|
</button>
|
|
422
|
-
<button onClick=${() => handleDownloadZip(rt.packageGroupId, rt.title || rt.name)}>
|
|
422
|
+
<button class="btn-outline btn-sm" onClick=${() => handleDownloadZip(rt.packageGroupId, rt.title || rt.name)}>
|
|
423
423
|
${t('packages.downloadZip') || 'Download ZIP'}
|
|
424
424
|
</button>
|
|
425
425
|
` : null}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file scheduler-tab.js
|
|
3
|
+
* @description Profile › Scheduler — the master view of every recurring schedule
|
|
4
|
+
* the owner has running: AIMEAT-managed (ai / agent_task / extension), the
|
|
5
|
+
* owner's extension cron jobs, and each agent's self-reported internal
|
|
6
|
+
* scheduler. The server owns the clock; the owner can create, pause/resume,
|
|
7
|
+
* run-now, and cancel managed schedules (including any an agent created).
|
|
8
|
+
* @structure
|
|
9
|
+
* - SchedulerTab (default) — loads /v1/schedules, renders grouped sections + create form
|
|
10
|
+
* @usage Registered in profile.js TABS as { id:'scheduler', component: SchedulerTab }.
|
|
11
|
+
* @version-history
|
|
12
|
+
* v1.0.0 -- 2026-06-03 -- Initial master scheduler view
|
|
13
|
+
*/
|
|
14
|
+
import { h } from 'preact';
|
|
15
|
+
import { useState, useEffect, useRef, useCallback } from 'preact/hooks';
|
|
16
|
+
import htm from 'htm';
|
|
17
|
+
import { t } from '/js/i18n.js';
|
|
18
|
+
import { timeAgo } from '/js/utils.js';
|
|
19
|
+
import { listAgents } from '/js/services/agents.js';
|
|
20
|
+
import {
|
|
21
|
+
listAllSchedules, createSchedule, setScheduleEnabled,
|
|
22
|
+
triggerSchedule, deleteSchedule,
|
|
23
|
+
} from '/js/services/schedules.js';
|
|
24
|
+
|
|
25
|
+
const html = htm.bind(h);
|
|
26
|
+
|
|
27
|
+
const CRON_PRESETS = [
|
|
28
|
+
{ key: 'morning', cron: '0 7 * * *' },
|
|
29
|
+
{ key: 'evening', cron: '0 19 * * *' },
|
|
30
|
+
{ key: 'hourly', cron: '0 * * * *' },
|
|
31
|
+
{ key: 'custom', cron: '' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const KIND_BADGE = {
|
|
35
|
+
ai: { cls: 'sch-badge--ai', key: 'profile.scheduler.kind.ai' },
|
|
36
|
+
agent_task: { cls: 'sch-badge--agent', key: 'profile.scheduler.kind.agent_task' },
|
|
37
|
+
extension: { cls: 'sch-badge--ext', key: 'profile.scheduler.kind.extension' },
|
|
38
|
+
core: { cls: 'sch-badge--core', key: 'profile.scheduler.kind.core' },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function kindBadge(kind) {
|
|
42
|
+
const b = KIND_BADGE[kind] || KIND_BADGE.core;
|
|
43
|
+
return html`<span class="sch-badge ${b.cls}">${t(b.key)}</span>`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resultBadge(result) {
|
|
47
|
+
if (!result) return html`<span class="sch-muted">—</span>`;
|
|
48
|
+
const cls = result === 'error' ? 'sch-badge--err' : 'sch-badge--ok';
|
|
49
|
+
return html`<span class="sch-badge ${cls}">${result}</span>`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function SchedulerTab({ showToast }) {
|
|
53
|
+
const [data, setData] = useState({ managed: [], extensions: [], agentInternal: [] });
|
|
54
|
+
const [agents, setAgents] = useState([]);
|
|
55
|
+
const [loading, setLoading] = useState(true);
|
|
56
|
+
const [error, setError] = useState(null);
|
|
57
|
+
const [showForm, setShowForm] = useState(false);
|
|
58
|
+
|
|
59
|
+
const loadData = useCallback(async () => {
|
|
60
|
+
try {
|
|
61
|
+
const [schedRes, agentsRes] = await Promise.all([listAllSchedules(), listAgents().catch(() => null)]);
|
|
62
|
+
setData(schedRes?.data || { managed: [], extensions: [], agentInternal: [] });
|
|
63
|
+
setAgents(agentsRes?.data?.agents || []);
|
|
64
|
+
setError(null);
|
|
65
|
+
} catch (e) {
|
|
66
|
+
setError(e.message || 'Failed to load schedules');
|
|
67
|
+
} finally {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
const loadRef = useRef(loadData);
|
|
73
|
+
loadRef.current = loadData;
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
loadData();
|
|
76
|
+
const handler = () => loadRef.current();
|
|
77
|
+
window.addEventListener('aimeat-live-update', handler);
|
|
78
|
+
const poller = setInterval(() => loadRef.current(), 15000);
|
|
79
|
+
return () => { window.removeEventListener('aimeat-live-update', handler); clearInterval(poller); };
|
|
80
|
+
}, [loadData]);
|
|
81
|
+
|
|
82
|
+
const onToggle = async (job) => {
|
|
83
|
+
try { await setScheduleEnabled(job.id, !job.enabled); await loadData(); }
|
|
84
|
+
catch (e) { showToast?.(e.message, true); }
|
|
85
|
+
};
|
|
86
|
+
const onTrigger = async (job) => {
|
|
87
|
+
try { await triggerSchedule(job.id); showToast?.(t('profile.scheduler.triggered')); await loadData(); }
|
|
88
|
+
catch (e) { showToast?.(e.message, true); }
|
|
89
|
+
};
|
|
90
|
+
const onCancel = async (job) => {
|
|
91
|
+
if (!window.confirm(t('profile.scheduler.confirmCancel'))) return;
|
|
92
|
+
try { await deleteSchedule(job.id); showToast?.(t('profile.scheduler.cancelled')); await loadData(); }
|
|
93
|
+
catch (e) { showToast?.(e.message, true); }
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const renderManagedRow = (job) => html`
|
|
97
|
+
<tr key=${job.id}>
|
|
98
|
+
<td>
|
|
99
|
+
<div class="sch-name">${job.displayName || job.name}</div>
|
|
100
|
+
${job.purpose && html`<div class="sch-muted sch-purpose">${job.purpose}</div>`}
|
|
101
|
+
${job.createdByAgent && html`<span class="sch-tag">${t('profile.scheduler.byAgent')}</span>`}
|
|
102
|
+
</td>
|
|
103
|
+
<td>${kindBadge(job.type)}</td>
|
|
104
|
+
<td><code class="sch-cron">${job.cron}</code>${job.timezone && html`<div class="sch-muted">${job.timezone}</div>`}</td>
|
|
105
|
+
<td>${job.agentName || html`<span class="sch-muted">—</span>`}</td>
|
|
106
|
+
<td>${job.lastRunAt ? timeAgo(job.lastRunAt) : html`<span class="sch-muted">${t('profile.scheduler.never')}</span>`} ${resultBadge(job.lastRunResult)}</td>
|
|
107
|
+
<td>${job.nextRunAt ? timeAgo(job.nextRunAt) : html`<span class="sch-muted">—</span>`}</td>
|
|
108
|
+
<td>${job.runCount ?? 0}</td>
|
|
109
|
+
<td class="sch-actions">
|
|
110
|
+
<button class="btn-ghost sch-btn" onClick=${() => onToggle(job)} title=${job.enabled ? t('profile.scheduler.pause') : t('profile.scheduler.resume')}>
|
|
111
|
+
${job.enabled ? t('profile.scheduler.pause') : t('profile.scheduler.resume')}
|
|
112
|
+
</button>
|
|
113
|
+
<button class="btn-ghost sch-btn" onClick=${() => onTrigger(job)}>${t('profile.scheduler.runNow')}</button>
|
|
114
|
+
<button class="btn-danger sch-btn" onClick=${() => onCancel(job)}>${t('profile.scheduler.cancel')}</button>
|
|
115
|
+
</td>
|
|
116
|
+
</tr>`;
|
|
117
|
+
|
|
118
|
+
if (loading) return html`<div class="sch-loading">${t('profile.scheduler.loading')}</div>`;
|
|
119
|
+
|
|
120
|
+
return html`
|
|
121
|
+
<div class="sch-tab">
|
|
122
|
+
<div class="sch-head">
|
|
123
|
+
<div>
|
|
124
|
+
<div class="section-title">${t('profile.scheduler.title')}</div>
|
|
125
|
+
<div class="section-desc">${t('profile.scheduler.desc')}</div>
|
|
126
|
+
</div>
|
|
127
|
+
<button class="btn-primary" onClick=${() => setShowForm(v => !v)}>
|
|
128
|
+
${showForm ? t('profile.scheduler.close') : t('profile.scheduler.newSchedule')}
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
${error && html`<div class="sch-error">${error}</div>`}
|
|
133
|
+
|
|
134
|
+
${showForm && html`<${CreateForm} agents=${agents} showToast=${showToast}
|
|
135
|
+
onCreated=${() => { setShowForm(false); loadData(); }} />`}
|
|
136
|
+
|
|
137
|
+
<!-- AIMEAT-managed -->
|
|
138
|
+
<div class="sch-section">
|
|
139
|
+
<h3 class="sch-section-title">${t('profile.scheduler.managedTitle')}</h3>
|
|
140
|
+
${data.managed.length === 0
|
|
141
|
+
? html`<div class="sch-empty">${t('profile.scheduler.noManaged')}</div>`
|
|
142
|
+
: html`<table class="sch-table"><thead><tr>
|
|
143
|
+
<th>${t('profile.scheduler.col.name')}</th>
|
|
144
|
+
<th>${t('profile.scheduler.col.kind')}</th>
|
|
145
|
+
<th>${t('profile.scheduler.col.cron')}</th>
|
|
146
|
+
<th>${t('profile.scheduler.col.agent')}</th>
|
|
147
|
+
<th>${t('profile.scheduler.col.lastRun')}</th>
|
|
148
|
+
<th>${t('profile.scheduler.col.nextRun')}</th>
|
|
149
|
+
<th>${t('profile.scheduler.col.runs')}</th>
|
|
150
|
+
<th></th>
|
|
151
|
+
</tr></thead><tbody>${data.managed.map(renderManagedRow)}</tbody></table>`}
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<!-- Owner extension cron -->
|
|
155
|
+
${data.extensions.length > 0 && html`<div class="sch-section">
|
|
156
|
+
<h3 class="sch-section-title">${t('profile.scheduler.extTitle')}</h3>
|
|
157
|
+
<table class="sch-table"><thead><tr>
|
|
158
|
+
<th>${t('profile.scheduler.col.name')}</th>
|
|
159
|
+
<th>${t('profile.scheduler.col.extension')}</th>
|
|
160
|
+
<th>${t('profile.scheduler.col.cron')}</th>
|
|
161
|
+
<th>${t('profile.scheduler.col.lastRun')}</th>
|
|
162
|
+
<th>${t('profile.scheduler.col.nextRun')}</th>
|
|
163
|
+
</tr></thead><tbody>
|
|
164
|
+
${data.extensions.map(j => html`<tr key=${j.id}>
|
|
165
|
+
<td>${j.displayName || j.name}</td>
|
|
166
|
+
<td><code class="sch-cron">${j.extensionName}${j.actionId ? '/' + j.actionId : ''}</code></td>
|
|
167
|
+
<td><code class="sch-cron">${j.cron}</code></td>
|
|
168
|
+
<td>${j.lastRunAt ? timeAgo(j.lastRunAt) : '—'} ${resultBadge(j.lastRunResult)}</td>
|
|
169
|
+
<td>${j.nextRunAt ? timeAgo(j.nextRunAt) : '—'}</td>
|
|
170
|
+
</tr>`)}
|
|
171
|
+
</tbody></table>
|
|
172
|
+
</div>`}
|
|
173
|
+
|
|
174
|
+
<!-- Agent internal (read-only mirror) -->
|
|
175
|
+
${data.agentInternal.length > 0 && html`<div class="sch-section">
|
|
176
|
+
<h3 class="sch-section-title">${t('profile.scheduler.internalTitle')}</h3>
|
|
177
|
+
<div class="sch-muted sch-internal-note">${t('profile.scheduler.internalNote')}</div>
|
|
178
|
+
${data.agentInternal.map(grp => html`<div class="sch-internal-group" key=${grp.gaii}>
|
|
179
|
+
<div class="sch-internal-agent">${grp.agentName}</div>
|
|
180
|
+
<table class="sch-table"><tbody>
|
|
181
|
+
${grp.entries.map((e, i) => html`<tr key=${e.id || i}>
|
|
182
|
+
<td>${e.name}${e.purpose && html`<div class="sch-muted">${e.purpose}</div>`}</td>
|
|
183
|
+
<td><code class="sch-cron">${e.cron || e.schedule || '—'}</code>${e.timezone && html`<div class="sch-muted">${e.timezone}</div>`}</td>
|
|
184
|
+
<td>${e.status || 'active'}</td>
|
|
185
|
+
</tr>`)}
|
|
186
|
+
</tbody></table>
|
|
187
|
+
</div>`)}
|
|
188
|
+
</div>`}
|
|
189
|
+
</div>`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Create form (exported so the agent Schedules sub-tab can reuse it) ──────
|
|
193
|
+
export function CreateForm({ agents = [], showToast, onCreated, lockedAgent }) {
|
|
194
|
+
const [kind, setKind] = useState(lockedAgent ? 'agent_task' : 'ai');
|
|
195
|
+
const [preset, setPreset] = useState('morning');
|
|
196
|
+
const [form, setForm] = useState({
|
|
197
|
+
display_name: '', cron: '0 7 * * *', timezone: '', purpose: '',
|
|
198
|
+
agent_name: lockedAgent || '', prompt: '', input_keys: '', output_key: '',
|
|
199
|
+
task_title: '', task_description: '', extension_name: '', action_id: '',
|
|
200
|
+
});
|
|
201
|
+
const [maxRuns, setMaxRuns] = useState({ enabled: false, limit: 7 });
|
|
202
|
+
const [dailyLimit, setDailyLimit] = useState({ enabled: false, limit: 1 });
|
|
203
|
+
const [saving, setSaving] = useState(false);
|
|
204
|
+
|
|
205
|
+
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
|
|
206
|
+
const onPreset = (p) => { setPreset(p); const found = CRON_PRESETS.find(c => c.key === p); if (found && found.cron) set('cron', found.cron); };
|
|
207
|
+
|
|
208
|
+
const buildConstraints = () => {
|
|
209
|
+
const c = [];
|
|
210
|
+
if (maxRuns.enabled) c.push({ type: 'max_runs', enabled: true, params: { limit: Number(maxRuns.limit) } });
|
|
211
|
+
if (dailyLimit.enabled) c.push({ type: 'daily_limit', enabled: true, params: { limit: Number(dailyLimit.limit) } });
|
|
212
|
+
return c;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const submit = async () => {
|
|
216
|
+
if (!form.display_name.trim()) { showToast?.(t('profile.scheduler.err.name'), true); return; }
|
|
217
|
+
if (!form.cron.trim()) { showToast?.(t('profile.scheduler.err.cron'), true); return; }
|
|
218
|
+
const body = {
|
|
219
|
+
kind, cron: form.cron.trim(), display_name: form.display_name.trim(),
|
|
220
|
+
timezone: form.timezone.trim() || undefined, purpose: form.purpose.trim() || undefined,
|
|
221
|
+
constraints: buildConstraints(),
|
|
222
|
+
};
|
|
223
|
+
if (kind === 'ai') {
|
|
224
|
+
if (!form.prompt.trim()) { showToast?.(t('profile.scheduler.err.prompt'), true); return; }
|
|
225
|
+
body.prompt = form.prompt.trim();
|
|
226
|
+
body.input_keys = form.input_keys.split(',').map(s => s.trim()).filter(Boolean);
|
|
227
|
+
if (form.output_key.trim()) body.output_key = form.output_key.trim();
|
|
228
|
+
if (form.agent_name) body.agent_name = form.agent_name;
|
|
229
|
+
} else if (kind === 'agent_task') {
|
|
230
|
+
if (!form.agent_name) { showToast?.(t('profile.scheduler.err.agent'), true); return; }
|
|
231
|
+
if (!form.task_title.trim()) { showToast?.(t('profile.scheduler.err.taskTitle'), true); return; }
|
|
232
|
+
body.agent_name = form.agent_name;
|
|
233
|
+
body.task_template = { title: form.task_title.trim(), description: form.task_description.trim() };
|
|
234
|
+
} else if (kind === 'extension') {
|
|
235
|
+
if (!form.extension_name.trim() || !form.action_id.trim()) { showToast?.(t('profile.scheduler.err.ext'), true); return; }
|
|
236
|
+
body.extension_name = form.extension_name.trim();
|
|
237
|
+
body.action_id = form.action_id.trim();
|
|
238
|
+
if (form.agent_name) body.agent_name = form.agent_name;
|
|
239
|
+
}
|
|
240
|
+
setSaving(true);
|
|
241
|
+
try {
|
|
242
|
+
await createSchedule(body);
|
|
243
|
+
showToast?.(t('profile.scheduler.created'));
|
|
244
|
+
onCreated?.();
|
|
245
|
+
} catch (e) { showToast?.(e.message, true); }
|
|
246
|
+
finally { setSaving(false); }
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
return html`
|
|
250
|
+
<div class="sch-form">
|
|
251
|
+
<div class="sch-form-row">
|
|
252
|
+
<label>${t('profile.scheduler.field.kind')}</label>
|
|
253
|
+
<select value=${kind} onChange=${e => setKind(e.target.value)}>
|
|
254
|
+
<option value="ai">${t('profile.scheduler.kind.ai')} — ${t('profile.scheduler.kindHint.ai')}</option>
|
|
255
|
+
<option value="agent_task">${t('profile.scheduler.kind.agent_task')} — ${t('profile.scheduler.kindHint.agent_task')}</option>
|
|
256
|
+
<option value="extension">${t('profile.scheduler.kind.extension')} — ${t('profile.scheduler.kindHint.extension')}</option>
|
|
257
|
+
</select>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div class="sch-form-row">
|
|
261
|
+
<label>${t('profile.scheduler.field.displayName')}</label>
|
|
262
|
+
<input type="text" value=${form.display_name} onInput=${e => set('display_name', e.target.value)}
|
|
263
|
+
placeholder=${t('profile.scheduler.ph.displayName')} />
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="sch-form-row">
|
|
267
|
+
<label>${t('profile.scheduler.field.schedule')}</label>
|
|
268
|
+
<div class="sch-presets">
|
|
269
|
+
${CRON_PRESETS.map(p => html`<button type="button"
|
|
270
|
+
class="sch-preset ${preset === p.key ? 'sch-preset--on' : ''}"
|
|
271
|
+
onClick=${() => onPreset(p.key)}>${t('profile.scheduler.preset.' + p.key)}</button>`)}
|
|
272
|
+
</div>
|
|
273
|
+
<input type="text" class="sch-cron-input" value=${form.cron} onInput=${e => set('cron', e.target.value)}
|
|
274
|
+
placeholder="0 7 * * *" />
|
|
275
|
+
<input type="text" value=${form.timezone} onInput=${e => set('timezone', e.target.value)}
|
|
276
|
+
placeholder=${t('profile.scheduler.ph.timezone')} />
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
${kind === 'ai' && html`<div class="sch-kind-fields">
|
|
280
|
+
<div class="sch-form-row"><label>${t('profile.scheduler.field.inputKeys')}</label>
|
|
281
|
+
<input type="text" value=${form.input_keys} onInput=${e => set('input_keys', e.target.value)}
|
|
282
|
+
placeholder=${t('profile.scheduler.ph.inputKeys')} /></div>
|
|
283
|
+
<div class="sch-form-row"><label>${t('profile.scheduler.field.prompt')}</label>
|
|
284
|
+
<textarea rows="3" value=${form.prompt} onInput=${e => set('prompt', e.target.value)}
|
|
285
|
+
placeholder=${t('profile.scheduler.ph.prompt')}></textarea></div>
|
|
286
|
+
<div class="sch-form-row"><label>${t('profile.scheduler.field.outputKey')}</label>
|
|
287
|
+
<input type="text" value=${form.output_key} onInput=${e => set('output_key', e.target.value)}
|
|
288
|
+
placeholder=${t('profile.scheduler.ph.outputKey')} /></div>
|
|
289
|
+
</div>`}
|
|
290
|
+
|
|
291
|
+
${kind === 'agent_task' && html`<div class="sch-kind-fields">
|
|
292
|
+
<div class="sch-form-row"><label>${t('profile.scheduler.field.agent')}</label>
|
|
293
|
+
${lockedAgent
|
|
294
|
+
? html`<input type="text" value=${lockedAgent} disabled />`
|
|
295
|
+
: html`<select value=${form.agent_name} onChange=${e => set('agent_name', e.target.value)}>
|
|
296
|
+
<option value="">${t('profile.scheduler.ph.agent')}</option>
|
|
297
|
+
${agents.map(a => html`<option value=${a.name}>${a.name}</option>`)}
|
|
298
|
+
</select>`}</div>
|
|
299
|
+
<div class="sch-form-row"><label>${t('profile.scheduler.field.taskTitle')}</label>
|
|
300
|
+
<input type="text" value=${form.task_title} onInput=${e => set('task_title', e.target.value)} /></div>
|
|
301
|
+
<div class="sch-form-row"><label>${t('profile.scheduler.field.taskDescription')}</label>
|
|
302
|
+
<textarea rows="3" value=${form.task_description} onInput=${e => set('task_description', e.target.value)}></textarea></div>
|
|
303
|
+
</div>`}
|
|
304
|
+
|
|
305
|
+
${kind === 'extension' && html`<div class="sch-kind-fields">
|
|
306
|
+
<div class="sch-form-row"><label>${t('profile.scheduler.field.extensionName')}</label>
|
|
307
|
+
<input type="text" value=${form.extension_name} onInput=${e => set('extension_name', e.target.value)} /></div>
|
|
308
|
+
<div class="sch-form-row"><label>${t('profile.scheduler.field.actionId')}</label>
|
|
309
|
+
<input type="text" value=${form.action_id} onInput=${e => set('action_id', e.target.value)} /></div>
|
|
310
|
+
</div>`}
|
|
311
|
+
|
|
312
|
+
<div class="sch-form-row">
|
|
313
|
+
<label>${t('profile.scheduler.field.purpose')}</label>
|
|
314
|
+
<input type="text" value=${form.purpose} onInput=${e => set('purpose', e.target.value)}
|
|
315
|
+
placeholder=${t('profile.scheduler.ph.purpose')} />
|
|
316
|
+
</div>
|
|
317
|
+
|
|
318
|
+
<div class="sch-constraints">
|
|
319
|
+
<div class="sch-constraints-title">${t('profile.scheduler.constraints')}</div>
|
|
320
|
+
<label class="sch-check">
|
|
321
|
+
<input type="checkbox" checked=${maxRuns.enabled} onChange=${e => setMaxRuns(s => ({ ...s, enabled: e.target.checked }))} />
|
|
322
|
+
${t('profile.scheduler.maxRuns')}
|
|
323
|
+
<input type="number" min="1" value=${maxRuns.limit} disabled=${!maxRuns.enabled}
|
|
324
|
+
onInput=${e => setMaxRuns(s => ({ ...s, limit: e.target.value }))} class="sch-num" />
|
|
325
|
+
</label>
|
|
326
|
+
<label class="sch-check">
|
|
327
|
+
<input type="checkbox" checked=${dailyLimit.enabled} onChange=${e => setDailyLimit(s => ({ ...s, enabled: e.target.checked }))} />
|
|
328
|
+
${t('profile.scheduler.dailyLimit')}
|
|
329
|
+
<input type="number" min="0" step="0.1" value=${dailyLimit.limit} disabled=${!dailyLimit.enabled}
|
|
330
|
+
onInput=${e => setDailyLimit(s => ({ ...s, limit: e.target.value }))} class="sch-num" />
|
|
331
|
+
</label>
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div class="sch-form-actions">
|
|
335
|
+
<button class="btn-primary" disabled=${saving} onClick=${submit}>
|
|
336
|
+
${saving ? t('profile.scheduler.saving') : t('profile.scheduler.create')}
|
|
337
|
+
</button>
|
|
338
|
+
</div>
|
|
339
|
+
</div>`;
|
|
340
|
+
}
|
|
@@ -87,7 +87,7 @@ function ServiceDetail({ svc }) {
|
|
|
87
87
|
return () => { cancelled = true; };
|
|
88
88
|
}, [svcId]);
|
|
89
89
|
|
|
90
|
-
if (loading) return html`<div class="svc-detail" onClick=${e => e.stopPropagation()}><${Spinner} text
|
|
90
|
+
if (loading) return html`<div class="svc-detail" onClick=${e => e.stopPropagation()}><${Spinner} text=${t('common.loading')} /></div>`;
|
|
91
91
|
if (error_) return html`<div class="svc-detail" onClick=${e => e.stopPropagation()}><div class="svc-detail-error">${error_}</div></div>`;
|
|
92
92
|
|
|
93
93
|
// Merge local svc data with fetched detail (detail may have more fields)
|
|
@@ -5,15 +5,23 @@
|
|
|
5
5
|
* @version-history
|
|
6
6
|
* v1.0.0 — 2026-03-07 — Initial shared helpers (Spinner, recipientBadge, isExpiringSoon)
|
|
7
7
|
* v1.1.0 — 2026-03-17 — Add VisibilityPill, ToggleSwitch, GlassCard components; refactor recipientBadge to CSS classes
|
|
8
|
+
* v1.2.0 — 2026-06-02 — Component unification (§2): Spinner now delegates to the
|
|
9
|
+
* canonical /components/Spinner.js (single source of the .spinner markup).
|
|
10
|
+
* v1.3.0 — 2026-06-02 — Component unification (#22): GlassCard now delegates to the
|
|
11
|
+
* canonical /components/Card.js via variant="glass" (.card-glass); call sites unchanged.
|
|
8
12
|
*/
|
|
9
13
|
import { h } from 'preact';
|
|
10
14
|
import htm from 'htm';
|
|
11
15
|
const html = htm.bind(h);
|
|
12
16
|
import { t } from '/js/i18n.js';
|
|
17
|
+
import { Spinner as BaseSpinner } from '/components/Spinner.js';
|
|
18
|
+
import { Card } from '/components/Card.js';
|
|
13
19
|
|
|
14
|
-
/** Loading spinner.
|
|
20
|
+
/** Loading spinner — delegates to the canonical /components/Spinner.js (single
|
|
21
|
+
* source of the .spinner markup); keeps the profile default loading label so the
|
|
22
|
+
* 23 profile call sites that rely on it are unchanged. */
|
|
15
23
|
export function Spinner({ text }) {
|
|
16
|
-
return html
|
|
24
|
+
return html`<${BaseSpinner} text=${text || t('profile.loading')} />`;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
/** Render a colored recipient-type badge. */
|
|
@@ -43,16 +51,12 @@ export function VisibilityPill({ visibility, onClick }) {
|
|
|
43
51
|
</button>`;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
|
-
/** Shared toggle switch
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<input type="checkbox" checked=${checked} onChange=${onChange} class="pf-toggle-input" />
|
|
50
|
-
<span class="pf-toggle-slider"></span>
|
|
51
|
-
${label && html`<span class="pf-toggle-label">${label}</span>`}
|
|
52
|
-
</label>`;
|
|
53
|
-
}
|
|
54
|
+
/** Shared toggle switch — relocated to the canonical /components/ToggleSwitch.js (#14);
|
|
55
|
+
* re-exported here so notifications-tab/email-tab imports are unchanged. */
|
|
56
|
+
export { ToggleSwitch } from '/components/ToggleSwitch.js';
|
|
54
57
|
|
|
55
|
-
/** Glass-style card container (email-tab, notifications-tab)
|
|
58
|
+
/** Glass-style card container (email-tab, notifications-tab) — delegates to the
|
|
59
|
+
* canonical /components/Card.js (variant="glass" → .card-glass); call sites unchanged. */
|
|
56
60
|
export function GlassCard({ children }) {
|
|
57
|
-
return html
|
|
61
|
+
return html`<${Card} variant="glass" hoverable=${false}>${children}<//>`;
|
|
58
62
|
}
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* v1.0.0 — 2026-03-16 — Initial wallet tab
|
|
7
7
|
* v1.1.0 — 2026-03-17 — Replace inline styles with CSS classes
|
|
8
8
|
* v1.2.0 — 2026-05-22 — Show info message for federated users instead of infinite spinner
|
|
9
|
+
* v1.3.0 — 2026-06-02 — Component unification (#1): copy buttons use canonical
|
|
10
|
+
* <CopyButton>; delete local copyToClipboard helper + dead copied-state/handlers.
|
|
9
11
|
*/
|
|
10
12
|
import { h } from 'preact';
|
|
11
13
|
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
|
|
@@ -14,18 +16,9 @@ const html = htm.bind(h);
|
|
|
14
16
|
import { t } from '/js/i18n.js';
|
|
15
17
|
import { escHtml, timeAgo } from '/js/utils.js';
|
|
16
18
|
import { Spinner } from './shared.js';
|
|
19
|
+
import { CopyButton } from '/components/CopyButton.js';
|
|
17
20
|
import { getWallet, getTransactions, requestMorsels } from '/js/services/wallet.js';
|
|
18
21
|
|
|
19
|
-
/**
|
|
20
|
-
* Copy text to clipboard, return true on success.
|
|
21
|
-
*/
|
|
22
|
-
async function copyToClipboard(text) {
|
|
23
|
-
try {
|
|
24
|
-
await navigator.clipboard.writeText(text);
|
|
25
|
-
return true;
|
|
26
|
-
} catch { return false; }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
22
|
/**
|
|
30
23
|
* Map backend transaction type to a display label.
|
|
31
24
|
*/
|
|
@@ -43,8 +36,6 @@ export default function WalletTab({ session, showToast, onStats }) {
|
|
|
43
36
|
const [walletData, setWalletData] = useState(null);
|
|
44
37
|
const [walletTx, setWalletTx] = useState(null);
|
|
45
38
|
const [expandedTx, setExpandedTx] = useState(null);
|
|
46
|
-
const [copiedId, setCopiedId] = useState(null);
|
|
47
|
-
const [balanceCopied, setBalanceCopied] = useState(false);
|
|
48
39
|
|
|
49
40
|
// Request form state
|
|
50
41
|
const [reqAmount, setReqAmount] = useState('');
|
|
@@ -81,33 +72,6 @@ export default function WalletTab({ session, showToast, onStats }) {
|
|
|
81
72
|
setExpandedTx(prev => prev === id ? null : id);
|
|
82
73
|
}, []);
|
|
83
74
|
|
|
84
|
-
async function handleCopyBalance() {
|
|
85
|
-
const w = walletData;
|
|
86
|
-
const text = `Balance: ${w.balance ?? 0} | Available: ${(w.balance ?? 0) - (w.in_escrow ?? w.escrow ?? 0)} | Escrow: ${w.in_escrow ?? w.escrow ?? 0}`;
|
|
87
|
-
const ok = await copyToClipboard(text);
|
|
88
|
-
if (ok) {
|
|
89
|
-
setBalanceCopied(true);
|
|
90
|
-
setTimeout(() => setBalanceCopied(false), 2000);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function handleCopyTx(tx, e) {
|
|
95
|
-
e.stopPropagation();
|
|
96
|
-
const lines = [
|
|
97
|
-
`ID: ${tx.id || '-'}`,
|
|
98
|
-
`Type: ${tx.type || '-'}`,
|
|
99
|
-
`Amount: ${tx.amount}`,
|
|
100
|
-
tx.counterparty_gaii ? `Counterparty: ${tx.counterparty_gaii}` : null,
|
|
101
|
-
tx.tracking_code ? `Tracking: ${tx.tracking_code}` : null,
|
|
102
|
-
tx.timestamp ? `Time: ${tx.timestamp}` : null,
|
|
103
|
-
].filter(Boolean).join('\n');
|
|
104
|
-
const ok = await copyToClipboard(lines);
|
|
105
|
-
if (ok) {
|
|
106
|
-
setCopiedId(tx.id);
|
|
107
|
-
setTimeout(() => setCopiedId(null), 2000);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
75
|
async function handleRequestMorsels(e) {
|
|
112
76
|
e.preventDefault();
|
|
113
77
|
const amount = parseInt(reqAmount, 10);
|
|
@@ -176,19 +140,21 @@ export default function WalletTab({ session, showToast, onStats }) {
|
|
|
176
140
|
|
|
177
141
|
<!-- Copy balance button -->
|
|
178
142
|
<div class="mb-1">
|
|
179
|
-
|
|
180
|
-
${
|
|
181
|
-
|
|
143
|
+
<${CopyButton}
|
|
144
|
+
text=${`Balance: ${w.balance ?? 0} | Available: ${(w.balance ?? 0) - escrow} | Escrow: ${escrow}`}
|
|
145
|
+
label=${t('profile.wallet.copyBalance')}
|
|
146
|
+
copiedLabel=${t('profile.wallet.copied')}
|
|
147
|
+
className="btn-outline btn-sm" />
|
|
182
148
|
</div>
|
|
183
149
|
|
|
184
150
|
<!-- Lifetime stats -->
|
|
185
151
|
${(lifetime.earned != null || lifetime.spent != null) && html`
|
|
186
152
|
<div class="section-title">${t('profile.wallet.lifetime')}</div>
|
|
187
153
|
<div class="wallet-lifetime">
|
|
188
|
-
<div class="
|
|
189
|
-
<div class="
|
|
190
|
-
<div class="
|
|
191
|
-
<div class="
|
|
154
|
+
<div class="stat-card"><div class="stat-card-value mono success">${lifetime.earned ?? 0}</div><div class="stat-card-label">${t('profile.wallet.lifetimeEarned')}</div></div>
|
|
155
|
+
<div class="stat-card"><div class="stat-card-value mono danger">${lifetime.spent ?? 0}</div><div class="stat-card-label">${t('profile.wallet.lifetimeSpent')}</div></div>
|
|
156
|
+
<div class="stat-card"><div class="stat-card-value mono">${lifetime.received_allowance ?? 0}</div><div class="stat-card-label">${t('profile.wallet.lifetimeAllowance')}</div></div>
|
|
157
|
+
<div class="stat-card"><div class="stat-card-value mono">${lifetime.welcome_bonus ?? 0}</div><div class="stat-card-label">${t('profile.wallet.lifetimeWelcome')}</div></div>
|
|
192
158
|
</div>
|
|
193
159
|
`}
|
|
194
160
|
|
|
@@ -230,7 +196,14 @@ export default function WalletTab({ session, showToast, onStats }) {
|
|
|
230
196
|
const isCredit = tx.amount > 0;
|
|
231
197
|
const typeLabel = txTypeLabel(tx);
|
|
232
198
|
const isExpanded = expandedTx === tx.id;
|
|
233
|
-
const
|
|
199
|
+
const txCopyText = [
|
|
200
|
+
`ID: ${tx.id || '-'}`,
|
|
201
|
+
`Type: ${tx.type || '-'}`,
|
|
202
|
+
`Amount: ${tx.amount}`,
|
|
203
|
+
tx.counterparty_gaii ? `Counterparty: ${tx.counterparty_gaii}` : null,
|
|
204
|
+
tx.tracking_code ? `Tracking: ${tx.tracking_code}` : null,
|
|
205
|
+
tx.timestamp ? `Time: ${tx.timestamp}` : null,
|
|
206
|
+
].filter(Boolean).join('\n');
|
|
234
207
|
return html`
|
|
235
208
|
<div class="tx-item-wrap">
|
|
236
209
|
<div class="tx-item" onClick=${() => toggleTx(tx.id)}>
|
|
@@ -273,9 +246,13 @@ export default function WalletTab({ session, showToast, onStats }) {
|
|
|
273
246
|
<span class="tx-detail-value work-grid-span">${t('profile.wallet.noDetails')}</span>
|
|
274
247
|
`}
|
|
275
248
|
<div class="work-grid-span">
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
249
|
+
<span onClick=${(e) => e.stopPropagation()}>
|
|
250
|
+
<${CopyButton}
|
|
251
|
+
text=${txCopyText}
|
|
252
|
+
label=${t('profile.wallet.copyTx')}
|
|
253
|
+
copiedLabel=${t('profile.wallet.copied')}
|
|
254
|
+
className="btn-outline btn-sm" />
|
|
255
|
+
</span>
|
|
279
256
|
</div>
|
|
280
257
|
</div>
|
|
281
258
|
`}
|