diff --git a/app/fanout/AGENTS_fanout.md b/app/fanout/AGENTS_fanout.md index 4417864..b816f1c 100644 --- a/app/fanout/AGENTS_fanout.md +++ b/app/fanout/AGENTS_fanout.md @@ -190,7 +190,11 @@ function MyTypeConfigEditor({ } ``` -If your type does NOT have user-configurable scope (like bot or community MQTT), omit the `scope`/`onScopeChange` props and the `ScopeSelector`. The `ScopeSelector` component is defined within the same file — it provides all/none/only/except radio buttons with channel and contact checklists. +If your type does NOT have user-configurable scope (like bot or community MQTT), omit the `scope`/`onScopeChange` props and the `ScopeSelector`. + +The `ScopeSelector` component is defined within the same file. It accepts an optional `showRawPackets` prop: +- **Without `showRawPackets`** (webhook, apprise): shows message scope only (all/only/except — no "none" option since that would make the integration a no-op). A warning appears when the effective selection matches nothing. +- **With `showRawPackets`** (private MQTT): adds a "Forward raw packets" toggle and includes the "No messages" option (valid when raw packets are enabled). The warning appears only when both raw packets and messages are effectively disabled. **c)** Add default config and scope in `handleAddCreate`: ```tsx diff --git a/frontend/src/components/settings/SettingsFanoutSection.tsx b/frontend/src/components/settings/SettingsFanoutSection.tsx index baf1ecc..7809c2a 100644 --- a/frontend/src/components/settings/SettingsFanoutSection.tsx +++ b/frontend/src/components/settings/SettingsFanoutSection.tsx @@ -189,31 +189,7 @@ function MqttPrivateConfigEditor({ -
- - - -
+ ); } @@ -398,9 +374,11 @@ function getFilterKeys(filter: unknown): string[] { function ScopeSelector({ scope, onChange, + showRawPackets = false, }: { scope: Record; onChange: (scope: Record) => void; + showRawPackets?: boolean; }) { const [channels, setChannels] = useState([]); const [contacts, setContacts] = useState([]); @@ -425,7 +403,9 @@ function ScopeSelector({ }, []); const messages = scope.messages ?? 'all'; - const mode = getScopeMode(messages); + const rawMode = getScopeMode(messages); + // When raw packets aren't offered, "none" is not a valid choice — treat as "all" + const mode = !showRawPackets && rawMode === 'none' ? 'all' : rawMode; const isListMode = mode === 'only' || mode === 'except'; const selectedChannels: string[] = @@ -487,6 +467,19 @@ function ScopeSelector({ except: 'All except listed channels/contacts', }; + const rawEnabled = showRawPackets && scope.raw_packets === 'all'; + + // Warn when the effective scope matches nothing + const messagesEffectivelyNone = + mode === 'none' || + (mode === 'only' && selectedChannels.length === 0 && selectedContacts.length === 0) || + (mode === 'except' && + channels.length > 0 && + filteredContacts.length > 0 && + selectedChannels.length >= channels.length && + selectedContacts.length >= filteredContacts.length); + const showEmptyScopeWarning = messagesEffectivelyNone && !rawEnabled; + // For "except" mode, checked means the item is in the exclusion list (will be excluded) const isChannelChecked = (key: string) => mode === 'except' ? selectedChannels.includes(key) : selectedChannels.includes(key); @@ -500,11 +493,28 @@ function ScopeSelector({ const checkboxLabel = mode === 'except' ? 'exclude' : 'include'; + const messageModes: ScopeMode[] = showRawPackets + ? ['all', 'none', 'only', 'except'] + : ['all', 'only', 'except']; + return (
+ + {showRawPackets && ( + + )} +
- {(['all', 'none', 'only', 'except'] as const).map((m) => ( + {messageModes.map((m) => (