feat(notifications): aggregate alerts from all nodes in the notification panel

Remote-node alerts now appear in the local notification bell alongside
local ones, each tagged with the originating node name.

- backend: reorder WS upgrade handler so /ws/notifications?nodeId=<remote>
  falls through to the existing proxy path instead of short-circuiting
- frontend/api.ts: add fetchForNode() helper for explicit node-targeted
  requests without touching the localStorage active-node key
- frontend/EditorLayout: fetch notification history from all registered
  nodes in parallel on mount and on node-list changes; open a per-remote
  WebSocket connection for real-time push; route mark-read / delete / clear
  actions back to the originating node; show node-name badge on remote alerts
This commit is contained in:
SaelixCode
2026-03-21 17:38:55 -04:00
parent 30fe77cd5d
commit 1690f0d3c6
4 changed files with 207 additions and 30 deletions
+9 -7
View File
@@ -495,8 +495,15 @@ server.on('upgrade', async (req, socket, head) => {
const parsedUrl = new URL(url, `http://${req.headers.host || 'localhost'}`);
const pathname = parsedUrl.pathname;
// Notification push channel - always local, never proxied to remote nodes
if (pathname === '/ws/notifications') {
// Resolve node context from query param
const nodeIdParam = parsedUrl.searchParams.get('nodeId');
const nodeId = nodeIdParam ? parseInt(nodeIdParam, 10) : NodeRegistry.getInstance().getDefaultNodeId();
const node = NodeRegistry.getInstance().getNode(nodeId);
// Notification push channel - local only when no remote nodeId is specified.
// When a nodeId pointing to a remote node is provided, fall through to the
// proxy block below so the browser subscribes to that remote node's push stream.
if (pathname === '/ws/notifications' && (!node || node.type !== 'remote')) {
const notifWss = new WebSocket.Server({ noServer: true });
notifWss.handleUpgrade(req, socket, head, (ws) => {
notifWss.close();
@@ -507,11 +514,6 @@ server.on('upgrade', async (req, socket, head) => {
return;
}
// Resolve node context from query param
const nodeIdParam = parsedUrl.searchParams.get('nodeId');
const nodeId = nodeIdParam ? parseInt(nodeIdParam, 10) : NodeRegistry.getInstance().getDefaultNodeId();
const node = NodeRegistry.getInstance().getNode(nodeId);
// Remote Node WebSocket Proxy - forward the entire WS connection to the remote Sencho instance
if (node && node.type === 'remote' && node.api_url && node.api_token) {
const wsTarget = node.api_url.replace(/\/$/, '').replace(/^https?/, (m) => m === 'https' ? 'wss' : 'ws');