- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
364 lines
9.5 KiB
Markdown
364 lines
9.5 KiB
Markdown
# SSE Implementation Guide
|
|
|
|
## Quick Start
|
|
|
|
This guide will help you implement and test the SSE-based session architecture.
|
|
|
|
## Step 1: Integration with server.js
|
|
|
|
Add these lines to your existing `/home/uroma/obsidian-web-interface/server.js`:
|
|
|
|
```javascript
|
|
// After existing imports (around line 12)
|
|
const eventBus = require('./services/event-bus');
|
|
const sseManager = require('./services/sse-manager');
|
|
const sessionRoutes = require('./routes/session-routes');
|
|
const sseRoutes = require('./routes/sse-routes');
|
|
|
|
// After existing middleware (around line 270)
|
|
// Register new API routes
|
|
app.use('/api', sessionRoutes);
|
|
app.use('/api', sseRoutes);
|
|
|
|
// Add monitoring endpoint
|
|
app.get('/api/debug/metrics', (req, res) => {
|
|
res.json({
|
|
eventBus: eventBus.getMetrics(),
|
|
sse: sseManager.getStats(),
|
|
timestamp: Date.now()
|
|
});
|
|
});
|
|
|
|
// Update graceful shutdown (around line 2600)
|
|
const originalCleanup = /* existing cleanup logic or null */;
|
|
|
|
process.on('SIGTERM', async () => {
|
|
console.log('[Server] Starting graceful shutdown...');
|
|
|
|
// Cleanup SSE connections
|
|
sseManager.cleanup();
|
|
|
|
// ... existing cleanup ...
|
|
|
|
process.exit(0);
|
|
});
|
|
```
|
|
|
|
## Step 2: Modify ClaudeService to emit events
|
|
|
|
In `/home/uroma/obsidian-web-interface/services/claude-service.js`:
|
|
|
|
```javascript
|
|
// Add at top of file
|
|
const eventBus = require('./event-bus');
|
|
|
|
// Find the section where Claude output is handled
|
|
// Replace callback-based approach with EventBus emits
|
|
|
|
// Example: When output is received
|
|
handleSessionOutput(sessionId, output) {
|
|
// Old way:
|
|
// this.emit('session-output', { sessionId, output });
|
|
|
|
// New way:
|
|
eventBus.emit('session-output', {
|
|
sessionId,
|
|
type: 'stdout',
|
|
content: output,
|
|
timestamp: Date.now()
|
|
});
|
|
}
|
|
|
|
// Example: When operations are detected
|
|
handleOperationsDetected(sessionId, operations, response) {
|
|
eventBus.emit('operations-detected', {
|
|
sessionId,
|
|
operations,
|
|
response: response.substring(0, 500) + '...'
|
|
});
|
|
}
|
|
```
|
|
|
|
## Step 3: Test SSE Endpoint
|
|
|
|
### Test 1: Basic SSE Connection
|
|
|
|
```bash
|
|
# Terminal 1: Start server
|
|
cd /home/uroma/obsidian-web-interface
|
|
npm start
|
|
|
|
# Terminal 2: Create a test session
|
|
curl -X POST http://localhost:3010/claude/api/claude/sessions \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"workingDir":"/home/uroma"}'
|
|
|
|
# Note the session ID from response
|
|
|
|
# Terminal 3: Test SSE connection (replace SESSION_ID)
|
|
curl -N http://localhost:3010/api/session/SESSION_ID/events
|
|
```
|
|
|
|
### Test 2: SSE with curl (watch events)
|
|
|
|
```bash
|
|
# Connect to SSE and watch for events
|
|
curl -N -H "Accept: text/event-stream" \
|
|
http://localhost:3010/api/session/SESSION_ID/events
|
|
```
|
|
|
|
### Test 3: Send command via REST API
|
|
|
|
```bash
|
|
# Send a command
|
|
curl -X POST http://localhost:3010/api/session/SESSION_ID/prompt \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"command":"ls -la"}'
|
|
|
|
# Watch for output in your SSE connection (Test 2)
|
|
```
|
|
|
|
## Step 4: Browser Testing
|
|
|
|
Create a test HTML file `/home/uroma/obsidian-web-interface/public/test-sse.html`:
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>SSE Test</title>
|
|
<style>
|
|
body { font-family: monospace; padding: 20px; }
|
|
#events { max-height: 500px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; }
|
|
.event { margin: 5px 0; padding: 5px; background: #f0f0f0; }
|
|
.event.error { background: #ffcccc; }
|
|
.event.output { background: #ccffcc; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>SSE Test</h1>
|
|
<div>
|
|
<label>Session ID:</label>
|
|
<input type="text" id="sessionId" placeholder="session-123" size="40">
|
|
<button onclick="connect()">Connect</button>
|
|
<button onclick="disconnect()">Disconnect</button>
|
|
<button onclick="clearEvents()">Clear</button>
|
|
</div>
|
|
<div>
|
|
<label>Command:</label>
|
|
<input type="text" id="command" placeholder="echo hello" size="40">
|
|
<button onclick="sendCommand()">Send</button>
|
|
</div>
|
|
<div id="events"></div>
|
|
|
|
<script src="/js/sse-client.js"></script>
|
|
<script>
|
|
let eventStream = null;
|
|
|
|
function connect() {
|
|
const sessionId = document.getElementById('sessionId').value;
|
|
if (!sessionId) {
|
|
alert('Please enter a session ID');
|
|
return;
|
|
}
|
|
|
|
eventStream = new SessionEventStream(sessionId, {
|
|
reconnectInterval: 2000,
|
|
maxReconnectAttempts: 20
|
|
});
|
|
|
|
eventStream.on('connected', (data) => {
|
|
logEvent('connected', data);
|
|
});
|
|
|
|
eventStream.on('output', (data) => {
|
|
logEvent('output', data);
|
|
});
|
|
|
|
eventStream.on('error', (data) => {
|
|
logEvent('error', data, true);
|
|
});
|
|
|
|
eventStream.on('status', (data) => {
|
|
logEvent('status', data);
|
|
});
|
|
|
|
eventStream.on('reconnecting', (data) => {
|
|
logEvent('reconnecting', data, true);
|
|
});
|
|
|
|
eventStream.on('disconnected', (data) => {
|
|
logEvent('disconnected', data, true);
|
|
});
|
|
}
|
|
|
|
function disconnect() {
|
|
if (eventStream) {
|
|
eventStream.disconnect();
|
|
eventStream = null;
|
|
}
|
|
}
|
|
|
|
async function sendCommand() {
|
|
const sessionId = document.getElementById('sessionId').value;
|
|
const command = document.getElementById('command').value;
|
|
|
|
if (!sessionId || !command) {
|
|
alert('Please enter session ID and command');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/session/${sessionId}/prompt`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ command })
|
|
});
|
|
|
|
const result = await response.json();
|
|
logEvent('command-sent', result);
|
|
} catch (error) {
|
|
logEvent('fetch-error', { error: error.message }, true);
|
|
}
|
|
}
|
|
|
|
function logEvent(type, data, isError = false) {
|
|
const eventsDiv = document.getElementById('events');
|
|
const eventDiv = document.createElement('div');
|
|
eventDiv.className = `event ${isError ? 'error' : type}`;
|
|
eventDiv.textContent = `[${type}] ${JSON.stringify(data, null, 2)}`;
|
|
eventsDiv.appendChild(eventDiv);
|
|
eventsDiv.scrollTop = eventsDiv.scrollHeight;
|
|
}
|
|
|
|
function clearEvents() {
|
|
document.getElementById('events').innerHTML = '';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
Access it at: `http://localhost:3010/test-sse.html`
|
|
|
|
## Step 5: Monitor Metrics
|
|
|
|
```bash
|
|
# Check EventBus and SSE metrics
|
|
curl http://localhost:3010/api/debug/metrics
|
|
|
|
# Expected response:
|
|
{
|
|
"eventBus": {
|
|
"eventsEmitted": 1234,
|
|
"eventsByType": { "session-output": 800, "session-error": 5, ... },
|
|
"listenerCounts": { "session-output-session-123": 1, ... },
|
|
"activeListeners": 10
|
|
},
|
|
"sse": {
|
|
"totalSessions": 3,
|
|
"totalConnections": 5,
|
|
"sessions": { "session-123": 2, "session-456": 1, ... },
|
|
"totalCreated": 50,
|
|
"totalClosed": 45,
|
|
"activeHeartbeats": 5
|
|
},
|
|
"timestamp": 1234567890
|
|
}
|
|
```
|
|
|
|
## Step 6: Test Reconnection
|
|
|
|
1. Start SSE connection in browser
|
|
2. Kill and restart server
|
|
3. Verify automatic reconnection with exponential backoff
|
|
4. Check browser console for reconnection logs
|
|
|
|
## Step 7: Load Testing
|
|
|
|
```javascript
|
|
// In browser console, run:
|
|
|
|
const connections = [];
|
|
for (let i = 0; i < 10; i++) {
|
|
const sessionId = 'session-123'; // Use a real session ID
|
|
const stream = new SessionEventStream(sessionId);
|
|
connections.push(stream);
|
|
}
|
|
|
|
// Check metrics
|
|
fetch('/api/debug/metrics').then(r => r.json()).then(console.log);
|
|
|
|
// Cleanup
|
|
connections.forEach(s => s.disconnect());
|
|
```
|
|
|
|
## Step 8: nginx Configuration
|
|
|
|
If using nginx as reverse proxy, add this configuration:
|
|
|
|
```nginx
|
|
# In your nginx server block
|
|
|
|
location /api/session/ {
|
|
# Disable buffering for SSE
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
|
|
# Pass to backend
|
|
proxy_pass http://localhost:3010;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection '';
|
|
proxy_set_header Cache-Control no-cache;
|
|
proxy_set_header X-Accel-Buffering no;
|
|
|
|
# Increase timeouts for long-lived connections
|
|
proxy_read_timeout 86400s; # 24 hours
|
|
proxy_send_timeout 86400s;
|
|
|
|
# Ensure no buffering
|
|
proxy_buffering off;
|
|
}
|
|
```
|
|
|
|
## Testing Checklist
|
|
|
|
- [ ] SSE connection established successfully
|
|
- [ ] Events received in real-time
|
|
- [ ] Multiple clients can connect to same session
|
|
- [ ] Reconnection works on connection drop
|
|
- [ ] Heartbeat prevents timeout
|
|
- [ ] Metrics endpoint returns correct data
|
|
- [ ] Session validation works (404 for invalid session)
|
|
- [ ] Command sent via REST API produces output via SSE
|
|
- [ ] No memory leaks after extended use
|
|
- [ ] Works through nginx reverse proxy
|
|
|
|
## Troubleshooting
|
|
|
|
### Issue: SSE connection closes immediately
|
|
|
|
**Solution:** Check nginx configuration, ensure `X-Accel-Buffering: no` is set.
|
|
|
|
### Issue: No events received
|
|
|
|
**Solution:** Check session ID is valid and session exists. Check browser console for errors.
|
|
|
|
### Issue: Frequent reconnections
|
|
|
|
**Solution:** Check network stability, increase `heartbeatTimeout` in client options.
|
|
|
|
### Issue: Memory usage increasing
|
|
|
|
**Solution:** Check EventBus listeners are properly unsubscribed on disconnect.
|
|
|
|
## Next Steps
|
|
|
|
After successful testing:
|
|
|
|
1. Update existing UI to use SSE instead of WebSocket
|
|
2. Add feature flag for gradual rollout
|
|
3. Monitor production metrics
|
|
4. Deprecate WebSocket endpoint
|
|
5. Remove legacy code after migration complete
|