@enfyra/mcp-server 0.1.3 → 0.1.4
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/package.json
CHANGED
package/src/lib/mcp-examples.js
CHANGED
|
@@ -1705,13 +1705,17 @@ register({
|
|
|
1705
1705
|
|
|
1706
1706
|
const { register: registerMenuNotification, unregister: unregisterMenuNotification } = useMenuNotificationRegistry()
|
|
1707
1707
|
watchEffect(() => {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1708
|
+
if (notificationBadge.value) {
|
|
1709
|
+
registerMenuNotification({
|
|
1710
|
+
id: 'notifications-menu-unread',
|
|
1711
|
+
target: { path: '/notifications' },
|
|
1712
|
+
value: notificationBadge.value,
|
|
1713
|
+
color: 'error',
|
|
1714
|
+
title: notificationDescription.value,
|
|
1715
|
+
})
|
|
1716
|
+
} else {
|
|
1717
|
+
unregisterMenuNotification('notifications-menu-unread')
|
|
1718
|
+
}
|
|
1715
1719
|
})
|
|
1716
1720
|
|
|
1717
1721
|
const { adminSocket } = useAdminSocket()
|
|
@@ -1737,6 +1741,8 @@ ensure_global_extension({
|
|
|
1737
1741
|
'Global extensions are mounted invisibly by Enfyra admin UI during layout init; do not create a menu and do not embed them with Widget.',
|
|
1738
1742
|
'Use them for shell-level registrations, realtime listeners, notification counters, account panel rows, and background refresh bridges.',
|
|
1739
1743
|
'Use useMenuNotificationRegistry for sidebar menu counts/dots when notification state should be visible in the menu as well as the notification center.',
|
|
1744
|
+
'Choose value only when the signal source already owns an exact count. Omit value for a dot when realtime only proves that something new exists.',
|
|
1745
|
+
'Do not fetch the destination domain list just to decorate a menu. A mail page fetches mail; a support page fetches tickets; the shell should use notification or summary signals.',
|
|
1740
1746
|
'Keep the global extension template empty or hidden; visible UI should be registered into an existing shell registry or component slot.',
|
|
1741
1747
|
'For account-panel UI, register data-driven row fields so Enfyra admin UI owns icon size, row spacing, badge placement, hover state, and expanded chrome.',
|
|
1742
1748
|
'Use contentComponent only for expanded inner content; use raw component only as an escape hatch when the row cannot fit the shell contract.',
|
|
@@ -1744,6 +1750,159 @@ ensure_global_extension({
|
|
|
1744
1750
|
'Remove socket or DOM listeners in onUnmounted; The Enfyra admin UI unmounts old global components when extension cache reloads or the extension is disabled.',
|
|
1745
1751
|
],
|
|
1746
1752
|
},
|
|
1753
|
+
{
|
|
1754
|
+
name: 'Signal menu attention without polling destination lists',
|
|
1755
|
+
code: `const signalBridgeCode = \`
|
|
1756
|
+
<template></template>
|
|
1757
|
+
|
|
1758
|
+
<script setup>
|
|
1759
|
+
const attentionRows = ref([])
|
|
1760
|
+
const notificationSignal = ref(false)
|
|
1761
|
+
const route = useRoute()
|
|
1762
|
+
|
|
1763
|
+
const notificationApi = useApi('/cloud_admin_notifications', {
|
|
1764
|
+
query: {
|
|
1765
|
+
filter: JSON.stringify({ readAt: { _is_null: true } }),
|
|
1766
|
+
fields: 'id,kind,targetPath,readAt',
|
|
1767
|
+
sort: '-createdAt,-id',
|
|
1768
|
+
limit: 10,
|
|
1769
|
+
},
|
|
1770
|
+
})
|
|
1771
|
+
|
|
1772
|
+
const hasNewEmail = computed(() =>
|
|
1773
|
+
attentionRows.value.some((row) => row.kind === 'email_inbound' && !row.readAt)
|
|
1774
|
+
)
|
|
1775
|
+
const hasNewSupport = computed(() =>
|
|
1776
|
+
attentionRows.value.some((row) => row.kind === 'support' && !row.readAt)
|
|
1777
|
+
)
|
|
1778
|
+
const accountBadge = computed(() => notificationSignal.value ? 'New' : null)
|
|
1779
|
+
const accountDescription = computed(() => notificationSignal.value ? 'New admin attention' : 'All caught up')
|
|
1780
|
+
|
|
1781
|
+
function syncFromNotificationRows() {
|
|
1782
|
+
const value = notificationApi.data?.value
|
|
1783
|
+
const rows = Array.isArray(value?.data)
|
|
1784
|
+
? value.data
|
|
1785
|
+
: Array.isArray(value?.data?.data)
|
|
1786
|
+
? value.data.data
|
|
1787
|
+
: []
|
|
1788
|
+
attentionRows.value = rows
|
|
1789
|
+
notificationSignal.value = rows.some((row) => !row.readAt)
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
async function refreshNotificationSignals() {
|
|
1793
|
+
await notificationApi.execute()
|
|
1794
|
+
syncFromNotificationRows()
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
const { register: registerAccountPanel } = useAccountPanelRegistry()
|
|
1798
|
+
registerAccountPanel({
|
|
1799
|
+
id: 'admin-attention',
|
|
1800
|
+
order: 20,
|
|
1801
|
+
label: 'Notifications',
|
|
1802
|
+
icon: computed(() => notificationSignal.value ? 'lucide:bell-ring' : 'lucide:bell'),
|
|
1803
|
+
description: accountDescription,
|
|
1804
|
+
count: accountBadge,
|
|
1805
|
+
badgeColor: 'info',
|
|
1806
|
+
onClick: () => navigateTo('/data/cloud_admin_notifications'),
|
|
1807
|
+
})
|
|
1808
|
+
|
|
1809
|
+
const { register: registerMenuNotification, unregister: unregisterMenuNotification } = useMenuNotificationRegistry()
|
|
1810
|
+
watchEffect(() => {
|
|
1811
|
+
if (hasNewEmail.value) {
|
|
1812
|
+
registerMenuNotification({
|
|
1813
|
+
id: 'attention-email',
|
|
1814
|
+
target: { path: '/email/messages' },
|
|
1815
|
+
color: 'info',
|
|
1816
|
+
title: 'New inbound email',
|
|
1817
|
+
})
|
|
1818
|
+
} else {
|
|
1819
|
+
unregisterMenuNotification('attention-email')
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
if (hasNewSupport.value) {
|
|
1823
|
+
registerMenuNotification({
|
|
1824
|
+
id: 'attention-support',
|
|
1825
|
+
target: { path: '/cloud/support' },
|
|
1826
|
+
color: 'info',
|
|
1827
|
+
title: 'New support activity',
|
|
1828
|
+
})
|
|
1829
|
+
} else {
|
|
1830
|
+
unregisterMenuNotification('attention-support')
|
|
1831
|
+
}
|
|
1832
|
+
})
|
|
1833
|
+
|
|
1834
|
+
watch(() => route.path, (path) => {
|
|
1835
|
+
if (path.startsWith('/email/messages')) {
|
|
1836
|
+
attentionRows.value = attentionRows.value.filter((row) => row.kind !== 'email_inbound')
|
|
1837
|
+
}
|
|
1838
|
+
if (path.startsWith('/cloud/support')) {
|
|
1839
|
+
attentionRows.value = attentionRows.value.filter((row) => row.kind !== 'support')
|
|
1840
|
+
}
|
|
1841
|
+
notificationSignal.value = attentionRows.value.some((row) => !row.readAt)
|
|
1842
|
+
})
|
|
1843
|
+
|
|
1844
|
+
const { adminSocket } = useAdminSocket()
|
|
1845
|
+
function handleAdminNotification(payload) {
|
|
1846
|
+
refreshNotificationSignals()
|
|
1847
|
+
if (payload?.kind === 'email_inbound') {
|
|
1848
|
+
registerMenuNotification({ id: 'attention-email', target: { path: '/email/messages' }, color: 'info', title: 'New inbound email' })
|
|
1849
|
+
}
|
|
1850
|
+
if (payload?.kind === 'support') {
|
|
1851
|
+
registerMenuNotification({ id: 'attention-support', target: { path: '/cloud/support' }, color: 'info', title: 'New support activity' })
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function getAdminSocket() {
|
|
1856
|
+
return adminSocket && adminSocket.value !== undefined ? adminSocket.value : adminSocket
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function bindAdminSocket(socket) {
|
|
1860
|
+
if (socket && typeof socket.on === 'function') {
|
|
1861
|
+
socket.on('admin:notification-created', handleAdminNotification)
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function unbindAdminSocket(socket) {
|
|
1866
|
+
if (socket && typeof socket.off === 'function') {
|
|
1867
|
+
socket.off('admin:notification-created', handleAdminNotification)
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
if (adminSocket && adminSocket.value !== undefined) {
|
|
1872
|
+
watch(adminSocket, (nextSocket, previousSocket) => {
|
|
1873
|
+
unbindAdminSocket(previousSocket)
|
|
1874
|
+
bindAdminSocket(nextSocket)
|
|
1875
|
+
})
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
onMounted(() => {
|
|
1879
|
+
refreshNotificationSignals()
|
|
1880
|
+
bindAdminSocket(getAdminSocket())
|
|
1881
|
+
})
|
|
1882
|
+
onUnmounted(() => {
|
|
1883
|
+
unbindAdminSocket(getAdminSocket())
|
|
1884
|
+
unregisterMenuNotification('attention-email')
|
|
1885
|
+
unregisterMenuNotification('attention-support')
|
|
1886
|
+
})
|
|
1887
|
+
</script>
|
|
1888
|
+
\`
|
|
1889
|
+
|
|
1890
|
+
ensure_global_extension({
|
|
1891
|
+
name: "AdminAttentionSignalBridge",
|
|
1892
|
+
description: "Routes notification signals into account-panel and sidebar menu attention markers without polling destination lists",
|
|
1893
|
+
code: signalBridgeCode,
|
|
1894
|
+
isEnabled: true,
|
|
1895
|
+
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1896
|
+
})`,
|
|
1897
|
+
notes: [
|
|
1898
|
+
'Use this pattern when the shell should show attention but the destination page owns the expensive or domain-specific list fetch.',
|
|
1899
|
+
'This example fetches only the notification source of truth, not the email, support, order, or job tables. Substitute your own notification or summary endpoint when available.',
|
|
1900
|
+
'Omitting value on registerMenuNotification renders a dot. That is the right promise when the shell knows "new work exists" but not an exact count.',
|
|
1901
|
+
'If a backend summary event already includes an exact unread count, use value for a count chip. If the event only says one record changed, use a dot and let the page fetch details.',
|
|
1902
|
+
'Map notification kinds to menu targets by product meaning, not by copying these paths. For example, approval_required could target /reviews, failed_job could target /operations/jobs, and quota_warning could target /billing.',
|
|
1903
|
+
'Clear local dot signals when the user enters the destination route or when the notification center marks the underlying notification as read.',
|
|
1904
|
+
],
|
|
1905
|
+
},
|
|
1747
1906
|
{
|
|
1748
1907
|
name: 'Register a data-driven account-panel item',
|
|
1749
1908
|
code: `<script setup>
|
|
@@ -305,6 +305,7 @@ function getExtensionThemeContract() {
|
|
|
305
305
|
'Page extensions should be full-bleed, responsive, and split large operations into focused pages or UTabs.',
|
|
306
306
|
'Use usePageHeaderRegistry for the shell title and useHeaderActionRegistry/useSubHeaderActionRegistry for page actions.',
|
|
307
307
|
'Use useMenuNotificationRegistry from global extensions to register sidebar menu notification counts or dots. Register stable ids, target menus by id/path/route, use value for counts, omit value for a dot, and choose color from primary/success/warning/error/info/neutral.',
|
|
308
|
+
'For shell menu notifications, first decide the signal source. Use a count only when the source already owns an exact count, such as a notification summary endpoint or bounded unread-notification query. Use a dot when a realtime event only proves that something new exists. Do not poll a domain list such as messages, tickets, orders, or jobs solely to decorate the menu; the destination page owns domain fetching.',
|
|
308
309
|
'Use useAccountPanelRegistry for account panel rows. AccountPanelItem supports count as the preferred numeric/text badge value, badge as a legacy alias, and badgeColor primary/neutral/info/error/warning/success.',
|
|
309
310
|
'For detail/form workflows that should stay left-aligned with empty space on the right, wrap the body in eapp-page-constrained; use eapp-page-constrained-wide only when the workflow genuinely needs more width.',
|
|
310
311
|
'Card/list grids inside the default shell must account for the 280px desktop sidebar. Do not switch general card grids to three columns at lg; use md:grid-cols-2 xl:grid-cols-3 unless a local container proves three columns have enough width.',
|
|
@@ -416,6 +417,7 @@ function getExtensionThemeContract() {
|
|
|
416
417
|
menu: 'useMenuNotificationRegistry().register({ id, target: { id?, path?, route? }, value?, color?, title?, order? }). value renders a count/chip; omitting value renders a dot. Parent menus sum numeric child values.',
|
|
417
418
|
accountPanel: 'useAccountPanelRegistry().register({ id, label, description, icon, count?, badge?, badgeColor?, expanded?, onToggle?, contentComponent? }). count is preferred over badge and the account trigger sums numeric visible item counts, capped at 99+.',
|
|
418
419
|
lifecycle: 'Register from global extensions for app-wide notification state; stable ids replace previous registrations and component-owned registrations are removed on unmount.',
|
|
420
|
+
reasoning: 'Counts and dots are different promises. A count says the shell knows an exact or bounded number from an appropriate notification/summary source. A dot says the shell only knows that new attention exists. Avoid fetching the destination domain list just to make a menu badge more precise.',
|
|
419
421
|
},
|
|
420
422
|
contractAuthority: [
|
|
421
423
|
'This is the authoritative Enfyra theme & color contract. Source of truth: documents/app/theme-color-contract.md. The app owns color through app/utils/primary-colors.ts (Material You seed-to-role generation), app/assets/css/theme.css (semantic variables and Nuxt UI ramps), app/assets/css/main.css (extension-safe semantic utilities), and app/app.config.ts (Nuxt UI component mapping). Pages and extensions only CONSUME classes/Nuxt UI props; they never define colors.',
|