From f6d3d03c56e3c321ece9d21732cc9e00ec3aef65 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Wed, 14 Jan 2026 20:07:43 -0500 Subject: [PATCH 1/2] fix: remove RSS content from Editorial working group Migration 153 incorrectly swept ALL perspectives with NULL working_group_id into the Editorial working group, including RSS feed articles. This migration sets working_group_id = NULL for all rss/email sourced content, so it displays via The Latest sections through addie_knowledge instead of on working group pages. Co-Authored-By: Claude Opus 4.5 --- .changeset/legal-islands-kick.md | 8 ++++++++ .../169_remove_rss_from_editorial.sql | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 .changeset/legal-islands-kick.md create mode 100644 server/src/db/migrations/169_remove_rss_from_editorial.sql diff --git a/.changeset/legal-islands-kick.md b/.changeset/legal-islands-kick.md new file mode 100644 index 0000000000..10b4399a09 --- /dev/null +++ b/.changeset/legal-islands-kick.md @@ -0,0 +1,8 @@ +--- +--- + +fix: Remove RSS and email content from Editorial working group + +Migration 153 incorrectly swept RSS feed articles into the Editorial working group. +RSS content should remain unassigned (working_group_id = NULL) and display +via The Latest sections through addie_knowledge. diff --git a/server/src/db/migrations/169_remove_rss_from_editorial.sql b/server/src/db/migrations/169_remove_rss_from_editorial.sql new file mode 100644 index 0000000000..2461242d01 --- /dev/null +++ b/server/src/db/migrations/169_remove_rss_from_editorial.sql @@ -0,0 +1,20 @@ +-- Migration: 169_remove_rss_from_editorial.sql +-- Fix: Remove RSS-sourced content from Editorial working group +-- +-- Migration 153 incorrectly swept ALL perspectives with NULL working_group_id +-- into the Editorial working group, including RSS feed articles. +-- RSS content should remain unassigned (working_group_id = NULL) and display +-- via The Latest sections through addie_knowledge, not on working group pages. + +-- ============================================================================= +-- Remove RSS and email content from any working group +-- ============================================================================= + +UPDATE perspectives +SET working_group_id = NULL +WHERE source_type IN ('rss', 'email') + AND working_group_id IS NOT NULL; + +-- ============================================================================= +-- Done +-- ============================================================================= From 1392b70f9b97f721898c6e6376e244c0f7057620 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 15 Jan 2026 05:56:37 -0500 Subject: [PATCH 2/2] feat: add admin escalations page and configurable notification channel - Add /admin/escalations page to view and manage Addie's escalations - Add escalation channel setting to system settings page - Update escalation tools to use configured channel from settings - Add Escalations link to admin sidebar - Add private channel validation for escalation channel Co-Authored-By: Claude Opus 4.5 --- .changeset/frank-donkeys-dress.md | 2 + server/public/admin-escalations.html | 430 +++++++++++++++++++++++ server/public/admin-settings.html | 123 ++++++- server/public/admin-sidebar.js | 1 + server/src/addie/mcp/escalation-tools.ts | 12 +- server/src/db/system-settings-db.ts | 31 ++ server/src/http.ts | 4 + server/src/routes/admin/settings.ts | 63 ++++ 8 files changed, 647 insertions(+), 19 deletions(-) create mode 100644 .changeset/frank-donkeys-dress.md create mode 100644 server/public/admin-escalations.html diff --git a/.changeset/frank-donkeys-dress.md b/.changeset/frank-donkeys-dress.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/frank-donkeys-dress.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/server/public/admin-escalations.html b/server/public/admin-escalations.html new file mode 100644 index 0000000000..104e17a907 --- /dev/null +++ b/server/public/admin-escalations.html @@ -0,0 +1,430 @@ + + + + + + + Admin - Escalations - AgenticAdvertising.org + + + + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/server/public/admin-settings.html b/server/public/admin-settings.html index d7b9e6ce67..e639b4a6e4 100644 --- a/server/public/admin-settings.html +++ b/server/public/admin-settings.html @@ -192,6 +192,32 @@

Billing Notifications Channel

+ +
+

Escalation Notifications Channel

+

+ Choose which Slack channel receives escalation notifications when Addie cannot fulfill a request. + This allows admins to be notified when users need human assistance. +

+ +
+ Loading... +
+ +
+
+ + + Select a channel where admins can see and respond to escalations. View escalations +
+ +
+
+ @@ -214,6 +240,7 @@

Billing Notifications Channel

currentSettings = await response.json(); updateCurrentChannelDisplay(); + updateCurrentEscalationChannelDisplay(); document.getElementById('loading').style.display = 'none'; document.getElementById('content').style.display = 'block'; @@ -229,7 +256,8 @@

Billing Notifications Channel

// Load Slack channels for picker async function loadSlackChannels() { - const select = document.getElementById('billingChannel'); + const billingSelect = document.getElementById('billingChannel'); + const escalationSelect = document.getElementById('escalationChannel'); try { const response = await fetch('/api/admin/settings/slack-channels'); @@ -241,24 +269,34 @@

Billing Notifications Channel

const data = await response.json(); slackChannels = data.channels; - // Build options - let html = ''; + // Build options for both dropdowns + let billingHtml = ''; + let escalationHtml = ''; + if (slackChannels.length === 0) { - html = ''; - select.innerHTML = html; - showStatusMessage('Addie is not in any private channels. Invite @Addie to your billing channel and refresh.', 'error'); + const emptyOption = ''; + billingSelect.innerHTML = emptyOption; + escalationSelect.innerHTML = emptyOption; + showStatusMessage('Addie is not in any private channels. Invite @Addie to your channels and refresh.', 'error'); } else { slackChannels.forEach(ch => { - const selected = currentSettings?.billing_channel?.channel_id === ch.id ? 'selected' : ''; - html += ``; + const billingSelected = currentSettings?.billing_channel?.channel_id === ch.id ? 'selected' : ''; + const escalationSelected = currentSettings?.escalation_channel?.channel_id === ch.id ? 'selected' : ''; + billingHtml += ``; + escalationHtml += ``; }); - select.innerHTML = html; - select.disabled = false; + billingSelect.innerHTML = billingHtml; + billingSelect.disabled = false; document.getElementById('saveBtn').disabled = false; + + escalationSelect.innerHTML = escalationHtml; + escalationSelect.disabled = false; + document.getElementById('saveEscalationBtn').disabled = false; } } catch (error) { console.error('Error loading Slack channels:', error); - select.innerHTML = ''; + billingSelect.innerHTML = ''; + escalationSelect.innerHTML = ''; showStatusMessage('Failed to load Slack channels: ' + error.message, 'error'); } } @@ -277,6 +315,20 @@

Billing Notifications Channel

} } + // Update the current escalation channel display + function updateCurrentEscalationChannelDisplay() { + const el = document.getElementById('currentEscalationChannel'); + const channel = currentSettings?.escalation_channel; + + if (channel?.channel_id && channel?.channel_name) { + el.className = 'current-value'; + el.innerHTML = `Current: #${escapeHtml(channel.channel_name)}`; + } else { + el.className = 'current-value not-set'; + el.innerHTML = 'Current: Not configured (escalation notifications disabled)'; + } + } + // Save billing channel async function saveBillingChannel() { const select = document.getElementById('billingChannel'); @@ -326,6 +378,55 @@

Billing Notifications Channel

} } + // Save escalation channel + async function saveEscalationChannel() { + const select = document.getElementById('escalationChannel'); + const saveBtn = document.getElementById('saveEscalationBtn'); + const channelId = select.value || null; + + // Find channel name + let channelName = null; + if (channelId) { + const channel = slackChannels.find(c => c.id === channelId); + channelName = channel?.name || null; + } + + saveBtn.disabled = true; + saveBtn.textContent = 'Saving...'; + + try { + const response = await fetch('/api/admin/settings/escalation-channel', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ channel_id: channelId, channel_name: channelName }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.message || 'Failed to save'); + } + + const data = await response.json(); + currentSettings.escalation_channel = data.escalation_channel; + updateCurrentEscalationChannelDisplay(); + + saveBtn.textContent = 'Saved!'; + saveBtn.classList.add('btn-success'); + showStatusMessage('Escalation channel updated successfully', 'success'); + + setTimeout(() => { + saveBtn.textContent = 'Save'; + saveBtn.classList.remove('btn-success'); + saveBtn.disabled = false; + }, 2000); + } catch (error) { + console.error('Error saving escalation channel:', error); + showStatusMessage('Failed to save: ' + error.message, 'error'); + saveBtn.textContent = 'Save'; + saveBtn.disabled = false; + } + } + // Status message helpers function showStatusMessage(message, type) { const el = document.getElementById('statusMessage'); diff --git a/server/public/admin-sidebar.js b/server/public/admin-sidebar.js index 3dc75dce5c..9f650fe630 100644 --- a/server/public/admin-sidebar.js +++ b/server/public/admin-sidebar.js @@ -46,6 +46,7 @@ { href: '/admin/agreements', label: 'Agreements', icon: '📋' }, { href: '/admin/email', label: 'Email', icon: '📧' }, { href: '/admin/addie', label: 'Addie', icon: '🤖' }, + { href: '/admin/escalations', label: 'Escalations', icon: '🚨' }, { href: '/admin/feeds', label: 'Industry Feeds', icon: '📰' }, { href: '/admin/notification-channels', label: 'Alert Channels', icon: '📢' }, { href: '/admin/api-keys', label: 'API Keys', icon: '🔑' }, diff --git a/server/src/addie/mcp/escalation-tools.ts b/server/src/addie/mcp/escalation-tools.ts index d27b5343b3..abb1518661 100644 --- a/server/src/addie/mcp/escalation-tools.ts +++ b/server/src/addie/mcp/escalation-tools.ts @@ -17,7 +17,7 @@ import { } from '../../db/escalation-db.js'; import { getThreadService } from '../thread-service.js'; import { sendChannelMessage } from '../../slack/client.js'; -import { getActiveChannels } from '../../db/notification-channels-db.js'; +import { getEscalationChannel } from '../../db/system-settings-db.js'; import { AddieDatabase } from '../../db/addie-db.js'; const logger = createLogger('addie-escalation-tools'); @@ -112,15 +112,11 @@ DO NOT use for: ]; /** - * Find the escalation notification channel + * Get the configured escalation notification channel from system settings */ async function getEscalationChannelId(): Promise { - const channels = await getActiveChannels(); - // Look for a channel named "Addie Escalations" or similar - const escalationChannel = channels.find( - (c) => c.name.toLowerCase().includes('escalation') || c.name.toLowerCase().includes('addie-admin') - ); - return escalationChannel?.slack_channel_id || null; + const setting = await getEscalationChannel(); + return setting.channel_id; } /** diff --git a/server/src/db/system-settings-db.ts b/server/src/db/system-settings-db.ts index d63af1a3c7..1a0521da2f 100644 --- a/server/src/db/system-settings-db.ts +++ b/server/src/db/system-settings-db.ts @@ -20,10 +20,16 @@ export interface BillingChannelSetting { channel_name: string | null; } +export interface EscalationChannelSetting { + channel_id: string | null; + channel_name: string | null; +} + // ============== Setting Keys ============== export const SETTING_KEYS = { BILLING_SLACK_CHANNEL: 'billing_slack_channel', + ESCALATION_SLACK_CHANNEL: 'escalation_slack_channel', } as const; // ============== Generic Operations ============== @@ -90,3 +96,28 @@ export async function setBillingChannel( updatedBy ); } + +// ============== Escalation Channel Operations ============== + +/** + * Get the configured escalation notification Slack channel + */ +export async function getEscalationChannel(): Promise { + const result = await getSetting(SETTING_KEYS.ESCALATION_SLACK_CHANNEL); + return result ?? { channel_id: null, channel_name: null }; +} + +/** + * Set the escalation notification Slack channel + */ +export async function setEscalationChannel( + channelId: string | null, + channelName: string | null, + updatedBy?: string +): Promise { + await setSetting( + SETTING_KEYS.ESCALATION_SLACK_CHANNEL, + { channel_id: channelId, channel_name: channelName }, + updatedBy + ); +} diff --git a/server/src/http.ts b/server/src/http.ts index 417fc390f0..f3369e5d38 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -3527,6 +3527,10 @@ Disallow: /api/admin/ await this.serveHtmlWithConfig(req, res, 'admin-settings.html'); }); + this.app.get('/admin/escalations', requireAuth, requireAdmin, async (req, res) => { + await this.serveHtmlWithConfig(req, res, 'admin-escalations.html'); + }); + // Registry API endpoints (consolidated agents, publishers, lookups) this.setupRegistryRoutes(); } diff --git a/server/src/routes/admin/settings.ts b/server/src/routes/admin/settings.ts index 08a8ec539b..ac95e81e47 100644 --- a/server/src/routes/admin/settings.ts +++ b/server/src/routes/admin/settings.ts @@ -13,6 +13,8 @@ import { getAllSettings, getBillingChannel, setBillingChannel, + getEscalationChannel, + setEscalationChannel, } from '../../db/system-settings-db.js'; import { getSlackChannels, getChannelInfo, isSlackConfigured } from '../../slack/client.js'; @@ -26,10 +28,12 @@ export function createAdminSettingsRouter(): Router { try { const settings = await getAllSettings(); const billingChannel = await getBillingChannel(); + const escalationChannel = await getEscalationChannel(); res.json({ settings, billing_channel: billingChannel, + escalation_channel: escalationChannel, }); } catch (error) { logger.error({ err: error }, 'Failed to get system settings'); @@ -136,5 +140,64 @@ export function createAdminSettingsRouter(): Router { } }); + // PUT /api/admin/settings/escalation-channel - Update escalation notification channel + router.put('/escalation-channel', requireAuth, requireAdmin, async (req: Request, res: Response) => { + try { + const { channel_id, channel_name } = req.body; + + // Allow null to clear the channel + if (channel_id !== null && channel_id !== undefined) { + // Validate channel ID format + if (typeof channel_id !== 'string' || !/^[CG][A-Z0-9]+$/.test(channel_id)) { + res.status(400).json({ + error: 'Invalid channel ID format', + message: 'Channel ID should start with C or G followed by alphanumeric characters', + }); + return; + } + + // Verify the channel is private (escalation info may contain sensitive user data) + if (isSlackConfigured()) { + const channelInfo = await getChannelInfo(channel_id); + if (channelInfo && !channelInfo.is_private) { + res.status(400).json({ + error: 'Invalid channel', + message: 'Only private channels are allowed for escalation notifications', + }); + return; + } + } + } + + // Validate channel name if provided + if (channel_name !== null && channel_name !== undefined) { + if (typeof channel_name !== 'string' || channel_name.length > 200) { + res.status(400).json({ + error: 'Invalid channel name', + message: 'Channel name must be a string under 200 characters', + }); + return; + } + } + + const userId = req.user?.id; + await setEscalationChannel(channel_id ?? null, channel_name ?? null, userId); + + logger.info({ channel_id, channel_name, userId }, 'Escalation channel updated'); + + const updated = await getEscalationChannel(); + res.json({ + success: true, + escalation_channel: updated, + }); + } catch (error) { + logger.error({ err: error }, 'Failed to update escalation channel'); + res.status(500).json({ + error: 'Failed to update escalation channel', + message: error instanceof Error ? error.message : 'Unknown error', + }); + } + }); + return router; }