@enfyra/mcp-server 0.1.3 → 0.1.5
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
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
export const EXAMPLE_REASONING_GUIDE = [
|
|
2
|
+
'Examples are reasoning anchors, not templates to copy blindly. Preserve the platform contract, then adapt table names, route paths, relation names, fields, UI labels, and lifecycle triggers to the live app.',
|
|
3
|
+
'First identify the invariant being demonstrated: security boundary, query shape, shell registry contract, schema relation direction, runtime lifecycle, or browser proxy pattern.',
|
|
4
|
+
'Then identify what is illustrative: chat/order/report/cloud paths, sample field names, icons, labels, menu order, and specific notification kinds.',
|
|
5
|
+
'When a note says do not, treat it as a contract or safety boundary unless live metadata proves a different supported contract. When a note says for example, map the idea to the current domain instead of copying the literal names.',
|
|
6
|
+
'Before applying an example, inspect live metadata/routes/features and choose the closest supported tool. Use the smallest example that proves the decision, then compose with other examples only when the task truly needs multiple contracts.',
|
|
7
|
+
];
|
|
8
|
+
|
|
1
9
|
export const EXAMPLE_CATEGORIES = {
|
|
2
10
|
'ssr-app-auth': {
|
|
3
11
|
title: 'SSR app auth, OAuth, refresh, and proxy setup',
|
|
4
|
-
useWhen: 'Use when building Nuxt, Next, or another browser app that should rely on Enfyra cookies through an app-origin proxy.',
|
|
12
|
+
useWhen: 'Use when building Nuxt, Next, or another browser app that should rely on Enfyra cookies through an app-origin proxy; adapt the framework-specific wrapper while preserving the same-origin proxy and cookie boundary.',
|
|
5
13
|
examples: [
|
|
6
14
|
{
|
|
7
15
|
name: 'Nuxt routeRules for REST and Socket.IO',
|
|
@@ -550,6 +558,7 @@ update_record({
|
|
|
550
558
|
])
|
|
551
559
|
})`,
|
|
552
560
|
notes: [
|
|
561
|
+
'Chat is the illustrative domain here. For another domain, keep the same modeling question: what is the parent entity, what is stored on the parent, and what belongs on child rows?',
|
|
553
562
|
'create_table creates the default route for /chat_conversation.',
|
|
554
563
|
'Keep the latest message as a relation named lastMessage after chat_message exists; do not duplicate last message text/date columns.',
|
|
555
564
|
'Do not create tables just to get custom paths; use create_route for that.',
|
|
@@ -584,6 +593,7 @@ update_record({
|
|
|
584
593
|
])
|
|
585
594
|
})`,
|
|
586
595
|
notes: [
|
|
596
|
+
'The relation names conversation and sender are examples of domain language; choose relation property names that match the entity model users reason about.',
|
|
587
597
|
'Use enfyra_user as the user table.',
|
|
588
598
|
'Use table ids for targetTable when already known; MCP can also resolve exact table names such as "enfyra_user" before schema mutation.',
|
|
589
599
|
'Do not add inverse relations on enfyra_user unless a concrete user-to-record response, UI, or deep query will use it.',
|
|
@@ -606,6 +616,7 @@ update_record({
|
|
|
606
616
|
notes: [
|
|
607
617
|
'Use inversePropertyName only when the parent table will actually expose, deep-load, count, or sort by that child collection.',
|
|
608
618
|
'For example, conversation.messages is justified if a conversation detail response loads the latest message page with deep.messages limit/sort, or if a list sorts by _max(messages.createdAt).',
|
|
619
|
+
'Translate this to the current domain by asking whether the parent screen needs a child collection or aggregate; if not, keep the relation one-directional.',
|
|
609
620
|
'If the app only filters chat_message by conversation.id, omit inversePropertyName and keep the schema one-directional.',
|
|
610
621
|
'Before creating an inverse, inspect existing relations and state why the reverse traversal is needed.',
|
|
611
622
|
],
|
|
@@ -754,6 +765,7 @@ create_column({
|
|
|
754
765
|
'Use a conversation read pre-hook/RLS boundary so the route only returns conversations visible to @USER.',
|
|
755
766
|
'lastMessage is a relation to chat_message; do not duplicate preview fields on chat_conversation.',
|
|
756
767
|
'all: true tells MCP to send REST limit=0 and load all matching conversation rows.',
|
|
768
|
+
'This is a small bounded user inbox example. For larger inventories, prefer pagination even when RLS scopes the records.',
|
|
757
769
|
'Do not fetch messages for every conversation on initial list load; load messages after selecting a conversation.',
|
|
758
770
|
],
|
|
759
771
|
},
|
|
@@ -807,6 +819,7 @@ GET /enfyra/post?filter={"<primaryKeyFromMetadata>":{"_eq":123}}&limit=1`,
|
|
|
807
819
|
notes: [
|
|
808
820
|
'Use fields with dotted relation paths when you only need scalar fields from related records.',
|
|
809
821
|
'This is enough for simple many-to-one or one-to-one relation display such as owner.email, customer.name, or lastMessage.text.',
|
|
822
|
+
'Treat order/customer as placeholders; the transferable idea is "show parent rows with a few scalar relation fields".',
|
|
810
823
|
'Do not add deep when fields alone can express the relation data you need.',
|
|
811
824
|
],
|
|
812
825
|
},
|
|
@@ -894,6 +907,7 @@ query_table({
|
|
|
894
907
|
notes: [
|
|
895
908
|
'Use _max(relation.field) for latest-child ordering, _min(relation.field) for earliest-child ordering, and _count(relation) for child-count ordering.',
|
|
896
909
|
'Aggregate sort helpers only work on direct one-to-many or many-to-many list relations.',
|
|
910
|
+
'Support tickets and messages are illustrative. Apply this when a parent list must be ordered by child recency or child volume.',
|
|
897
911
|
'The aggregate field must be a real published, non-encrypted scalar field on the related table for user-facing APIs.',
|
|
898
912
|
'Do not use _max, _min, or _count on private relations or unpublished fields unless the endpoint intentionally exposes that fact.',
|
|
899
913
|
'Do not use raw sort=-messages.createdAt for parent ordering; it is ambiguous and rejected.',
|
|
@@ -1510,7 +1524,7 @@ return saved`,
|
|
|
1510
1524
|
},
|
|
1511
1525
|
extensions: {
|
|
1512
1526
|
title: 'Dynamic app extensions and menus',
|
|
1513
|
-
useWhen: 'Use when adding custom UI pages
|
|
1527
|
+
useWhen: 'Use when adding custom Enfyra admin UI pages, widgets, global shell integrations, menu entries, account-panel rows, or shell attention signals.',
|
|
1514
1528
|
examples: [
|
|
1515
1529
|
{
|
|
1516
1530
|
name: 'Create or update HTTP method colors',
|
|
@@ -1562,6 +1576,7 @@ ensure_page_extension({
|
|
|
1562
1576
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1563
1577
|
})`,
|
|
1564
1578
|
notes: [
|
|
1579
|
+
'Reports is an illustrative page. Keep the shell/page contracts, but choose the real route, menu label, icon, permissions, and body layout from the operator workflow.',
|
|
1565
1580
|
'Menu provides navigation; extension provides content.',
|
|
1566
1581
|
'Use enfyra_menu.label, not title.',
|
|
1567
1582
|
'Sensitive admin menus should include a permission condition at creation time.',
|
|
@@ -1645,6 +1660,7 @@ ensure_page_extension({
|
|
|
1645
1660
|
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1646
1661
|
})`,
|
|
1647
1662
|
notes: [
|
|
1663
|
+
'This shows composition mechanics. Replace reports/status/table with domain sections that are independently reusable or complex enough to deserve widgets.',
|
|
1648
1664
|
'Use widgets for bulky or reusable sections such as operation panels, timelines, tables, sidebars, and status cards.',
|
|
1649
1665
|
'Embed widgets by their numeric enfyra_extension id, not by extensionId/name.',
|
|
1650
1666
|
'Props and listeners pass through the Widget wrapper. Widget defineProps values update reactively when the parent refs/computed values change.',
|
|
@@ -1705,13 +1721,17 @@ register({
|
|
|
1705
1721
|
|
|
1706
1722
|
const { register: registerMenuNotification, unregister: unregisterMenuNotification } = useMenuNotificationRegistry()
|
|
1707
1723
|
watchEffect(() => {
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1724
|
+
if (notificationBadge.value) {
|
|
1725
|
+
registerMenuNotification({
|
|
1726
|
+
id: 'notifications-menu-unread',
|
|
1727
|
+
target: { path: '/notifications' },
|
|
1728
|
+
value: notificationBadge.value,
|
|
1729
|
+
color: 'error',
|
|
1730
|
+
title: notificationDescription.value,
|
|
1731
|
+
})
|
|
1732
|
+
} else {
|
|
1733
|
+
unregisterMenuNotification('notifications-menu-unread')
|
|
1734
|
+
}
|
|
1715
1735
|
})
|
|
1716
1736
|
|
|
1717
1737
|
const { adminSocket } = useAdminSocket()
|
|
@@ -1736,7 +1756,10 @@ ensure_global_extension({
|
|
|
1736
1756
|
notes: [
|
|
1737
1757
|
'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
1758
|
'Use them for shell-level registrations, realtime listeners, notification counters, account panel rows, and background refresh bridges.',
|
|
1759
|
+
'The notification center is only one possible shell integration. The transferable shape is invisible global extension -> shell registry -> cleanup on unmount.',
|
|
1739
1760
|
'Use useMenuNotificationRegistry for sidebar menu counts/dots when notification state should be visible in the menu as well as the notification center.',
|
|
1761
|
+
'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.',
|
|
1762
|
+
'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
1763
|
'Keep the global extension template empty or hidden; visible UI should be registered into an existing shell registry or component slot.',
|
|
1741
1764
|
'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
1765
|
'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 +1767,160 @@ ensure_global_extension({
|
|
|
1744
1767
|
'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
1768
|
],
|
|
1746
1769
|
},
|
|
1770
|
+
{
|
|
1771
|
+
name: 'Signal menu attention without polling destination lists',
|
|
1772
|
+
code: `const signalBridgeCode = \`
|
|
1773
|
+
<template></template>
|
|
1774
|
+
|
|
1775
|
+
<script setup>
|
|
1776
|
+
const attentionRows = ref([])
|
|
1777
|
+
const notificationSignal = ref(false)
|
|
1778
|
+
const route = useRoute()
|
|
1779
|
+
|
|
1780
|
+
const notificationApi = useApi('/cloud_admin_notifications', {
|
|
1781
|
+
query: {
|
|
1782
|
+
filter: JSON.stringify({ readAt: { _is_null: true } }),
|
|
1783
|
+
fields: 'id,kind,targetPath,readAt',
|
|
1784
|
+
sort: '-createdAt,-id',
|
|
1785
|
+
limit: 10,
|
|
1786
|
+
},
|
|
1787
|
+
})
|
|
1788
|
+
|
|
1789
|
+
const hasNewEmail = computed(() =>
|
|
1790
|
+
attentionRows.value.some((row) => row.kind === 'email_inbound' && !row.readAt)
|
|
1791
|
+
)
|
|
1792
|
+
const hasNewSupport = computed(() =>
|
|
1793
|
+
attentionRows.value.some((row) => row.kind === 'support' && !row.readAt)
|
|
1794
|
+
)
|
|
1795
|
+
const accountBadge = computed(() => notificationSignal.value ? 'New' : null)
|
|
1796
|
+
const accountDescription = computed(() => notificationSignal.value ? 'New admin attention' : 'All caught up')
|
|
1797
|
+
|
|
1798
|
+
function syncFromNotificationRows() {
|
|
1799
|
+
const value = notificationApi.data?.value
|
|
1800
|
+
const rows = Array.isArray(value?.data)
|
|
1801
|
+
? value.data
|
|
1802
|
+
: Array.isArray(value?.data?.data)
|
|
1803
|
+
? value.data.data
|
|
1804
|
+
: []
|
|
1805
|
+
attentionRows.value = rows
|
|
1806
|
+
notificationSignal.value = rows.some((row) => !row.readAt)
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
async function refreshNotificationSignals() {
|
|
1810
|
+
await notificationApi.execute()
|
|
1811
|
+
syncFromNotificationRows()
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const { register: registerAccountPanel } = useAccountPanelRegistry()
|
|
1815
|
+
registerAccountPanel({
|
|
1816
|
+
id: 'admin-attention',
|
|
1817
|
+
order: 20,
|
|
1818
|
+
label: 'Notifications',
|
|
1819
|
+
icon: computed(() => notificationSignal.value ? 'lucide:bell-ring' : 'lucide:bell'),
|
|
1820
|
+
description: accountDescription,
|
|
1821
|
+
count: accountBadge,
|
|
1822
|
+
badgeColor: 'info',
|
|
1823
|
+
onClick: () => navigateTo('/data/cloud_admin_notifications'),
|
|
1824
|
+
})
|
|
1825
|
+
|
|
1826
|
+
const { register: registerMenuNotification, unregister: unregisterMenuNotification } = useMenuNotificationRegistry()
|
|
1827
|
+
watchEffect(() => {
|
|
1828
|
+
if (hasNewEmail.value) {
|
|
1829
|
+
registerMenuNotification({
|
|
1830
|
+
id: 'attention-email',
|
|
1831
|
+
target: { path: '/email/messages' },
|
|
1832
|
+
color: 'info',
|
|
1833
|
+
title: 'New inbound email',
|
|
1834
|
+
})
|
|
1835
|
+
} else {
|
|
1836
|
+
unregisterMenuNotification('attention-email')
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
if (hasNewSupport.value) {
|
|
1840
|
+
registerMenuNotification({
|
|
1841
|
+
id: 'attention-support',
|
|
1842
|
+
target: { path: '/cloud/support' },
|
|
1843
|
+
color: 'info',
|
|
1844
|
+
title: 'New support activity',
|
|
1845
|
+
})
|
|
1846
|
+
} else {
|
|
1847
|
+
unregisterMenuNotification('attention-support')
|
|
1848
|
+
}
|
|
1849
|
+
})
|
|
1850
|
+
|
|
1851
|
+
watch(() => route.path, (path) => {
|
|
1852
|
+
if (path.startsWith('/email/messages')) {
|
|
1853
|
+
attentionRows.value = attentionRows.value.filter((row) => row.kind !== 'email_inbound')
|
|
1854
|
+
}
|
|
1855
|
+
if (path.startsWith('/cloud/support')) {
|
|
1856
|
+
attentionRows.value = attentionRows.value.filter((row) => row.kind !== 'support')
|
|
1857
|
+
}
|
|
1858
|
+
notificationSignal.value = attentionRows.value.some((row) => !row.readAt)
|
|
1859
|
+
})
|
|
1860
|
+
|
|
1861
|
+
const { adminSocket } = useAdminSocket()
|
|
1862
|
+
function handleAdminNotification(payload) {
|
|
1863
|
+
refreshNotificationSignals()
|
|
1864
|
+
if (payload?.kind === 'email_inbound') {
|
|
1865
|
+
registerMenuNotification({ id: 'attention-email', target: { path: '/email/messages' }, color: 'info', title: 'New inbound email' })
|
|
1866
|
+
}
|
|
1867
|
+
if (payload?.kind === 'support') {
|
|
1868
|
+
registerMenuNotification({ id: 'attention-support', target: { path: '/cloud/support' }, color: 'info', title: 'New support activity' })
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
function getAdminSocket() {
|
|
1873
|
+
return adminSocket && adminSocket.value !== undefined ? adminSocket.value : adminSocket
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function bindAdminSocket(socket) {
|
|
1877
|
+
if (socket && typeof socket.on === 'function') {
|
|
1878
|
+
socket.on('admin:notification-created', handleAdminNotification)
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function unbindAdminSocket(socket) {
|
|
1883
|
+
if (socket && typeof socket.off === 'function') {
|
|
1884
|
+
socket.off('admin:notification-created', handleAdminNotification)
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
if (adminSocket && adminSocket.value !== undefined) {
|
|
1889
|
+
watch(adminSocket, (nextSocket, previousSocket) => {
|
|
1890
|
+
unbindAdminSocket(previousSocket)
|
|
1891
|
+
bindAdminSocket(nextSocket)
|
|
1892
|
+
})
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
onMounted(() => {
|
|
1896
|
+
refreshNotificationSignals()
|
|
1897
|
+
bindAdminSocket(getAdminSocket())
|
|
1898
|
+
})
|
|
1899
|
+
onUnmounted(() => {
|
|
1900
|
+
unbindAdminSocket(getAdminSocket())
|
|
1901
|
+
unregisterMenuNotification('attention-email')
|
|
1902
|
+
unregisterMenuNotification('attention-support')
|
|
1903
|
+
})
|
|
1904
|
+
</script>
|
|
1905
|
+
\`
|
|
1906
|
+
|
|
1907
|
+
ensure_global_extension({
|
|
1908
|
+
name: "AdminAttentionSignalBridge",
|
|
1909
|
+
description: "Routes notification signals into account-panel and sidebar menu attention markers without polling destination lists",
|
|
1910
|
+
code: signalBridgeCode,
|
|
1911
|
+
isEnabled: true,
|
|
1912
|
+
extensionKnowledgeAckKey: "<extensionAckKey from get_enfyra_required_knowledge>"
|
|
1913
|
+
})`,
|
|
1914
|
+
notes: [
|
|
1915
|
+
'Use this reasoning pattern when the shell should show attention but the destination page owns the expensive or domain-specific list fetch.',
|
|
1916
|
+
'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.',
|
|
1917
|
+
'Omitting value on registerMenuNotification renders a dot. That is the right promise when the shell knows "new work exists" but not an exact count.',
|
|
1918
|
+
'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.',
|
|
1919
|
+
'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.',
|
|
1920
|
+
'Generalize the lifecycle: seed from a bounded signal source, react to realtime events, clear local attention when the user reaches the owning page, and avoid duplicating that page\'s data fetch.',
|
|
1921
|
+
'Clear local dot signals when the user enters the destination route or when the notification center marks the underlying notification as read.',
|
|
1922
|
+
],
|
|
1923
|
+
},
|
|
1747
1924
|
{
|
|
1748
1925
|
name: 'Register a data-driven account-panel item',
|
|
1749
1926
|
code: `<script setup>
|
|
@@ -1780,6 +1957,7 @@ register({
|
|
|
1780
1957
|
</script>`,
|
|
1781
1958
|
notes: [
|
|
1782
1959
|
'Prefer this contract for shell/account-panel items: data fields for the row, optional contentComponent for the expanded body.',
|
|
1960
|
+
'Notifications is illustrative. Account-panel rows can represent any account-scoped attention or shortcut, such as approvals, billing, deployments, or personal tasks.',
|
|
1783
1961
|
'Use count for the primary visible badge value. badge remains supported as a legacy alias, but count is what the account trigger aggregates.',
|
|
1784
1962
|
'Do not draw a custom full row with page-scale cards, hero headings, large whitespace, or nested buttons unless the shell contract cannot express the UI.',
|
|
1785
1963
|
'Let the Enfyra admin UI handle the row button, icon container, label, microcopy, badge, chevron, hover state, spacing, and expanded wrapper.',
|
|
@@ -1834,6 +2012,7 @@ registerHeaderActions([
|
|
|
1834
2012
|
</script>`,
|
|
1835
2013
|
notes: [
|
|
1836
2014
|
'Use PageHeader for the title strip; do not render a duplicate header inside extension body.',
|
|
2015
|
+
'The exact actions are illustrative. Choose action prominence from user intent: navigation, secondary utility, primary mutation, or destructive confirmation.',
|
|
1837
2016
|
'Use gradient: "none" for generated operational pages; hardcoded named gradients are decorative and should be explicit user intent.',
|
|
1838
2017
|
'Back/navigation actions should be neutral ghost so they read as navigation, not a primary operation.',
|
|
1839
2018
|
'Visible secondary operations should be neutral outline; soft is only for low-emphasis chrome actions.',
|
|
@@ -1873,7 +2052,7 @@ registerHeaderActions([
|
|
|
1873
2052
|
},
|
|
1874
2053
|
{
|
|
1875
2054
|
name: 'Plan an admin dashboard as multiple pages',
|
|
1876
|
-
code: `//
|
|
2055
|
+
code: `// Illustrative menu shape for an operations surface:
|
|
1877
2056
|
ensure_menu({
|
|
1878
2057
|
type: "Dropdown Menu",
|
|
1879
2058
|
label: "Operations",
|
|
@@ -1899,6 +2078,7 @@ ensure_menu({
|
|
|
1899
2078
|
// For admin record management, link to /data/<table>, e.g. /data/report, not public website paths.`,
|
|
1900
2079
|
notes: [
|
|
1901
2080
|
'Design the menu/page split before generating dashboard code.',
|
|
2081
|
+
'Operations/jobs/orders/reports/settings are examples of separating mental models. Replace them with the real domains users navigate between.',
|
|
1902
2082
|
'Permission-gate sensitive parent dropdown menus too, using any child page route or backing route that represents read access.',
|
|
1903
2083
|
'Keep /dashboard as a summary and distribution page, not a detailed operations table.',
|
|
1904
2084
|
'Use focused pages for operational domains.',
|
|
@@ -1930,6 +2110,7 @@ onMounted(() => fetchOrders())
|
|
|
1930
2110
|
notes: [
|
|
1931
2111
|
'Use app-provided composables in extensions.',
|
|
1932
2112
|
'useApi does not auto-run; call execute() on mounted or through an action.',
|
|
2113
|
+
'The /order path is illustrative; inspect routes and fetch the smallest data shape the extension needs.',
|
|
1933
2114
|
'Keep extension UI focused; move backend logic into handlers/hooks when needed.',
|
|
1934
2115
|
],
|
|
1935
2116
|
},
|
|
@@ -2056,6 +2237,7 @@ onMounted(() => Promise.all([flowStats.execute(), orderStats.execute()]))
|
|
|
2056
2237
|
'Aggregate keys must be real fields or relations.',
|
|
2057
2238
|
'Read results from response.meta.aggregate.',
|
|
2058
2239
|
'Use top-level filter for time windows and cross-field conditions.',
|
|
2240
|
+
'The flow/order pair is illustrative. Choose aggregates that answer the page question, such as failed work, pending approvals, unread support, quota pressure, or revenue.',
|
|
2059
2241
|
'Only aggregate fields and relations that the dashboard is allowed to expose; aggregate values can reveal hidden data even when rows omit that field.',
|
|
2060
2242
|
'sum/avg require numeric fields; amount_usd must be a real float/numeric SQL column, not metadata-only float over a varchar physical column.',
|
|
2061
2243
|
],
|
|
@@ -2075,6 +2257,7 @@ export function listExampleCategories() {
|
|
|
2075
2257
|
export function getExamples(category) {
|
|
2076
2258
|
if (!category) {
|
|
2077
2259
|
return {
|
|
2260
|
+
reasoningGuide: EXAMPLE_REASONING_GUIDE,
|
|
2078
2261
|
categories: listExampleCategories(),
|
|
2079
2262
|
hint: 'Call get_enfyra_examples with one category key to retrieve concrete examples for that area.',
|
|
2080
2263
|
};
|
|
@@ -2090,6 +2273,7 @@ export function getExamples(category) {
|
|
|
2090
2273
|
|
|
2091
2274
|
return {
|
|
2092
2275
|
category,
|
|
2276
|
+
reasoningGuide: EXAMPLE_REASONING_GUIDE,
|
|
2093
2277
|
...entry,
|
|
2094
2278
|
};
|
|
2095
2279
|
}
|
|
@@ -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.',
|