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/.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/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...
+
+
+
+
+
+
+
+
+
+
+
+
+
+ All statuses
+ Open
+ Acknowledged
+ In Progress
+ Resolved
+ Won't Do
+
+
+ All categories
+ Capability Gap
+ Needs Human Action
+ Complex Request
+ Sensitive Topic
+ Other
+
+
+
+
+
+
+
+
+
+
+
+
+
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 Channel
+
+ Loading channels...
+
+
Select a channel where admins can see and respond to escalations. View escalations
+
+
+ Save
+
+
+
+
@@ -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 = '-- No channel (disabled) -- ';
+ // Build options for both dropdowns
+ let billingHtml = '-- No channel (disabled) -- ';
+ let escalationHtml = '-- No channel (disabled) -- ';
+
if (slackChannels.length === 0) {
- html = 'No private channels found - invite Addie first ';
- select.innerHTML = html;
- showStatusMessage('Addie is not in any private channels. Invite @Addie to your billing channel and refresh.', 'error');
+ const emptyOption = 'No private channels found - invite Addie first ';
+ 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 += `#${escapeHtml(ch.name)} `;
+ const billingSelected = currentSettings?.billing_channel?.channel_id === ch.id ? 'selected' : '';
+ const escalationSelected = currentSettings?.escalation_channel?.channel_id === ch.id ? 'selected' : '';
+ billingHtml += `#${escapeHtml(ch.name)} `;
+ escalationHtml += `#${escapeHtml(ch.name)} `;
});
- 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 = 'Error loading channels ';
+ billingSelect.innerHTML = 'Error loading channels ';
+ escalationSelect.innerHTML = 'Error loading channels ';
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/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
+-- =============================================================================
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;
}