Improve SSE status and event auth handling
This commit is contained in:
@@ -9,6 +9,13 @@ import (
|
|||||||
func bearerAuthMiddleware(requiredToken string) func(http.Handler) http.Handler {
|
func bearerAuthMiddleware(requiredToken string) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path == "/api/events" {
|
||||||
|
if r.URL.Query().Get("access_token") == requiredToken {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auth := r.Header.Get("Authorization")
|
auth := r.Header.Get("Authorization")
|
||||||
if !strings.HasPrefix(auth, "Bearer ") {
|
if !strings.HasPrefix(auth, "Bearer ") {
|
||||||
writeError(w, http.StatusUnauthorized, "Missing or invalid Authorization header")
|
writeError(w, http.StatusUnauthorized, "Missing or invalid Authorization header")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/local-mcp/local-mcp-go/internal/events"
|
"github.com/local-mcp/local-mcp-go/internal/events"
|
||||||
)
|
)
|
||||||
@@ -24,6 +25,8 @@ func handleSSE(broker *events.Broker) http.HandlerFunc {
|
|||||||
// Subscribe to event broker
|
// Subscribe to event broker
|
||||||
ch := broker.Subscribe()
|
ch := broker.Subscribe()
|
||||||
defer broker.Unsubscribe(ch)
|
defer broker.Unsubscribe(ch)
|
||||||
|
heartbeat := time.NewTicker(15 * time.Second)
|
||||||
|
defer heartbeat.Stop()
|
||||||
|
|
||||||
// Send initial connection event
|
// Send initial connection event
|
||||||
w.Write([]byte("data: {\"type\":\"connected\"}\n\n"))
|
w.Write([]byte("data: {\"type\":\"connected\"}\n\n"))
|
||||||
@@ -38,6 +41,9 @@ func handleSSE(broker *events.Broker) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
w.Write(msg)
|
w.Write(msg)
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
|
case <-heartbeat.C:
|
||||||
|
w.Write([]byte(": keepalive\n\n"))
|
||||||
|
flusher.Flush()
|
||||||
case <-r.Context().Done():
|
case <-r.Context().Done():
|
||||||
return // client disconnected
|
return // client disconnected
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/local-mcp/local-mcp-go/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handleHealth() http.HandlerFunc {
|
func handleHealth() http.HandlerFunc {
|
||||||
@@ -16,28 +18,58 @@ func handleHealth() http.HandlerFunc {
|
|||||||
|
|
||||||
func handleStatus(stores Stores) http.HandlerFunc {
|
func handleStatus(stores Stores) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
counts, _ := stores.Instructions.Counts()
|
counts, err := stores.Instructions.Counts()
|
||||||
latest, _ := stores.Agents.Latest()
|
if err != nil {
|
||||||
cfg, _ := stores.Settings.Get()
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
resp := map[string]any{
|
cfg, err := stores.Settings.Get()
|
||||||
"uptime_seconds": int(time.Since(serverStartTime).Seconds()),
|
if err != nil {
|
||||||
"queue_pending": counts.PendingCount,
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
"queue_consumed": counts.ConsumedCount,
|
return
|
||||||
"agent_stale_after_seconds": cfg.AgentStaleAfterSeconds,
|
}
|
||||||
|
|
||||||
|
latest, err := stores.Agents.Latest()
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
agent := map[string]any{
|
||||||
|
"connected": false,
|
||||||
|
"last_seen_at": nil,
|
||||||
|
"last_fetch_at": nil,
|
||||||
|
"agent_id": nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
if latest != nil {
|
if latest != nil {
|
||||||
isStale := time.Since(latest.LastSeenAt).Seconds() > float64(cfg.AgentStaleAfterSeconds)
|
connected := time.Since(latest.LastSeenAt).Seconds() <= float64(cfg.AgentStaleAfterSeconds)
|
||||||
resp["agent"] = map[string]any{
|
agent = map[string]any{
|
||||||
"agent_id": latest.AgentID,
|
"connected": connected,
|
||||||
"last_fetch_at": latest.LastFetchAt.Format(time.RFC3339Nano),
|
"last_seen_at": latest.LastSeenAt.Format(time.RFC3339Nano),
|
||||||
"last_result_type": latest.LastResultType,
|
"last_fetch_at": latest.LastFetchAt.Format(time.RFC3339Nano),
|
||||||
"is_stale": isStale,
|
"agent_id": latest.AgentID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp := map[string]any{
|
||||||
|
"server": map[string]any{
|
||||||
|
"status": "up",
|
||||||
|
"started_at": serverStartTime.Format(time.RFC3339Nano),
|
||||||
|
},
|
||||||
|
"agent": agent,
|
||||||
|
"queue": map[string]any{
|
||||||
|
"pending_count": counts.PendingCount,
|
||||||
|
"consumed_count": counts.ConsumedCount,
|
||||||
|
},
|
||||||
|
"settings": models.Settings{
|
||||||
|
DefaultWaitSeconds: cfg.DefaultWaitSeconds,
|
||||||
|
DefaultEmptyResponse: cfg.DefaultEmptyResponse,
|
||||||
|
AgentStaleAfterSeconds: cfg.AgentStaleAfterSeconds,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, resp)
|
writeJSON(w, http.StatusOK, resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { state } from './state.js';
|
import { state } from './state.js';
|
||||||
import { api } from './api.js';
|
import { api, getStoredToken } from './api.js';
|
||||||
|
|
||||||
let _es = null;
|
let _es = null;
|
||||||
let _reconnectTimer = null;
|
let _reconnectTimer = null;
|
||||||
@@ -19,7 +19,12 @@ export function connectSSE() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _connect() {
|
function _connect() {
|
||||||
_es = new EventSource('/api/events');
|
const token = getStoredToken();
|
||||||
|
const url = token
|
||||||
|
? `/api/events?access_token=${encodeURIComponent(token)}`
|
||||||
|
: '/api/events';
|
||||||
|
|
||||||
|
_es = new EventSource(url);
|
||||||
|
|
||||||
_es.onopen = () => {
|
_es.onopen = () => {
|
||||||
console.debug('[SSE] connected');
|
console.debug('[SSE] connected');
|
||||||
@@ -43,7 +48,6 @@ function _connect() {
|
|||||||
|
|
||||||
_es.onerror = () => {
|
_es.onerror = () => {
|
||||||
console.warn('[SSE] connection lost – reconnecting in', RECONNECT_DELAY_MS, 'ms');
|
console.warn('[SSE] connection lost – reconnecting in', RECONNECT_DELAY_MS, 'ms');
|
||||||
state.set('serverOnline', false);
|
|
||||||
_reconnecting = true;
|
_reconnecting = true;
|
||||||
state.set('sseReconnecting', true);
|
state.set('sseReconnecting', true);
|
||||||
_es.close();
|
_es.close();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const _state = {
|
|||||||
status: null, // StatusResponse | null
|
status: null, // StatusResponse | null
|
||||||
config: null, // ConfigResponse | null
|
config: null, // ConfigResponse | null
|
||||||
serverOnline: false,
|
serverOnline: false,
|
||||||
|
sseReconnecting: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _listeners = {}; // key -> Set<fn>
|
const _listeners = {}; // key -> Set<fn>
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ function updateHeaderLeds(serverOnline, status) {
|
|||||||
|
|
||||||
if (!serverLed || !agentLed) return;
|
if (!serverLed || !agentLed) return;
|
||||||
|
|
||||||
// Don't overwrite reconnecting state – events.js sets that
|
if (state.get('sseReconnecting')) {
|
||||||
if (serverOnline && !state.get('sseReconnecting')) {
|
serverLed.className = 'led led--amber led--pulse';
|
||||||
|
serverLed.querySelector('.led__label').textContent = 'Reconnecting…';
|
||||||
|
} else if (serverOnline) {
|
||||||
serverLed.className = 'led led--green led--pulse';
|
serverLed.className = 'led led--green led--pulse';
|
||||||
serverLed.querySelector('.led__label').textContent = 'Server Online';
|
serverLed.querySelector('.led__label').textContent = 'Server Online';
|
||||||
} else if (!serverOnline) {
|
} else if (!serverOnline) {
|
||||||
@@ -58,8 +60,11 @@ function renderStatusPanel(status) {
|
|||||||
const el = document.getElementById('status-panel-body');
|
const el = document.getElementById('status-panel-body');
|
||||||
if (!el || !status) return;
|
if (!el || !status) return;
|
||||||
|
|
||||||
const agent = status.agent;
|
const agent = status.agent ?? {};
|
||||||
const queue = status.queue;
|
const queue = {
|
||||||
|
pending_count: status.queue?.pending_count ?? 0,
|
||||||
|
consumed_count: status.queue?.consumed_count ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="stat-row">
|
<div class="stat-row">
|
||||||
|
|||||||
Reference in New Issue
Block a user