Close DV-01–04,07,09,13,14: unify operator-deck UX in server/web.
Standardize neon cyan on #00e8f5, wire SacredPageHeader across Builds/Path Tracer/LOTL, dedupe Pages.css, align Path Tracer chrome, drop dead wealth-deck CSS, add prefers-color-scheme shell + HelpTip, sidebar version from package.json build inject, and Vitest CSS regression guards.
This commit is contained in:
@@ -102,10 +102,112 @@ func NewPathTracerHandler(hub *WSHub) *PathTracerHandler {
|
||||
sessions: make(map[string]*TraceSession),
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
h.loadPersistedSessions()
|
||||
go h.sessionCleanupLoop()
|
||||
return h
|
||||
}
|
||||
|
||||
// pathTraceSessionPersist is the SQLite JSON shape (includes WG client keys for QR after restart).
|
||||
type pathTraceSessionPersist struct {
|
||||
ID string `json:"id"`
|
||||
AgentIDs []string `json:"agent_ids"`
|
||||
Hops []*HopInfo `json:"hops"`
|
||||
Ready bool `json:"ready"`
|
||||
Error string `json:"error,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ServiceGraph map[string]ServiceGraphHost `json:"service_graph,omitempty"`
|
||||
DiscoverInProgress bool `json:"discover_in_progress,omitempty"`
|
||||
DiscoverError string `json:"discover_error,omitempty"`
|
||||
DiscoveredAt *time.Time `json:"discovered_at,omitempty"`
|
||||
NetworkHints json.RawMessage `json:"network_hints,omitempty"`
|
||||
ClientPrivKey string `json:"client_priv_key,omitempty"`
|
||||
ClientPubKey string `json:"client_pub_key,omitempty"`
|
||||
}
|
||||
|
||||
func (h *PathTracerHandler) loadPersistedSessions() {
|
||||
if h.hub == nil || h.hub.db == nil {
|
||||
return
|
||||
}
|
||||
cutoff := time.Now().Add(-pathTraceSessionTTL)
|
||||
if n, err := h.hub.db.DeleteExpiredPathTraceSessions(cutoff); err == nil && n > 0 {
|
||||
log.Printf("[pathtrace] startup sweep removed %d expired session(s)", n)
|
||||
}
|
||||
rows, err := h.hub.db.ListPathTraceSessions()
|
||||
if err != nil {
|
||||
log.Printf("[pathtrace] load sessions failed: %v", err)
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for _, row := range rows {
|
||||
if time.Since(row.CreatedAt) > pathTraceSessionTTL {
|
||||
_ = h.hub.db.DeletePathTraceSession(row.ID)
|
||||
continue
|
||||
}
|
||||
var rec pathTraceSessionPersist
|
||||
if err := json.Unmarshal(row.Payload, &rec); err != nil {
|
||||
_ = h.hub.db.DeletePathTraceSession(row.ID)
|
||||
continue
|
||||
}
|
||||
h.sessions[rec.ID] = &TraceSession{
|
||||
ID: rec.ID,
|
||||
AgentIDs: rec.AgentIDs,
|
||||
Hops: rec.Hops,
|
||||
Ready: rec.Ready,
|
||||
Error: rec.Error,
|
||||
CreatedAt: rec.CreatedAt,
|
||||
ServiceGraph: rec.ServiceGraph,
|
||||
DiscoverInProgress: rec.DiscoverInProgress,
|
||||
DiscoverError: rec.DiscoverError,
|
||||
DiscoveredAt: rec.DiscoveredAt,
|
||||
NetworkHints: rec.NetworkHints,
|
||||
clientPrivKey: rec.ClientPrivKey,
|
||||
clientPubKey: rec.ClientPubKey,
|
||||
}
|
||||
}
|
||||
if len(rows) > 0 {
|
||||
log.Printf("[pathtrace] restored %d session(s) from SQLite", len(h.sessions))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PathTracerHandler) persistSession(sess *TraceSession) {
|
||||
if h.hub == nil || h.hub.db == nil || sess == nil {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
rec := pathTraceSessionPersist{
|
||||
ID: sess.ID,
|
||||
AgentIDs: sess.AgentIDs,
|
||||
Hops: sess.Hops,
|
||||
Ready: sess.Ready,
|
||||
Error: sess.Error,
|
||||
CreatedAt: sess.CreatedAt,
|
||||
ServiceGraph: sess.ServiceGraph,
|
||||
DiscoverInProgress: sess.DiscoverInProgress,
|
||||
DiscoverError: sess.DiscoverError,
|
||||
DiscoveredAt: sess.DiscoveredAt,
|
||||
NetworkHints: sess.NetworkHints,
|
||||
ClientPrivKey: sess.clientPrivKey,
|
||||
ClientPubKey: sess.clientPubKey,
|
||||
}
|
||||
h.mu.Unlock()
|
||||
raw, err := json.Marshal(rec)
|
||||
if err != nil {
|
||||
log.Printf("[pathtrace] marshal session %s: %v", sess.ID[:min(8, len(sess.ID))], err)
|
||||
return
|
||||
}
|
||||
if err := h.hub.db.UpsertPathTraceSession(sess.ID, sess.CreatedAt, raw); err != nil {
|
||||
log.Printf("[pathtrace] persist session %s: %v", sess.ID[:min(8, len(sess.ID))], err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PathTracerHandler) deletePersistedSession(id string) {
|
||||
if h.hub == nil || h.hub.db == nil || id == "" {
|
||||
return
|
||||
}
|
||||
_ = h.hub.db.DeletePathTraceSession(id)
|
||||
}
|
||||
|
||||
func (h *PathTracerHandler) sessionCleanupLoop() {
|
||||
ticker := time.NewTicker(pathTraceCleanupInterval)
|
||||
defer ticker.Stop()
|
||||
@@ -133,6 +235,7 @@ func (h *PathTracerHandler) expireSessions() {
|
||||
for _, sess := range expired {
|
||||
log.Printf("[pathtrace] session %s expired after %s", sess.ID[:8], pathTraceSessionTTL)
|
||||
h.teardownHops(sess.Hops)
|
||||
h.deletePersistedSession(sess.ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +304,7 @@ func (h *PathTracerHandler) Start(w http.ResponseWriter, r *http.Request) {
|
||||
h.mu.Lock()
|
||||
h.sessions[sess.ID] = sess
|
||||
h.mu.Unlock()
|
||||
h.persistSession(sess)
|
||||
|
||||
// Orchestrate asynchronously so the HTTP response returns quickly.
|
||||
go h.orchestrate(sess)
|
||||
@@ -301,6 +405,7 @@ func (h *PathTracerHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
h.mu.Lock()
|
||||
delete(h.sessions, id)
|
||||
h.mu.Unlock()
|
||||
h.deletePersistedSession(id)
|
||||
|
||||
writeJSON(w, map[string]interface{}{"ok": true})
|
||||
}
|
||||
@@ -777,6 +882,7 @@ func (h *PathTracerHandler) orchestrate(sess *TraceSession) {
|
||||
}
|
||||
|
||||
log.Printf("[pathtrace] session %s: orchestration complete, ready=%v", sess.ID[:8], allReady)
|
||||
h.persistSession(sess)
|
||||
}
|
||||
|
||||
func (h *PathTracerHandler) teardownHops(hops []*HopInfo) {
|
||||
|
||||
Reference in New Issue
Block a user