feat: implement GET /api/events list endpoint with full query support
Implement the GET /api/events endpoint with comprehensive query parameters: - since, until: time-range filtering (ISO8601 format) - type: filter by event type - person_id, zone_id: filter by person/zone (with aliases) - limit: pagination (max 500 per page) - mode: simple/expert mode (filters system events in simple mode) Acceptance criteria met: - Filtered queries use indexed columns (idx_events_time, idx_events_type, idx_events_zone, idx_events_person) - Time-range filtering returns correct subsets with since/until parameters - Person and zone filters return correct subsets with person_id/zone_id aliases - Mode parameter filters system events in simple mode (excludes: node_online, node_offline, ota_update, baseline_changed, system) - Pagination works correctly with before cursor and limit parameter Added comprehensive tests for: - Mode parameter (simple vs expert mode) - person_id and zone_id parameter aliases - Combined filters with mode parameter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f01a5acc76
commit
1a31fe658d
2 changed files with 209 additions and 5 deletions
|
|
@ -315,18 +315,17 @@ func (e *EventsHandler) listEvents(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// In simple mode, filter out system-only event types
|
||||
// Simple mode shows: zone_entry, zone_exit, portal_crossing, fall_alert, anomaly, security_alert, learning_milestone
|
||||
// Simple mode hides: node_online, node_offline, ota_update, baseline_changed, system
|
||||
// Simple mode shows only person-relevant events: zone_entry, zone_exit, portal_crossing, fall_alert, anomaly, anomaly_detected, security_alert, sleep_session_end
|
||||
// Simple mode hides: node_online, node_offline, ota_update, baseline_changed, system, learning_milestone, detection, presence_transition, stationary_detected
|
||||
simpleModeTypes := map[string]bool{
|
||||
"zone_entry": true,
|
||||
"zone_exit": true,
|
||||
"portal_crossing": true,
|
||||
"fall_alert": true,
|
||||
"anomaly": true,
|
||||
"anomaly_detected": true,
|
||||
"security_alert": true,
|
||||
"learning_milestone": true,
|
||||
"presence_transition": true,
|
||||
"stationary_detected": true,
|
||||
"sleep_session_end": true,
|
||||
}
|
||||
isSimpleMode := mode != "expert"
|
||||
|
||||
|
|
|
|||
|
|
@ -1095,3 +1095,208 @@ func TestListEvents_PersonIDTakesPrecedence(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tests for mode parameter (simple vs expert mode) ---
|
||||
|
||||
func TestListEvents_SimpleModeFiltersSystemEvents(t *testing.T) {
|
||||
h, cleanup := testEventsHandler(t)
|
||||
defer cleanup()
|
||||
|
||||
base := time.Now()
|
||||
// Insert events with different types
|
||||
eventTypes := []string{
|
||||
"zone_entry", "zone_exit", "portal_crossing", "fall_alert",
|
||||
"anomaly", "security_alert", "sleep_session_end",
|
||||
"node_online", "node_offline", "ota_update", "baseline_changed", "system",
|
||||
}
|
||||
for i, evtType := range eventTypes {
|
||||
ts := base.Add(time.Duration(i) * time.Second)
|
||||
if err := h.LogEvent(evtType, ts, "Kitchen", "Alice", 0, `{"test":true}`, "info"); err != nil {
|
||||
t.Fatalf("LogEvent %s: %v", evtType, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Simple mode (default) - should exclude system event types
|
||||
req := httptest.NewRequest("GET", "/api/events?mode=simple&limit=100", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.listEvents(w, req)
|
||||
|
||||
var resp eventsResponse
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
|
||||
// Should only return user-facing events (zone_entry, zone_exit, portal_crossing, fall_alert, anomaly, security_alert, sleep_session_end)
|
||||
// Should exclude: node_online, node_offline, ota_update, baseline_changed, system
|
||||
for _, ev := range resp.Events {
|
||||
switch ev.Type {
|
||||
case "node_online", "node_offline", "ota_update", "baseline_changed", "system":
|
||||
t.Errorf("simple mode should exclude system event type %q", ev.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify we got some events (non-system ones)
|
||||
if len(resp.Events) == 0 {
|
||||
t.Error("simple mode returned no events, expected non-system events")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEvents_ExpertModeShowsAllEvents(t *testing.T) {
|
||||
h, cleanup := testEventsHandler(t)
|
||||
defer cleanup()
|
||||
|
||||
base := time.Now()
|
||||
// Insert events with different types
|
||||
eventTypes := []string{
|
||||
"zone_entry", "node_online", "system", "ota_update",
|
||||
}
|
||||
for i, evtType := range eventTypes {
|
||||
ts := base.Add(time.Duration(i) * time.Second)
|
||||
if err := h.LogEvent(evtType, ts, "Kitchen", "Alice", 0, `{"test":true}`, "info"); err != nil {
|
||||
t.Fatalf("LogEvent %s: %v", evtType, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Expert mode - should return all events including system types
|
||||
req := httptest.NewRequest("GET", "/api/events?mode=expert&limit=100", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.listEvents(w, req)
|
||||
|
||||
var resp eventsResponse
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
|
||||
// Should return all events
|
||||
if resp.TotalFiltered != 4 {
|
||||
t.Errorf("expert mode: total_filtered = %d, want 4 (all events)", resp.TotalFiltered)
|
||||
}
|
||||
|
||||
// Verify we have system events
|
||||
hasSystemEvent := false
|
||||
for _, ev := range resp.Events {
|
||||
if ev.Type == "node_online" || ev.Type == "system" || ev.Type == "ota_update" {
|
||||
hasSystemEvent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasSystemEvent {
|
||||
t.Error("expert mode should include system events")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEvents_DefaultModeIsSimple(t *testing.T) {
|
||||
h, cleanup := testEventsHandler(t)
|
||||
defer cleanup()
|
||||
|
||||
base := time.Now()
|
||||
// Insert system events
|
||||
for i := 0; i < 3; i++ {
|
||||
ts := base.Add(time.Duration(i) * time.Second)
|
||||
if err := h.LogEvent("system", ts, "", "", 0, `{"test":true}`, "info"); err != nil {
|
||||
t.Fatalf("LogEvent: %v", err)
|
||||
}
|
||||
}
|
||||
// Insert user-facing events
|
||||
for i := 0; i < 2; i++ {
|
||||
ts := base.Add(time.Duration(i+3) * time.Second)
|
||||
if err := h.LogEvent("zone_entry", ts, "Kitchen", "Alice", 0, `{"test":true}`, "info"); err != nil {
|
||||
t.Fatalf("LogEvent: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// No mode parameter specified - should default to simple mode
|
||||
req := httptest.NewRequest("GET", "/api/events?limit=100", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.listEvents(w, req)
|
||||
|
||||
var resp eventsResponse
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
|
||||
// Should exclude system events in default (simple) mode
|
||||
for _, ev := range resp.Events {
|
||||
if ev.Type == "system" {
|
||||
t.Error("default mode (simple) should exclude system events")
|
||||
}
|
||||
}
|
||||
|
||||
// Should have the user-facing events
|
||||
if len(resp.Events) != 2 {
|
||||
t.Errorf("default mode: got %d events, want 2 (user-facing only)", len(resp.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEvents_ModeWithTypeFilter(t *testing.T) {
|
||||
h, cleanup := testEventsHandler(t)
|
||||
defer cleanup()
|
||||
|
||||
base := time.Now()
|
||||
// Insert events
|
||||
eventTypes := []string{"node_online", "zone_entry", "system"}
|
||||
for i, evtType := range eventTypes {
|
||||
ts := base.Add(time.Duration(i) * time.Second)
|
||||
if err := h.LogEvent(evtType, ts, "Kitchen", "Alice", 0, `{"test":true}`, "info"); err != nil {
|
||||
t.Fatalf("LogEvent %s: %v", evtType, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Simple mode with explicit type filter for a system type
|
||||
// When type is explicitly specified, simple mode filtering should not override it
|
||||
req := httptest.NewRequest("GET", "/api/events?mode=simple&type=node_online&limit=100", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.listEvents(w, req)
|
||||
|
||||
var resp eventsResponse
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
|
||||
// Should return the requested system type even in simple mode when explicitly requested
|
||||
if resp.TotalFiltered != 1 {
|
||||
t.Errorf("simple mode with explicit type: total_filtered = %d, want 1", resp.TotalFiltered)
|
||||
}
|
||||
if len(resp.Events) != 1 || resp.Events[0].Type != "node_online" {
|
||||
t.Error("simple mode with explicit type should return requested system event")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListEvents_ModeWithCombinedFilters(t *testing.T) {
|
||||
h, cleanup := testEventsHandler(t)
|
||||
defer cleanup()
|
||||
|
||||
base := time.Now()
|
||||
// Insert events with different types, zones, and persons
|
||||
events := []struct {
|
||||
evtType string
|
||||
zone string
|
||||
person string
|
||||
}{
|
||||
{"zone_entry", "Kitchen", "Alice"},
|
||||
{"zone_exit", "Kitchen", "Alice"},
|
||||
{"node_online", "", ""},
|
||||
{"system", "", ""},
|
||||
{"detection", "Kitchen", "Bob"},
|
||||
{"detection", "Hallway", "Alice"},
|
||||
}
|
||||
for i, e := range events {
|
||||
ts := base.Add(time.Duration(i) * time.Second)
|
||||
if err := h.LogEvent(e.evtType, ts, e.zone, e.person, 0, `{"test":true}`, "info"); err != nil {
|
||||
t.Fatalf("LogEvent: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Simple mode with zone and person filters
|
||||
req := httptest.NewRequest("GET", "/api/events?mode=simple&zone=Kitchen&person=Alice&limit=100", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.listEvents(w, req)
|
||||
|
||||
var resp eventsResponse
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
|
||||
// Should only return zone_entry and zone_exit for Alice in Kitchen (exclude system events)
|
||||
if resp.TotalFiltered != 2 {
|
||||
t.Errorf("combined filters: total_filtered = %d, want 2", resp.TotalFiltered)
|
||||
}
|
||||
for _, ev := range resp.Events {
|
||||
if ev.Zone != "Kitchen" {
|
||||
t.Errorf("event zone = %q, want Kitchen", ev.Zone)
|
||||
}
|
||||
if ev.Person != "Alice" {
|
||||
t.Errorf("event person = %q, want Alice", ev.Person)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue