Fix project isolation: Make loadChatHistory respect active project sessions
- 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>
This commit is contained in:
363
SSE_IMPLEMENTATION_GUIDE.md
Normal file
363
SSE_IMPLEMENTATION_GUIDE.md
Normal file
@@ -0,0 +1,363 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user