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:
73
server/internal/db/pathtrace_sessions.go
Normal file
73
server/internal/db/pathtrace_sessions.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PathTraceSessionRow is one persisted Path Tracer session blob.
|
||||
type PathTraceSessionRow struct {
|
||||
ID string
|
||||
CreatedAt time.Time
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
// UpsertPathTraceSession stores or replaces a session JSON blob.
|
||||
func (d *Database) UpsertPathTraceSession(id string, createdAt time.Time, payload []byte) error {
|
||||
_, err := d.Exec(
|
||||
`INSERT INTO pathtrace_sessions (id, created_at, payload)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET created_at = excluded.created_at, payload = excluded.payload`,
|
||||
id, createdAt.UTC().Format(time.RFC3339Nano), payload,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListPathTraceSessions returns all session rows ordered by age.
|
||||
func (d *Database) ListPathTraceSessions() ([]PathTraceSessionRow, error) {
|
||||
rows, err := d.Query(`SELECT id, created_at, payload FROM pathtrace_sessions ORDER BY created_at ASC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []PathTraceSessionRow
|
||||
for rows.Next() {
|
||||
var row PathTraceSessionRow
|
||||
var created string
|
||||
if err := rows.Scan(&row.ID, &created, &row.Payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row.CreatedAt, _ = time.Parse(time.RFC3339Nano, created)
|
||||
if row.CreatedAt.IsZero() {
|
||||
row.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", created)
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// DeletePathTraceSession removes one session row.
|
||||
func (d *Database) DeletePathTraceSession(id string) error {
|
||||
_, err := d.Exec(`DELETE FROM pathtrace_sessions WHERE id = ?`, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteExpiredPathTraceSessions removes rows older than cutoff.
|
||||
func (d *Database) DeleteExpiredPathTraceSessions(cutoff time.Time) (int64, error) {
|
||||
res, err := d.Exec(`DELETE FROM pathtrace_sessions WHERE created_at < ?`, cutoff.UTC().Format(time.RFC3339Nano))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
|
||||
// PathTraceSessionCount returns how many session rows exist.
|
||||
func (d *Database) PathTraceSessionCount() (int, error) {
|
||||
var n int
|
||||
err := d.QueryRow(`SELECT COUNT(*) FROM pathtrace_sessions`).Scan(&n)
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
36
server/internal/db/pathtrace_sessions_test.go
Normal file
36
server/internal/db/pathtrace_sessions_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPathTraceSessionPersistenceAndSweep(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
|
||||
fresh := []byte(`{"id":"sess-fresh","ready":true}`)
|
||||
stale := []byte(`{"id":"sess-stale","ready":false}`)
|
||||
|
||||
if err := d.UpsertPathTraceSession("sess-fresh", time.Now().Add(-30*time.Minute), fresh); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := d.UpsertPathTraceSession("sess-stale", time.Now().Add(-3*time.Hour), stale); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
deleted, err := d.DeleteExpiredPathTraceSessions(time.Now().Add(-2 * time.Hour))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if deleted != 1 {
|
||||
t.Fatalf("expected 1 stale row deleted, got %d", deleted)
|
||||
}
|
||||
|
||||
rows, err := d.ListPathTraceSessions()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rows) != 1 || rows[0].ID != "sess-fresh" {
|
||||
t.Fatalf("expected fresh session only, got %+v", rows)
|
||||
}
|
||||
}
|
||||
@@ -241,6 +241,12 @@ func (d *Database) migrate() error {
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_fleet_phenotypes_fingerprint ON fleet_phenotypes(fingerprint)`,
|
||||
`CREATE TABLE IF NOT EXISTS pathtrace_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at DATETIME NOT NULL,
|
||||
payload TEXT NOT NULL
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_pathtrace_sessions_created ON pathtrace_sessions(created_at)`,
|
||||
}
|
||||
for _, m := range extraMigrations {
|
||||
if _, err := d.Exec(m); err != nil {
|
||||
@@ -252,6 +258,7 @@ func (d *Database) migrate() error {
|
||||
scaleIndexes := []string{
|
||||
`CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_agents_last_seen ON agents(last_seen)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_agents_status_last_seen ON agents(status, last_seen)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_fleet_task_runs_agent ON fleet_task_runs(agent_id)`,
|
||||
}
|
||||
for _, m := range scaleIndexes {
|
||||
|
||||
@@ -64,6 +64,36 @@ func TestGetAgentNotFound(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertAgentPreservesOperatorNameWhenDiffersFromHostname(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
a := &models.Agent{
|
||||
ID: "renamed-worker",
|
||||
Name: "Living Room PC",
|
||||
Hostname: "DESKTOP-ABC123",
|
||||
Status: "offline",
|
||||
LastSeen: time.Now(),
|
||||
}
|
||||
if err := d.UpsertAgent(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reconnect reports hostname only — UpsertAgent must keep operator label.
|
||||
a.Name = "DESKTOP-ABC123"
|
||||
a.Hostname = "DESKTOP-ABC123"
|
||||
a.Status = "online"
|
||||
if err := d.UpsertAgent(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := d.GetAgent(a.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Name != "Living Room PC" {
|
||||
t.Fatalf("operator name should be preserved; got %q hostname=%q", got.Name, got.Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpsertAgentPreservesCreatedAt(t *testing.T) {
|
||||
d := openTestDB(t)
|
||||
a := seedAgent(t, d, "persist-created")
|
||||
|
||||
Reference in New Issue
Block a user