From 44d94d53bec454b948ec4dc6f8ca99367d8e05df Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Fri, 28 Nov 2025 21:12:27 +0100 Subject: [PATCH 01/22] wip: add design doc for capturing with global / session mode --- docs/design/capture-session-system.md | 321 ++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 docs/design/capture-session-system.md diff --git a/docs/design/capture-session-system.md b/docs/design/capture-session-system.md new file mode 100644 index 0000000..b0b0a4d --- /dev/null +++ b/docs/design/capture-session-system.md @@ -0,0 +1,321 @@ +# Technical Design: Capture Session System + +## Goal + +Enable on-demand event collection in Devlog. By default, no events are collected. Users can enable capture through the +dashboard UI. Each user gets their own isolated storage with a configurable capture mode: + +1. **Session Mode**: Collects events only for HTTP requests with the user's session cookie +2. **Global Mode**: Collects all events (but stored per-user, so clearing doesn't affect others) + +## High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ CAPTURE FLOW │ +│ (Request → Event → Storage) │ +└─────────────────────────────────────────────────────────────────────────────┘ + + HTTP Request + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ HTTPServerCollector.Middleware │ +│ │ +│ 1. Extract session cookie → ctx = WithSessionID(ctx, id) │ +│ 2. shouldCapture := aggregator.ShouldCapture(ctx) ◄───────────────────┐ │ +│ │ │ +│ if !shouldCapture { next.ServeHTTP(w,r); return } // early bailout │ │ +│ │ │ +│ 3. ctx = aggregator.StartEvent(ctx) // adds GroupID │ │ +│ 4. Capture request body, wrap response writer │ │ +│ 5. next.ServeHTTP(wrappedWriter, r.WithContext(ctx)) │ │ +│ 6. aggregator.EndEvent(ctx, httpServerRequest) │ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ │ + │ ctx carries: SessionID + GroupID │ + ▼ │ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Child Collectors │ +│ (DB Query, Log, HTTP Client) │ +│ │ +│ if !aggregator.ShouldCapture(ctx) { return } ◄────────────────────────┤ │ +│ │ │ +│ aggregator.CollectEvent(ctx, data) │ │ +│ // Uses GroupID from ctx to link as child event │ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ │ + ▼ │ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ EventAggregator │ +│ │ +│ ShouldCapture(ctx) bool ◄─────────────────────────────────────────────┘ │ +│ - Iterates registered storages │ +│ - Returns true if any storage.ShouldCapture(ctx) returns true │ +│ │ +│ RegisterStorage(s) / UnregisterStorage(id) / GetStorage(id) │ +│ StartEvent(ctx) / EndEvent(ctx, data) / CollectEvent(ctx, data) │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + │ Dispatch: for each storage that ShouldCapture(ctx) + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ CaptureStorages (per user) │ +│ │ +│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │ +│ │ CaptureStorage │ │ CaptureStorage │ │ CaptureStorage │ │ +│ │ (User A) │ │ (User B) │ │ (User C) │ │ +│ ├───────────────────┤ ├───────────────────┤ ├───────────────────┤ │ +│ │ sessionID: uuid-A │ │ sessionID: uuid-B │ │ sessionID: uuid-C │ │ +│ │ mode: session │ │ mode: global │ │ mode: session │ │ +│ ├───────────────────┤ ├───────────────────┤ ├───────────────────┤ │ +│ │ ShouldCapture: │ │ ShouldCapture: │ │ ShouldCapture: │ │ +│ │ ctx.session == A │ │ return true │ │ ctx.session == C │ │ +│ ├───────────────────┤ ├───────────────────┤ ├───────────────────┤ │ +│ │ RingBuffer │ │ RingBuffer │ │ RingBuffer │ │ +│ │ Notifier ─────────│► │ Notifier ─────────│► │ Notifier ─────────│► │ +│ └───────────────────┘ └───────────────────┘ └───────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ │ │ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ NOTIFICATION FLOW │ +│ (Storage → SSE → Dashboard) │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ │ │ + │ Subscribe(ctx) │ Subscribe(ctx) │ Subscribe(ctx) + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ SSE Handler │ │ SSE Handler │ │ SSE Handler │ +│ (User A) │ │ (User B) │ │ (User C) │ +│ │ │ │ │ │ +│ GET /events-sse│ │GET /events-sse │ │GET /events-sse │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ Dashboard │ │ Dashboard │ │ Dashboard │ +│ (User A) │ │ (User B) │ │ (User C) │ +│ Session Mode │ │ Global Mode │ │ Session Mode │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## Data Flow Summary + +### Capture Flow (Request → Storage) + +1. **Middleware** extracts session cookie, adds to context +2. **Middleware** asks `aggregator.ShouldCapture(ctx)` - O(1) check +3. If capturing: do expensive work (body capture, response wrap) +4. **Aggregator** creates Event, dispatches to matching storages +5. **Storage** adds event pointer to its ring buffer + +### Notification Flow (Storage → Dashboard) + +1. **Storage** notifies its subscribers when event is added +2. **SSE Handler** receives notification, sends to browser +3. **Dashboard** updates event list in real-time + +## EventAggregator + +Central coordinator (no storage, just dispatch). Treats all storages uniformly via the `EventStorage` interface: + +- `ShouldCapture(ctx)` - Iterates storages, returns true if any wants to capture +- `StartEvent(ctx)` / `EndEvent(ctx, data)` / `CollectEvent(ctx, data)` - Event lifecycle +- `RegisterStorage(s)` / `UnregisterStorage(id)` / `GetStorage(id)` - Uniform storage management + +## EventStorage Interface + +```go +type EventStorage interface { + ID() uuid.UUID + ShouldCapture(ctx context.Context) bool + Add(event *Event) + GetEvent(id uuid.UUID) (*Event, bool) + GetEvents(limit uint64) []*Event + Subscribe(ctx context.Context) <-chan *Event + Clear() + Close() +} +``` + +## CaptureStorage + +Single storage type with configurable capture mode: + +```go +type CaptureMode int +const ( + CaptureModeSession CaptureMode = iota // only matching session + CaptureModeGlobal // all requests +) +``` + +- `sessionID`: Identifies the storage owner (from cookie) +- `captureMode`: Session or Global +- `ShouldCapture(ctx)`: + - If mode == session: return `ctx.sessionID == s.sessionID` + - If mode == global: return `true` +- `SetCaptureMode(mode)` / `CaptureMode()`: Toggle capture mode +- Created when user clicks "Start Capture" +- Destroyed after idle timeout or explicit stop +- Each user gets their own storage (clearing is isolated) + +## Capture Lifecycle + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ CAPTURE LIFECYCLE │ +└─────────────────────────────────────────────────────────────────────────────┘ + +START ACTIVE CLEANUP + │ │ │ + ▼ ▼ ▼ +┌──────────────┐ ┌─────────────┐ ┌──────────────┐ +│ POST │ ─────── │ SSE │ ─────────────── │ Idle Timeout │ +│ /capture │ cookie │ connected │ disconnect │ (30s) │ +│ /start │ │ │ │ │ +└──────────────┘ └─────────────┘ └──────────────┘ + │ │ │ + ▼ ▼ ▼ + Create storage Keep-alive via Unregister + Register with SSE connection storage from + aggregator (or explicit aggregator + Set cookie "Stop" button) → GC cleanup + │ + ▼ + ┌─────────────────────┐ + │ POST /capture/mode │ + │ Toggle: session ↔ │ + │ global │ + └─────────────────────┘ +``` + +## Key Design Decisions + +| Decision | Choice | Rationale | +|------------------------|---------------------------------------------|------------------------------------------------------| +| Breaking API change | Yes | v0.2.1 exists, users use high-level `devlog` package | +| Dashboard skip | Use SkipPaths | Add dashboard routes to HTTPServerOptions.SkipPaths | +| Storage model | Single CaptureStorage with mode per user | Each user isolated, clearing doesn't affect others | +| Storage-driven capture | Each storage decides via ShouldCapture(ctx) | Clean separation, extensible | +| Separate buffers | Each storage has own RingBuffer | Isolation, independent capacity | +| Shared events | Storages hold *Event pointers | Memory efficient, GC handles cleanup | +| Cookie scope | Application root (`/`) | Cookie sent with all app requests | +| Cookie name | `devlog_session` | Clear purpose, HttpOnly for security | +| UI placement | Header bar | Always visible, quick access | +| Idle timeout | 30s after SSE disconnect | Allows page reload without losing session | + +## API Endpoints (New) + +| Method | Path | Purpose | +|--------|-------------------|---------------------------------------------------------| +| POST | `/capture/start` | Create CaptureStorage for user, set cookie, register | +| POST | `/capture/stop` | Unregister storage, clear cookie | +| POST | `/capture/mode` | Set capture mode (session or global) for user's storage | +| GET | `/capture/status` | Get current capture state (active, mode) | + +## Configuration + +```go +type Options struct { + // ... existing fields ... + + // Capture configuration (if nil, no capture by default) + CaptureOptions *CaptureOptions +} + +type CaptureOptions struct { + StorageCapacity uint64 // Events per storage (default: 1000) + SessionIdleTimeout time.Duration // Cleanup delay after disconnect (default: 30s) +} +``` + +## Files to Modify + +| File | Changes | +|---------------------------------|---------------------------------------------------------------------------------| +| `collector/event_storage.go` | **NEW** - EventStorage interface and implementations | +| `collector/event_aggregator.go` | **RENAME** from event_collector.go - storage registry, dispatch to storages | +| `collector/session_context.go` | **NEW** - Context helpers for session ID | +| `collector/http_server.go` | Extract cookie, add session ID to context | +| `dashboard/handler.go` | New endpoints, storage-aware event fetching | +| `dashboard/views/header.templ` | Capture control buttons in header | +| `devlog.go` | CaptureOptions, storage initialization, rename EventCollector → EventAggregator | + +## TDD Test Cases + +### 1. CaptureStorage Tests (`collector/event_storage_test.go` - NEW) + +| Test | Fixture | Action | Expectation | +|------------------------------------------------------------|----------------------------------|------------------------------------|---------------------------| +| `TestCaptureStorage_ShouldCapture_SessionMode_NoSessionInCtx` | CaptureStorage(session mode) | ShouldCapture(ctx without session) | Returns false | +| `TestCaptureStorage_ShouldCapture_SessionMode_WrongSession` | CaptureStorage(uuid-A, session) | ShouldCapture(ctx with uuid-B) | Returns false | +| `TestCaptureStorage_ShouldCapture_SessionMode_MatchingSession` | CaptureStorage(uuid-A, session) | ShouldCapture(ctx with uuid-A) | Returns true | +| `TestCaptureStorage_ShouldCapture_GlobalMode_AlwaysTrue` | CaptureStorage(uuid-A, global) | ShouldCapture(any ctx) | Returns true | +| `TestCaptureStorage_SetCaptureMode` | CaptureStorage | SetCaptureMode(mode) | CaptureMode() returns mode| +| `TestCaptureStorage_Add_StoresEvent` | CaptureStorage | Add(event) | GetEvents() returns event | +| `TestCaptureStorage_RingBuffer_Capacity` | CaptureStorage capacity=5 | Add 10 events | Only last 5 kept | +| `TestCaptureStorage_Subscribe_ReceivesEvents` | CaptureStorage | Subscribe, Add event | Channel receives event | +| `TestCaptureStorage_GetEvent_ByID` | CaptureStorage with events | GetEvent(id) | Returns correct event | + +### 2. EventAggregator Tests (`collector/event_aggregator_test.go` - ADAPT existing) + +| Test | Fixture | Action | Expectation | +|-----------------------------------------------------------------|-------------------------------------------|-----------------------------------------|----------------------------| +| `TestEventAggregator_ShouldCapture_NoStorages` | Aggregator without storages | ShouldCapture(ctx) | Returns false | +| `TestEventAggregator_ShouldCapture_SessionModeMatch` | Aggregator + Storage(uuid-A, session) | ShouldCapture(ctx with uuid-A) | Returns true | +| `TestEventAggregator_ShouldCapture_SessionModeNoMatch` | Aggregator + Storage(uuid-A, session) | ShouldCapture(ctx with uuid-B) | Returns false | +| `TestEventAggregator_ShouldCapture_GlobalMode` | Aggregator + Storage(uuid-A, global) | ShouldCapture(any ctx) | Returns true | +| `TestEventAggregator_CollectEvent_DispatchesToMatchingStorages` | Aggregator + Storage(A, session) + Storage(B, global) | CollectEvent(ctx with uuid-A) | Event in both storages | +| `TestEventAggregator_CollectEvent_MultipleGlobalStorages` | Aggregator + Storage(A, global) + Storage(B, global) | CollectEvent(ctx) | Event in both storages | +| `TestEventAggregator_CollectEvent_NoCapture_NoDispatch` | Aggregator + Storage(A, session) | CollectEvent(ctx with uuid-B) | No events stored | +| `TestEventAggregator_RegisterUnregister_Storage` | Aggregator | Register, Unregister | Storage added/removed | +| `TestEventAggregator_StartEndEvent_WithCapture` | Aggregator + Storage(global) | StartEvent, EndEvent | Event with timing stored | +| `TestEventAggregator_StartEndEvent_NoCapture` | Aggregator (no storages) | StartEvent, EndEvent | No events stored | +| `TestEventAggregator_NestedEvents_WithCapture` | Aggregator + Storage(global) | Start parent, collect child, end parent | Parent has child | + +### 3. HTTPServerCollector Tests (`collector/http_server_test.go` - ADAPT) + +| Test | Fixture | Action | Expectation | +|----------------------------------------------------------------|------------------------------------------------|------------------------------------|-----------------------------------| +| `TestHTTPServer_Middleware_ExtractsCookie` | Middleware + Storage | Request with devlog_session cookie | Session ID in context | +| `TestHTTPServer_Middleware_NoCookie_NoSessionInCtx` | Middleware | Request without cookie | No session ID in context | +| `TestHTTPServer_Middleware_NoCapture_SkipsExpensiveWork` | Middleware + no storages | Request | No body capture, fast passthrough | +| `TestHTTPServer_Middleware_Capture_CapturesBody` | Middleware + Storage(global) | Request with body | Body captured in event | +| `TestHTTPServer_Middleware_SessionCapture_OnlyMatchingSession` | Middleware + Storage(A, session) + Storage(B, session) | Request with session A cookie | Event only in Storage A | + +### 4. Session Context Tests (`collector/session_context_test.go` - NEW) + +| Test | Fixture | Action | Expectation | +|-----------------------------------|----------------------|---------------------------|-----------------------------------| +| `TestWithSessionID_AddsToContext` | Empty context | WithSessionID(ctx, uuid) | SessionIDFromContext returns uuid | +| `TestSessionIDFromContext_NotSet` | Empty context | SessionIDFromContext(ctx) | Returns uuid.Nil, false | +| `TestSessionIDFromContext_Set` | Context with session | SessionIDFromContext(ctx) | Returns correct uuid, true | + +### 5. Dashboard Handler Tests (`dashboard/handler_test.go` - NEW or ADAPT) + +| Test | Fixture | Action | Expectation | +|----------------------------------------------|--------------------------------|-----------------------------|---------------------------------------| +| `TestHandler_CaptureStart_CreatesStorage` | Handler + Aggregator | POST /capture/start | CaptureStorage registered, cookie set | +| `TestHandler_CaptureStart_SetsCookie` | Handler | POST /capture/start | Response has devlog_session cookie | +| `TestHandler_CaptureStop_UnregistersStorage` | Handler + active capture | POST /capture/stop | CaptureStorage unregistered | +| `TestHandler_CaptureStop_ClearsCookie` | Handler + active capture | POST /capture/stop | Cookie cleared (MaxAge=-1) | +| `TestHandler_CaptureMode_SetsSessionMode` | Handler + active capture | POST /capture/mode (session)| Storage mode is session | +| `TestHandler_CaptureMode_SetsGlobalMode` | Handler + active capture | POST /capture/mode (global) | Storage mode is global | +| `TestHandler_CaptureStatus_ReturnsState` | Handler | GET /capture/status | JSON with active/mode state | +| `TestHandler_EventsSSE_FromUserStorage` | Handler + Storage with events | GET /events-sse with cookie | Receives events from user's storage | +| `TestHandler_EventList_FromUserStorage` | Handler + Storage with events | GET /event-list with cookie | Returns user's events | + +### 6. Integration / E2E Tests (`devlog_e2e_test.go` - ADAPT) + +| Test | Fixture | Action | Expectation | +|-----------------------------------------------|----------------------------------|---------------------------------------------|-----------------------------------| +| `TestE2E_NoCapture_ByDefault` | Devlog instance (default config) | Make HTTP request | No events captured | +| `TestE2E_SessionMode_OnlyMatchingRequests` | Devlog + capture (session mode) | Requests with/without cookie | Only cookie requests captured | +| `TestE2E_GlobalMode_CapturesAll` | Devlog + capture (global mode) | Make HTTP requests | All requests captured | +| `TestE2E_Capture_CapturesChildEvents` | Devlog + capture | Request with DB query + log | HTTP + DB + log in same storage | +| `TestE2E_Capture_Cleanup_AfterTimeout` | Devlog + capture | Start capture, disconnect SSE, wait timeout | Storage cleaned up | +| `TestE2E_MultipleUsers_IsolatedStorages` | Devlog | Two users, each captures | Each has own storage, clear isolated | +| `TestE2E_MultipleUsers_GlobalMode_EachGetsAllEvents` | Devlog | Two users in global mode, make request | Both get the event in their storage | From f4be190de04deb2ca5c7ec997516a4f5f14469c4 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Sat, 29 Nov 2025 00:22:29 +0100 Subject: [PATCH 02/22] wip: basic working session capturing --- collector/db_query_collector.go | 20 +- collector/event_aggregator.go | 172 +++++++++++ collector/event_aggregator_test.go | 311 ++++++++++++++++++++ collector/event_storage.go | 137 +++++++++ collector/event_storage_test.go | 231 +++++++++++++++ collector/http_client.go | 45 ++- collector/http_server.go | 64 ++++- collector/http_server_test.go | 199 +++++++++++++ collector/log_collector.go | 20 +- collector/session_context.go | 25 ++ collector/session_context_test.go | 41 +++ dashboard/handler.go | 448 ++++++++++++++++++++++++++--- dashboard/static/main.css | 10 + dashboard/views/button.go | 8 +- dashboard/views/dashboard.templ | 20 +- dashboard/views/dashboard_templ.go | 5 +- dashboard/views/header.templ | 145 ++++++++-- dashboard/views/header_templ.go | 409 +++++++++++++++++++++++++- dashboard/views/layout.templ | 10 +- dashboard/views/layout_templ.go | 8 +- devlog.go | 47 ++- devlog_e2e_test.go | 1 - 22 files changed, 2231 insertions(+), 145 deletions(-) create mode 100644 collector/event_aggregator.go create mode 100644 collector/event_aggregator_test.go create mode 100644 collector/event_storage.go create mode 100644 collector/event_storage_test.go create mode 100644 collector/session_context.go create mode 100644 collector/session_context_test.go diff --git a/collector/db_query_collector.go b/collector/db_query_collector.go index 1e2e4df..b4b5887 100644 --- a/collector/db_query_collector.go +++ b/collector/db_query_collector.go @@ -23,9 +23,10 @@ type DBQuery struct { } type DBQueryCollector struct { - buffer *RingBuffer[DBQuery] - notifier *Notifier[DBQuery] - eventCollector *EventCollector + buffer *RingBuffer[DBQuery] + notifier *Notifier[DBQuery] + eventCollector *EventCollector // Deprecated: use eventAggregator + eventAggregator *EventAggregator } func (c *DBQueryCollector) Collect(ctx context.Context, query DBQuery) { @@ -33,7 +34,9 @@ func (c *DBQueryCollector) Collect(ctx context.Context, query DBQuery) { c.buffer.Add(query) } c.notifier.Notify(query) - if c.eventCollector != nil { + if c.eventAggregator != nil { + c.eventAggregator.CollectEvent(ctx, query) + } else if c.eventCollector != nil { c.eventCollector.CollectEvent(ctx, query) } } @@ -55,7 +58,11 @@ type DBQueryOptions struct { NotifierOptions *NotifierOptions // EventCollector is an optional event collector for collecting logs as grouped events + // Deprecated: Use EventAggregator instead EventCollector *EventCollector + + // EventAggregator is the aggregator for collecting queries as grouped events + EventAggregator *EventAggregator } func DefaultDBQueryOptions() DBQueryOptions { @@ -73,8 +80,9 @@ func NewDBQueryCollectorWithOptions(capacity uint64, options DBQueryOptions) *DB } collector := &DBQueryCollector{ - notifier: NewNotifierWithOptions[DBQuery](notifierOptions), - eventCollector: options.EventCollector, + notifier: NewNotifierWithOptions[DBQuery](notifierOptions), + eventCollector: options.EventCollector, + eventAggregator: options.EventAggregator, } if capacity > 0 { collector.buffer = NewRingBuffer[DBQuery](capacity) diff --git a/collector/event_aggregator.go b/collector/event_aggregator.go new file mode 100644 index 0000000..9873dc4 --- /dev/null +++ b/collector/event_aggregator.go @@ -0,0 +1,172 @@ +package collector + +import ( + "context" + "sync" + "time" + + "github.com/gofrs/uuid" +) + +// EventAggregator coordinates event collection and dispatches events to registered storages. +// It does not store events itself - each storage has its own buffer. +type EventAggregator struct { + storages map[uuid.UUID]EventStorage + openGroups map[uuid.UUID]*Event + + mu sync.RWMutex +} + +// NewEventAggregator creates a new EventAggregator. +func NewEventAggregator() *EventAggregator { + return &EventAggregator{ + storages: make(map[uuid.UUID]EventStorage), + openGroups: make(map[uuid.UUID]*Event), + } +} + +// RegisterStorage registers a storage with the aggregator. +func (a *EventAggregator) RegisterStorage(storage EventStorage) { + a.mu.Lock() + defer a.mu.Unlock() + a.storages[storage.ID()] = storage +} + +// UnregisterStorage removes a storage from the aggregator. +func (a *EventAggregator) UnregisterStorage(id uuid.UUID) { + a.mu.Lock() + defer a.mu.Unlock() + delete(a.storages, id) +} + +// GetStorage returns a storage by ID, or nil if not found. +func (a *EventAggregator) GetStorage(id uuid.UUID) EventStorage { + a.mu.RLock() + defer a.mu.RUnlock() + return a.storages[id] +} + +// ShouldCapture returns true if any registered storage wants to capture events for the given context. +func (a *EventAggregator) ShouldCapture(ctx context.Context) bool { + a.mu.RLock() + defer a.mu.RUnlock() + + for _, storage := range a.storages { + if storage.ShouldCapture(ctx) { + return true + } + } + return false +} + +// StartEvent starts a new event and returns a new context with the group ID. +// Child events collected with this context will be grouped under this event. +// Call EndEvent to finish the event. +func (a *EventAggregator) StartEvent(ctx context.Context) context.Context { + eventID := uuid.Must(uuid.NewV7()) + + a.mu.Lock() + defer a.mu.Unlock() + + evt := &Event{ + ID: eventID, + Start: time.Now(), + } + + // Check if there's an outer group + outerGroupID, ok := groupIDFromContext(ctx) + if ok { + evt.GroupID = &outerGroupID + } + + a.openGroups[eventID] = evt + + return withGroupID(ctx, eventID) +} + +// EndEvent finishes an event started with StartEvent and dispatches it to matching storages. +func (a *EventAggregator) EndEvent(ctx context.Context, data any) { + groupID, ok := groupIDFromContext(ctx) + if !ok { + return + } + + a.mu.Lock() + defer a.mu.Unlock() + + evt := a.openGroups[groupID] + if evt == nil { + return + } + + evt.Data = data + evt.End = time.Now() + + // Link to parent if exists + if evt.GroupID != nil { + parentEvt := a.openGroups[*evt.GroupID] + if parentEvt != nil { + parentEvt.Children = append(parentEvt.Children, evt) + } + } + + delete(a.openGroups, groupID) + + // Only dispatch top-level events to storages + if evt.GroupID == nil { + a.dispatchToStorages(ctx, evt) + } +} + +// CollectEvent creates and immediately completes an event, dispatching to matching storages. +func (a *EventAggregator) CollectEvent(ctx context.Context, data any) { + eventID := uuid.Must(uuid.NewV7()) + now := time.Now() + + a.mu.Lock() + defer a.mu.Unlock() + + evt := &Event{ + ID: eventID, + Data: data, + Start: now, + End: now, + } + + // Check if there's a parent group + outerGroupID, ok := groupIDFromContext(ctx) + if ok { + evt.GroupID = &outerGroupID + parentEvt := a.openGroups[outerGroupID] + if parentEvt != nil { + parentEvt.Children = append(parentEvt.Children, evt) + } + } + + // Only dispatch top-level events to storages + if evt.GroupID == nil { + a.dispatchToStorages(ctx, evt) + } +} + +// dispatchToStorages sends the event to all storages that want to capture it. +// Must be called with lock held. +func (a *EventAggregator) dispatchToStorages(ctx context.Context, evt *Event) { + for _, storage := range a.storages { + if storage.ShouldCapture(ctx) { + storage.Add(evt) + } + } +} + +// Close releases resources used by the aggregator. +func (a *EventAggregator) Close() { + a.mu.Lock() + defer a.mu.Unlock() + + for _, storage := range a.storages { + storage.Close() + } + a.storages = make(map[uuid.UUID]EventStorage) + a.openGroups = make(map[uuid.UUID]*Event) +} diff --git a/collector/event_aggregator_test.go b/collector/event_aggregator_test.go new file mode 100644 index 0000000..c1c611b --- /dev/null +++ b/collector/event_aggregator_test.go @@ -0,0 +1,311 @@ +package collector_test + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/networkteam/devlog/collector" +) + +func TestEventAggregator_ShouldCapture_NoStorages(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + ctx := context.Background() + + assert.False(t, aggregator.ShouldCapture(ctx)) +} + +func TestEventAggregator_ShouldCapture_SessionModeMatch(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) + aggregator.RegisterStorage(storage) + + ctx := collector.WithSessionID(context.Background(), sessionID) + + assert.True(t, aggregator.ShouldCapture(ctx)) +} + +func TestEventAggregator_ShouldCapture_SessionModeNoMatch(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + otherSessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) + aggregator.RegisterStorage(storage) + + ctx := collector.WithSessionID(context.Background(), otherSessionID) + + assert.False(t, aggregator.ShouldCapture(ctx)) +} + +func TestEventAggregator_ShouldCapture_GlobalMode(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + // Should return true for any context + assert.True(t, aggregator.ShouldCapture(context.Background())) + + // Even with a different session ID + otherSessionID := uuid.Must(uuid.NewV4()) + ctx := collector.WithSessionID(context.Background(), otherSessionID) + assert.True(t, aggregator.ShouldCapture(ctx)) +} + +func TestEventAggregator_CollectEvent_DispatchesToMatchingStorages(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionA := uuid.Must(uuid.NewV4()) + sessionB := uuid.Must(uuid.NewV4()) + + storageA := collector.NewCaptureStorage(sessionA, 100, collector.CaptureModeSession) + storageB := collector.NewCaptureStorage(sessionB, 100, collector.CaptureModeGlobal) + + aggregator.RegisterStorage(storageA) + aggregator.RegisterStorage(storageB) + + // Event with session A should go to both storages + // (A matches session, B is global) + ctx := collector.WithSessionID(context.Background(), sessionA) + aggregator.CollectEvent(ctx, "test event") + + eventsA := storageA.GetEvents(10) + eventsB := storageB.GetEvents(10) + + require.Len(t, eventsA, 1) + require.Len(t, eventsB, 1) + assert.Equal(t, "test event", eventsA[0].Data) + assert.Equal(t, "test event", eventsB[0].Data) +} + +func TestEventAggregator_CollectEvent_MultipleGlobalStorages(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionA := uuid.Must(uuid.NewV4()) + sessionB := uuid.Must(uuid.NewV4()) + + storageA := collector.NewCaptureStorage(sessionA, 100, collector.CaptureModeGlobal) + storageB := collector.NewCaptureStorage(sessionB, 100, collector.CaptureModeGlobal) + + aggregator.RegisterStorage(storageA) + aggregator.RegisterStorage(storageB) + + // Event should go to both global storages + aggregator.CollectEvent(context.Background(), "test event") + + eventsA := storageA.GetEvents(10) + eventsB := storageB.GetEvents(10) + + require.Len(t, eventsA, 1) + require.Len(t, eventsB, 1) +} + +func TestEventAggregator_CollectEvent_NoCapture_NoDispatch(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionA := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionA, 100, collector.CaptureModeSession) + aggregator.RegisterStorage(storage) + + // Event with different session should not be captured + otherSessionID := uuid.Must(uuid.NewV4()) + ctx := collector.WithSessionID(context.Background(), otherSessionID) + aggregator.CollectEvent(ctx, "test event") + + events := storage.GetEvents(10) + assert.Len(t, events, 0) +} + +func TestEventAggregator_RegisterUnregister_Storage(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + + // Register + aggregator.RegisterStorage(storage) + assert.True(t, aggregator.ShouldCapture(context.Background())) + + // Get storage + retrieved := aggregator.GetStorage(storage.ID()) + assert.NotNil(t, retrieved) + assert.Equal(t, storage.ID(), retrieved.ID()) + + // Unregister + aggregator.UnregisterStorage(storage.ID()) + assert.False(t, aggregator.ShouldCapture(context.Background())) + assert.Nil(t, aggregator.GetStorage(storage.ID())) +} + +func TestEventAggregator_StartEndEvent_WithCapture(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + ctx := context.Background() + ctx = aggregator.StartEvent(ctx) + + time.Sleep(10 * time.Millisecond) + + aggregator.EndEvent(ctx, "test event") + + events := storage.GetEvents(10) + require.Len(t, events, 1) + assert.Equal(t, "test event", events[0].Data) + assert.True(t, events[0].End.After(events[0].Start)) +} + +func TestEventAggregator_StartEndEvent_NoCapture(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + // No storages registered + + ctx := context.Background() + ctx = aggregator.StartEvent(ctx) + aggregator.EndEvent(ctx, "test event") + + // Can't verify no events stored since there's no storage + // but at least it shouldn't panic + assert.False(t, aggregator.ShouldCapture(ctx)) +} + +func TestEventAggregator_NestedEvents_WithCapture(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + // Start parent + ctx := context.Background() + parentCtx := aggregator.StartEvent(ctx) + + // Collect child + aggregator.CollectEvent(parentCtx, "child event") + + // End parent + aggregator.EndEvent(parentCtx, "parent event") + + events := storage.GetEvents(10) + require.Len(t, events, 1) + + parent := events[0] + assert.Equal(t, "parent event", parent.Data) + require.Len(t, parent.Children, 1) + assert.Equal(t, "child event", parent.Children[0].Data) +} + +func TestEventAggregator_DeeplyNestedEvents(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + ctx := context.Background() + + // Level 1 + ctx1 := aggregator.StartEvent(ctx) + + // Level 2 + ctx2 := aggregator.StartEvent(ctx1) + + // Level 3 + ctx3 := aggregator.StartEvent(ctx2) + + // End in reverse order + aggregator.EndEvent(ctx3, "Level 3") + aggregator.EndEvent(ctx2, "Level 2") + aggregator.EndEvent(ctx1, "Level 1") + + events := storage.GetEvents(10) + require.Len(t, events, 1) + + lvl1 := events[0] + assert.Equal(t, "Level 1", lvl1.Data) + require.Len(t, lvl1.Children, 1) + + lvl2 := lvl1.Children[0] + assert.Equal(t, "Level 2", lvl2.Data) + require.Len(t, lvl2.Children, 1) + + lvl3 := lvl2.Children[0] + assert.Equal(t, "Level 3", lvl3.Data) + assert.Len(t, lvl3.Children, 0) +} + +func TestEventAggregator_ConcurrentEvents(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + ctx := context.Background() + numGoroutines := 10 + + var wg sync.WaitGroup + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(id int) { + defer wg.Done() + + eventCtx := aggregator.StartEvent(ctx) + aggregator.CollectEvent(eventCtx, "Child") + aggregator.EndEvent(eventCtx, "Parent") + }(i) + } + + wg.Wait() + + events := storage.GetEvents(uint64(numGoroutines * 2)) + assert.Len(t, events, numGoroutines) + + for _, evt := range events { + assert.Equal(t, "Parent", evt.Data) + require.Len(t, evt.Children, 1) + assert.Equal(t, "Child", evt.Children[0].Data) + } +} + +func TestEventAggregator_EndEventWithoutStart(t *testing.T) { + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + // Try to end an event without starting one + ctx := context.Background() + aggregator.EndEvent(ctx, "This should not be captured") + + events := storage.GetEvents(10) + assert.Len(t, events, 0) +} diff --git a/collector/event_storage.go b/collector/event_storage.go new file mode 100644 index 0000000..e3d23a4 --- /dev/null +++ b/collector/event_storage.go @@ -0,0 +1,137 @@ +package collector + +import ( + "context" + + "github.com/gofrs/uuid" +) + +// EventStorage is the interface for event storage backends. +// Storages are responsible for deciding which events to capture and storing them. +type EventStorage interface { + // ID returns the unique identifier for this storage + ID() uuid.UUID + + // ShouldCapture returns true if this storage wants to capture events for the given context + ShouldCapture(ctx context.Context) bool + + // Add adds an event to the storage + Add(event *Event) + + // GetEvent retrieves an event by its ID + GetEvent(id uuid.UUID) (*Event, bool) + + // GetEvents returns the most recent n events + GetEvents(limit uint64) []*Event + + // Subscribe returns a channel that receives notifications of new events + Subscribe(ctx context.Context) <-chan *Event + + // Clear removes all events from the storage + Clear() + + // Close releases resources used by the storage + Close() +} + +// CaptureMode defines how a CaptureStorage decides which events to capture +type CaptureMode int + +const ( + // CaptureModeSession captures only events from requests with matching session ID + CaptureModeSession CaptureMode = iota + // CaptureModeGlobal captures all events + CaptureModeGlobal +) + +// CaptureStorage implements EventStorage with configurable capture mode. +// Each user gets their own CaptureStorage instance. +type CaptureStorage struct { + id uuid.UUID + sessionID uuid.UUID + captureMode CaptureMode + + buffer *LookupRingBuffer[*Event, uuid.UUID] + notifier *Notifier[*Event] +} + +// NewCaptureStorage creates a new CaptureStorage for the given session ID. +func NewCaptureStorage(sessionID uuid.UUID, capacity uint64, mode CaptureMode) *CaptureStorage { + return &CaptureStorage{ + id: uuid.Must(uuid.NewV7()), + sessionID: sessionID, + captureMode: mode, + buffer: NewLookupRingBuffer[*Event, uuid.UUID](capacity), + notifier: NewNotifier[*Event](), + } +} + +// ID returns the unique identifier for this storage +func (s *CaptureStorage) ID() uuid.UUID { + return s.id +} + +// SessionID returns the session ID this storage belongs to +func (s *CaptureStorage) SessionID() uuid.UUID { + return s.sessionID +} + +// CaptureMode returns the current capture mode +func (s *CaptureStorage) CaptureMode() CaptureMode { + return s.captureMode +} + +// SetCaptureMode sets the capture mode +func (s *CaptureStorage) SetCaptureMode(mode CaptureMode) { + s.captureMode = mode +} + +// ShouldCapture returns true if this storage wants to capture events for the given context +func (s *CaptureStorage) ShouldCapture(ctx context.Context) bool { + switch s.captureMode { + case CaptureModeGlobal: + return true + case CaptureModeSession: + ctxSessionID, ok := SessionIDFromContext(ctx) + if !ok { + return false + } + return ctxSessionID == s.sessionID + default: + return false + } +} + +// Add adds an event to the storage and notifies subscribers +func (s *CaptureStorage) Add(event *Event) { + s.buffer.Add(event) + s.notifier.Notify(event) +} + +// GetEvent retrieves an event by its ID +func (s *CaptureStorage) GetEvent(id uuid.UUID) (*Event, bool) { + return s.buffer.Lookup(id) +} + +// GetEvents returns the most recent n events +func (s *CaptureStorage) GetEvents(limit uint64) []*Event { + return s.buffer.GetRecords(limit) +} + +// Subscribe returns a channel that receives notifications of new events +func (s *CaptureStorage) Subscribe(ctx context.Context) <-chan *Event { + return s.notifier.Subscribe(ctx) +} + +// Clear removes all events from the storage +func (s *CaptureStorage) Clear() { + s.buffer.Clear() +} + +// Close releases resources used by the storage +func (s *CaptureStorage) Close() { + s.notifier.Close() +} + +// Ensure CaptureStorage implements EventStorage +var _ EventStorage = (*CaptureStorage)(nil) diff --git a/collector/event_storage_test.go b/collector/event_storage_test.go new file mode 100644 index 0000000..7f8ffea --- /dev/null +++ b/collector/event_storage_test.go @@ -0,0 +1,231 @@ +package collector_test + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/networkteam/devlog/collector" +) + +func TestCaptureStorage_ShouldCapture_SessionMode_NoSessionInCtx(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) + defer storage.Close() + + ctx := context.Background() // no session ID + + assert.False(t, storage.ShouldCapture(ctx)) +} + +func TestCaptureStorage_ShouldCapture_SessionMode_WrongSession(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + otherSessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) + defer storage.Close() + + ctx := collector.WithSessionID(context.Background(), otherSessionID) + + assert.False(t, storage.ShouldCapture(ctx)) +} + +func TestCaptureStorage_ShouldCapture_SessionMode_MatchingSession(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) + defer storage.Close() + + ctx := collector.WithSessionID(context.Background(), sessionID) + + assert.True(t, storage.ShouldCapture(ctx)) +} + +func TestCaptureStorage_ShouldCapture_GlobalMode_AlwaysTrue(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + defer storage.Close() + + // Should return true for any context + assert.True(t, storage.ShouldCapture(context.Background())) + + // Even with a different session ID + otherSessionID := uuid.Must(uuid.NewV4()) + ctx := collector.WithSessionID(context.Background(), otherSessionID) + assert.True(t, storage.ShouldCapture(ctx)) +} + +func TestCaptureStorage_SetCaptureMode(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) + defer storage.Close() + + assert.Equal(t, collector.CaptureModeSession, storage.CaptureMode()) + + storage.SetCaptureMode(collector.CaptureModeGlobal) + assert.Equal(t, collector.CaptureModeGlobal, storage.CaptureMode()) + + storage.SetCaptureMode(collector.CaptureModeSession) + assert.Equal(t, collector.CaptureModeSession, storage.CaptureMode()) +} + +func TestCaptureStorage_Add_StoresEvent(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + defer storage.Close() + + event := &collector.Event{ + ID: uuid.Must(uuid.NewV7()), + Data: "test data", + Start: time.Now(), + End: time.Now(), + } + + storage.Add(event) + + events := storage.GetEvents(10) + require.Len(t, events, 1) + assert.Equal(t, event.ID, events[0].ID) + assert.Equal(t, "test data", events[0].Data) +} + +func TestCaptureStorage_RingBuffer_Capacity(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 5, collector.CaptureModeGlobal) + defer storage.Close() + + // Add 10 events + for i := 0; i < 10; i++ { + event := &collector.Event{ + ID: uuid.Must(uuid.NewV7()), + Data: i, + Start: time.Now(), + End: time.Now(), + } + storage.Add(event) + } + + // Should only keep last 5 + events := storage.GetEvents(20) + require.Len(t, events, 5) + + // Verify we have the last 5 events (5-9) + for i, evt := range events { + assert.Equal(t, 5+i, evt.Data) + } +} + +func TestCaptureStorage_Subscribe_ReceivesEvents(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + defer storage.Close() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + subscription := storage.Subscribe(ctx) + + // Collect events in goroutine + var receivedEvents []*collector.Event + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + for evt := range subscription { + receivedEvents = append(receivedEvents, evt) + if len(receivedEvents) >= 2 { + cancel() + return + } + } + }() + + // Add events + event1 := &collector.Event{ + ID: uuid.Must(uuid.NewV7()), + Data: "event1", + Start: time.Now(), + End: time.Now(), + } + event2 := &collector.Event{ + ID: uuid.Must(uuid.NewV7()), + Data: "event2", + Start: time.Now(), + End: time.Now(), + } + + storage.Add(event1) + storage.Add(event2) + + wg.Wait() + + require.Len(t, receivedEvents, 2) + assert.Equal(t, "event1", receivedEvents[0].Data) + assert.Equal(t, "event2", receivedEvents[1].Data) +} + +func TestCaptureStorage_GetEvent_ByID(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + defer storage.Close() + + eventID := uuid.Must(uuid.NewV7()) + event := &collector.Event{ + ID: eventID, + Data: "test data", + Start: time.Now(), + End: time.Now(), + } + + storage.Add(event) + + // Should find the event + found, exists := storage.GetEvent(eventID) + assert.True(t, exists) + assert.Equal(t, eventID, found.ID) + assert.Equal(t, "test data", found.Data) + + // Should not find non-existent event + _, exists = storage.GetEvent(uuid.Must(uuid.NewV7())) + assert.False(t, exists) +} + +func TestCaptureStorage_Clear(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + defer storage.Close() + + event := &collector.Event{ + ID: uuid.Must(uuid.NewV7()), + Data: "test data", + Start: time.Now(), + End: time.Now(), + } + storage.Add(event) + + require.Len(t, storage.GetEvents(10), 1) + + storage.Clear() + + assert.Len(t, storage.GetEvents(10), 0) +} + +func TestCaptureStorage_ID(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + defer storage.Close() + + // Storage ID should not be the same as session ID + assert.NotEqual(t, uuid.Nil, storage.ID()) +} + +func TestCaptureStorage_SessionID(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + defer storage.Close() + + assert.Equal(t, sessionID, storage.SessionID()) +} diff --git a/collector/http_client.go b/collector/http_client.go index 64e26ae..49dbaf3 100644 --- a/collector/http_client.go +++ b/collector/http_client.go @@ -26,7 +26,11 @@ type HTTPClientOptions struct { NotifierOptions *NotifierOptions // EventCollector is an optional event collector for collecting requests as grouped events + // Deprecated: Use EventAggregator instead EventCollector *EventCollector + + // EventAggregator is the aggregator for collecting requests as grouped events + EventAggregator *EventAggregator } type HTTPClientRequestTransformer func(HTTPClientRequest) HTTPClientRequest @@ -44,9 +48,10 @@ func DefaultHTTPClientOptions() HTTPClientOptions { type HTTPClientCollector struct { buffer *RingBuffer[HTTPClientRequest] - options HTTPClientOptions - notifier *Notifier[HTTPClientRequest] - eventCollector *EventCollector + options HTTPClientOptions + notifier *Notifier[HTTPClientRequest] + eventCollector *EventCollector // Deprecated: use eventAggregator + eventAggregator *EventAggregator mu sync.RWMutex } @@ -64,9 +69,10 @@ func NewHTTPClientCollectorWithOptions(capacity uint64, options HTTPClientOption } collector := &HTTPClientCollector{ - options: options, - notifier: NewNotifierWithOptions[HTTPClientRequest](notifierOptions), - eventCollector: options.EventCollector, + options: options, + notifier: NewNotifierWithOptions[HTTPClientRequest](notifierOptions), + eventCollector: options.EventCollector, + eventAggregator: options.EventAggregator, } if capacity > 0 { collector.buffer = NewRingBuffer[HTTPClientRequest](capacity) @@ -121,6 +127,21 @@ type httpClientTransport struct { } func (t *httpClientTransport) RoundTrip(req *http.Request) (*http.Response, error) { + ctx := req.Context() + + // Check if we should capture this request (using EventAggregator) + // Default to true for backward compatibility when neither aggregator nor collector is set + shouldCapture := true + if t.collector.eventAggregator != nil { + shouldCapture = t.collector.eventAggregator.ShouldCapture(ctx) + } + // Note: EventCollector (deprecated) is always-capture, so no change needed + + // Early bailout if not capturing - just pass through + if !shouldCapture { + return t.next.RoundTrip(req) + } + // Generate a unique ID for this request id := generateID() @@ -154,8 +175,16 @@ func (t *httpClientTransport) RoundTrip(req *http.Request) (*http.Response, erro req.Body = body } - if t.collector.eventCollector != nil { - newCtx := t.collector.eventCollector.StartEvent(req.Context()) + // Start event tracking with EventAggregator + if t.collector.eventAggregator != nil { + newCtx := t.collector.eventAggregator.StartEvent(ctx) + defer func(req *HTTPClientRequest) { + t.collector.eventAggregator.EndEvent(newCtx, *req) + }(&httpReq) + + req = req.WithContext(newCtx) + } else if t.collector.eventCollector != nil { + newCtx := t.collector.eventCollector.StartEvent(ctx) defer func(req *HTTPClientRequest) { t.collector.eventCollector.EndEvent(newCtx, *req) }(&httpReq) diff --git a/collector/http_server.go b/collector/http_server.go index e4f0278..f3adc1b 100644 --- a/collector/http_server.go +++ b/collector/http_server.go @@ -8,8 +8,18 @@ import ( "net/http" "sync" "time" + + "github.com/gofrs/uuid" ) +// parseUUID parses a UUID string and returns the UUID or an error +func parseUUID(s string) (uuid.UUID, error) { + return uuid.FromString(s) +} + +// SessionCookieName is the name of the cookie used to identify capture sessions +const SessionCookieName = "devlog_session" + // HTTPServerOptions configures the HTTP server collector type HTTPServerOptions struct { // MaxBodySize is the maximum size in bytes of a single body @@ -32,7 +42,11 @@ type HTTPServerOptions struct { NotifierOptions *NotifierOptions // EventCollector is an optional event collector for collecting requests as grouped events + // Deprecated: Use EventAggregator instead EventCollector *EventCollector + + // EventAggregator is the aggregator for collecting requests as grouped events + EventAggregator *EventAggregator } type HTTPServerRequestTransformer func(HTTPServerRequest) HTTPServerRequest @@ -51,9 +65,10 @@ func DefaultHTTPServerOptions() HTTPServerOptions { type HTTPServerCollector struct { buffer *RingBuffer[HTTPServerRequest] - options HTTPServerOptions - notifier *Notifier[HTTPServerRequest] - eventCollector *EventCollector + options HTTPServerOptions + notifier *Notifier[HTTPServerRequest] + eventCollector *EventCollector // Deprecated: use eventAggregator + eventAggregator *EventAggregator mu sync.RWMutex } @@ -71,9 +86,10 @@ func NewHTTPServerCollectorWithOptions(capacity uint64, options HTTPServerOption } collector := &HTTPServerCollector{ - options: options, - notifier: NewNotifierWithOptions[HTTPServerRequest](notifierOptions), - eventCollector: options.EventCollector, + options: options, + notifier: NewNotifierWithOptions[HTTPServerRequest](notifierOptions), + eventCollector: options.EventCollector, + eventAggregator: options.EventAggregator, } if capacity > 0 { collector.buffer = NewRingBuffer[HTTPServerRequest](capacity) @@ -115,6 +131,30 @@ func (c *HTTPServerCollector) Middleware(next http.Handler) http.Handler { } } + ctx := r.Context() + + // Extract session cookie and add to context + if cookie, err := r.Cookie(SessionCookieName); err == nil { + if sessionID, err := parseUUID(cookie.Value); err == nil { + ctx = WithSessionID(ctx, sessionID) + r = r.WithContext(ctx) + } + } + + // Check if we should capture this request (using EventAggregator) + // Default to true for backward compatibility when neither aggregator nor collector is set + shouldCapture := true + if c.eventAggregator != nil { + shouldCapture = c.eventAggregator.ShouldCapture(ctx) + } + // Note: EventCollector (deprecated) is always-capture, so no change needed + + // Early bailout if not capturing (only applies when EventAggregator is active) + if !shouldCapture { + next.ServeHTTP(w, r) + return + } + // Generate a unique ID for this request id := generateID() @@ -156,8 +196,16 @@ func (c *HTTPServerCollector) Middleware(next http.Handler) http.Handler { collector: c, } - if c.eventCollector != nil { - newCtx := c.eventCollector.StartEvent(r.Context()) + // Start event tracking + if c.eventAggregator != nil { + newCtx := c.eventAggregator.StartEvent(ctx) + defer func(req *HTTPServerRequest) { + c.eventAggregator.EndEvent(newCtx, *req) + }(&httpReq) + + r = r.WithContext(newCtx) + } else if c.eventCollector != nil { + newCtx := c.eventCollector.StartEvent(ctx) defer func(req *HTTPServerRequest) { c.eventCollector.EndEvent(newCtx, *req) }(&httpReq) diff --git a/collector/http_server_test.go b/collector/http_server_test.go index e344961..c3f2077 100644 --- a/collector/http_server_test.go +++ b/collector/http_server_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -807,3 +808,201 @@ func TestHTTPServerCollector_MultipleHandlers(t *testing.T) { assert.True(t, capturedPaths["/api/users"], "Should have captured /api/users") assert.True(t, capturedPaths["/web/index"], "Should have captured /web/index") } + +func TestHTTPServerCollector_WithEventAggregator_GlobalMode(t *testing.T) { + // Create an EventAggregator with a GlobalMode storage + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeGlobal) + aggregator.RegisterStorage(storage) + + // Create a server collector with the EventAggregator + options := collector.DefaultHTTPServerOptions() + options.EventAggregator = aggregator + serverCollector := collector.NewHTTPServerCollectorWithOptions(100, options) + + // Create a simple handler + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Hello, World!")) + }) + + // Wrap the handler with our collector + wrappedHandler := serverCollector.Middleware(handler) + + // Create a test server + server := httptest.NewServer(wrappedHandler) + defer server.Close() + + // Make a request (without session cookie - GlobalMode should capture anyway) + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, server.URL+"/test", nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "Hello, World!", string(body)) + + // Verify the event was captured in the storage + events := storage.GetEvents(10) + require.Len(t, events, 1) + + // The event data should be an HTTPServerRequest + httpReq, ok := events[0].Data.(collector.HTTPServerRequest) + require.True(t, ok, "Event data should be HTTPServerRequest") + assert.Equal(t, http.MethodGet, httpReq.Method) + assert.Equal(t, "/test", httpReq.Path) + assert.Equal(t, http.StatusOK, httpReq.StatusCode) +} + +func TestHTTPServerCollector_WithEventAggregator_SessionMode_NoMatch(t *testing.T) { + // Create an EventAggregator with a SessionMode storage + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) + aggregator.RegisterStorage(storage) + + // Create a server collector with the EventAggregator + options := collector.DefaultHTTPServerOptions() + options.EventAggregator = aggregator + serverCollector := collector.NewHTTPServerCollectorWithOptions(100, options) + + // Create a simple handler + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Hello, World!")) + }) + + // Wrap the handler with our collector + wrappedHandler := serverCollector.Middleware(handler) + + // Create a test server + server := httptest.NewServer(wrappedHandler) + defer server.Close() + + // Make a request without a session cookie (SessionMode should NOT capture) + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, server.URL+"/test", nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "Hello, World!", string(body)) + + // Verify no events were captured (session doesn't match) + events := storage.GetEvents(10) + assert.Len(t, events, 0) +} + +func TestHTTPServerCollector_WithEventAggregator_SessionMode_Match(t *testing.T) { + // Create an EventAggregator with a SessionMode storage + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + sessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) + aggregator.RegisterStorage(storage) + + // Create a server collector with the EventAggregator + options := collector.DefaultHTTPServerOptions() + options.EventAggregator = aggregator + serverCollector := collector.NewHTTPServerCollectorWithOptions(100, options) + + // Create a simple handler + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Hello, World!")) + }) + + // Wrap the handler with our collector + wrappedHandler := serverCollector.Middleware(handler) + + // Create a test server + server := httptest.NewServer(wrappedHandler) + defer server.Close() + + // Make a request WITH the session cookie (SessionMode should capture) + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, server.URL+"/test", nil) + require.NoError(t, err) + req.AddCookie(&http.Cookie{ + Name: collector.SessionCookieName, + Value: sessionID.String(), + }) + + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "Hello, World!", string(body)) + + // Verify the event was captured + events := storage.GetEvents(10) + require.Len(t, events, 1) + + // The event data should be an HTTPServerRequest + httpReq, ok := events[0].Data.(collector.HTTPServerRequest) + require.True(t, ok, "Event data should be HTTPServerRequest") + assert.Equal(t, http.MethodGet, httpReq.Method) + assert.Equal(t, "/test", httpReq.Path) +} + +func TestHTTPServerCollector_WithEventAggregator_NoStorage(t *testing.T) { + // Create an EventAggregator with NO storage registered + aggregator := collector.NewEventAggregator() + defer aggregator.Close() + + // Create a server collector with the EventAggregator + options := collector.DefaultHTTPServerOptions() + options.EventAggregator = aggregator + serverCollector := collector.NewHTTPServerCollectorWithOptions(100, options) + + // Create a simple handler + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte("Hello, World!")) + }) + + // Wrap the handler with our collector + wrappedHandler := serverCollector.Middleware(handler) + + // Create a test server + server := httptest.NewServer(wrappedHandler) + defer server.Close() + + // Make a request (no storage means ShouldCapture returns false, early bailout) + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, server.URL+"/test", nil) + require.NoError(t, err) + + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, "Hello, World!", string(body)) + + // Verify no requests were captured by the server collector's buffer + // (early bailout should prevent capture) + requests := serverCollector.GetRequests(10) + assert.Len(t, requests, 0) +} diff --git a/collector/log_collector.go b/collector/log_collector.go index 48914f7..d550e3a 100644 --- a/collector/log_collector.go +++ b/collector/log_collector.go @@ -9,9 +9,10 @@ import ( ) type LogCollector struct { - buffer *RingBuffer[slog.Record] - notifier *Notifier[slog.Record] - eventCollector *EventCollector + buffer *RingBuffer[slog.Record] + notifier *Notifier[slog.Record] + eventCollector *EventCollector // Deprecated: use eventAggregator + eventAggregator *EventAggregator } func (c *LogCollector) Collect(ctx context.Context, record slog.Record) { @@ -19,7 +20,9 @@ func (c *LogCollector) Collect(ctx context.Context, record slog.Record) { c.buffer.Add(record) } c.notifier.Notify(record) - if c.eventCollector != nil { + if c.eventAggregator != nil { + c.eventAggregator.CollectEvent(ctx, record) + } else if c.eventCollector != nil { c.eventCollector.CollectEvent(ctx, record) } } @@ -49,7 +52,11 @@ type LogOptions struct { NotifierOptions *NotifierOptions // EventCollector is an optional event collector for collecting logs as grouped events + // Deprecated: Use EventAggregator instead EventCollector *EventCollector + + // EventAggregator is the aggregator for collecting logs as grouped events + EventAggregator *EventAggregator } func NewLogCollectorWithOptions(capacity uint64, options LogOptions) *LogCollector { @@ -59,8 +66,9 @@ func NewLogCollectorWithOptions(capacity uint64, options LogOptions) *LogCollect } collector := &LogCollector{ - notifier: NewNotifierWithOptions[slog.Record](notifierOptions), - eventCollector: options.EventCollector, + notifier: NewNotifierWithOptions[slog.Record](notifierOptions), + eventCollector: options.EventCollector, + eventAggregator: options.EventAggregator, } if capacity > 0 { collector.buffer = NewRingBuffer[slog.Record](capacity) diff --git a/collector/session_context.go b/collector/session_context.go new file mode 100644 index 0000000..929e4e4 --- /dev/null +++ b/collector/session_context.go @@ -0,0 +1,25 @@ +package collector + +import ( + "context" + + "github.com/gofrs/uuid" +) + +type sessionIDKeyType struct{} + +var sessionIDKey = sessionIDKeyType{} + +// WithSessionID returns a new context with the session ID added. +func WithSessionID(ctx context.Context, sessionID uuid.UUID) context.Context { + return context.WithValue(ctx, sessionIDKey, sessionID) +} + +// SessionIDFromContext retrieves the session ID from the context. +// Returns the session ID and true if found, or uuid.Nil and false if not set. +func SessionIDFromContext(ctx context.Context) (uuid.UUID, bool) { + if sessionID, ok := ctx.Value(sessionIDKey).(uuid.UUID); ok { + return sessionID, true + } + return uuid.Nil, false +} diff --git a/collector/session_context_test.go b/collector/session_context_test.go new file mode 100644 index 0000000..ae47781 --- /dev/null +++ b/collector/session_context_test.go @@ -0,0 +1,41 @@ +package collector_test + +import ( + "context" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + + "github.com/networkteam/devlog/collector" +) + +func TestWithSessionID_AddsToContext(t *testing.T) { + ctx := context.Background() + sessionID := uuid.Must(uuid.NewV4()) + + newCtx := collector.WithSessionID(ctx, sessionID) + + retrievedID, ok := collector.SessionIDFromContext(newCtx) + assert.True(t, ok) + assert.Equal(t, sessionID, retrievedID) +} + +func TestSessionIDFromContext_NotSet(t *testing.T) { + ctx := context.Background() + + retrievedID, ok := collector.SessionIDFromContext(ctx) + + assert.False(t, ok) + assert.Equal(t, uuid.Nil, retrievedID) +} + +func TestSessionIDFromContext_Set(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + ctx := collector.WithSessionID(context.Background(), sessionID) + + retrievedID, ok := collector.SessionIDFromContext(ctx) + + assert.True(t, ok) + assert.Equal(t, sessionID, retrievedID) +} diff --git a/dashboard/handler.go b/dashboard/handler.go index 5dd913f..340cdaf 100644 --- a/dashboard/handler.go +++ b/dashboard/handler.go @@ -2,10 +2,13 @@ package dashboard import ( "context" + "encoding/json" "fmt" "net/http" "net/url" "slices" + "sync" + "time" "github.com/a-h/templ" "github.com/gofrs/uuid" @@ -15,39 +18,86 @@ import ( "github.com/networkteam/devlog/dashboard/views" ) +// DefaultStorageCapacity is the default number of events per storage +const DefaultStorageCapacity uint64 = 1000 + +// DefaultSessionIdleTimeout is the default time before an inactive session is cleaned up +const DefaultSessionIdleTimeout = 30 * time.Second + +// sessionState tracks a user's capture session +type sessionState struct { + storageID uuid.UUID + lastActive time.Time +} + type Handler struct { - eventCollector *collector.EventCollector + eventAggregator *collector.EventAggregator + + // Session management + sessions map[uuid.UUID]*sessionState // sessionID -> sessionState + sessionsMu sync.RWMutex - pathPrefix string - truncateAfter uint64 + pathPrefix string + truncateAfter uint64 + storageCapacity uint64 + idleTimeout time.Duration + cleanupCtx context.Context + cleanupCtxCancel context.CancelFunc mux http.Handler } type HandlerOptions struct { - EventCollector *collector.EventCollector + // EventAggregator is the aggregator for collecting events + EventAggregator *collector.EventAggregator // PathPrefix where the Handler is mounted (e.g. "/_devlog"), can be left empty if the Handler is at the root ("/"). PathPrefix string - // TruncateAfter is the maximum number of events to show in the event list and dashboard. If 0 or larger than the event collector capacity, it will use the collector's capacity. + // TruncateAfter is the maximum number of events to show in the event list and dashboard. TruncateAfter uint64 + // StorageCapacity is the number of events per user storage. If 0, DefaultStorageCapacity is used. + StorageCapacity uint64 + // SessionIdleTimeout is how long to wait after SSE disconnect before cleaning up. If 0, DefaultSessionIdleTimeout is used. + SessionIdleTimeout time.Duration } func NewHandler(options HandlerOptions) *Handler { mux := http.NewServeMux() - if options.TruncateAfter == 0 || options.TruncateAfter > options.EventCollector.Capacity() { - options.TruncateAfter = options.EventCollector.Capacity() + + storageCapacity := options.StorageCapacity + if storageCapacity == 0 { + storageCapacity = DefaultStorageCapacity + } + + truncateAfter := options.TruncateAfter + if truncateAfter == 0 || truncateAfter > storageCapacity { + truncateAfter = storageCapacity + } + + idleTimeout := options.SessionIdleTimeout + if idleTimeout == 0 { + idleTimeout = DefaultSessionIdleTimeout } + cleanupCtx, cleanupCtxCancel := context.WithCancel(context.Background()) + handler := &Handler{ - eventCollector: options.EventCollector, - truncateAfter: options.TruncateAfter, + eventAggregator: options.EventAggregator, + sessions: make(map[uuid.UUID]*sessionState), + truncateAfter: truncateAfter, + storageCapacity: storageCapacity, + idleTimeout: idleTimeout, + cleanupCtx: cleanupCtx, + cleanupCtxCancel: cleanupCtxCancel, pathPrefix: options.PathPrefix, - mux: setHandlerOptions(options, mux), + mux: setHandlerOptions(options, truncateAfter, mux), } + // Start cleanup goroutine + go handler.sessionCleanupLoop() + mux.HandleFunc("GET /{$}", handler.root) mux.HandleFunc("GET /event-list", handler.getEventList) mux.HandleFunc("DELETE /event-list", handler.clearEventList) @@ -56,17 +106,23 @@ func NewHandler(options HandlerOptions) *Handler { mux.HandleFunc("GET /download/request-body/{eventId}", handler.downloadRequestBody) mux.HandleFunc("GET /download/response-body/{eventId}", handler.downloadResponseBody) + // Capture control endpoints + mux.HandleFunc("POST /capture/start", handler.captureStart) + mux.HandleFunc("POST /capture/stop", handler.captureStop) + mux.HandleFunc("POST /capture/mode", handler.captureMode) + mux.HandleFunc("GET /capture/status", handler.captureStatus) + mux.Handle("/static/", http.StripPrefix("/static", http.FileServerFS(static.Assets))) return handler } -func setHandlerOptions(options HandlerOptions, next http.Handler) http.Handler { +func setHandlerOptions(options HandlerOptions, truncateAfter uint64, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ctx = views.WithHandlerOptions(ctx, views.HandlerOptions{ PathPrefix: options.PathPrefix, - TruncateAfter: options.TruncateAfter, + TruncateAfter: truncateAfter, }) r = r.WithContext(ctx) @@ -78,36 +134,178 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.mux.ServeHTTP(w, r) } +// Close shuts down the handler and releases resources +func (h *Handler) Close() { + h.cleanupCtxCancel() + + // Unregister all storages + h.sessionsMu.Lock() + for sessionID, state := range h.sessions { + if storage := h.eventAggregator.GetStorage(state.storageID); storage != nil { + storage.Close() + } + h.eventAggregator.UnregisterStorage(state.storageID) + delete(h.sessions, sessionID) + } + h.sessionsMu.Unlock() +} + +// sessionCleanupLoop periodically checks for idle sessions and cleans them up +func (h *Handler) sessionCleanupLoop() { + ticker := time.NewTicker(h.idleTimeout / 2) + defer ticker.Stop() + + for { + select { + case <-h.cleanupCtx.Done(): + return + case <-ticker.C: + h.cleanupIdleSessions() + } + } +} + +func (h *Handler) cleanupIdleSessions() { + now := time.Now() + + h.sessionsMu.Lock() + defer h.sessionsMu.Unlock() + + for sessionID, state := range h.sessions { + if now.Sub(state.lastActive) > h.idleTimeout { + // Clean up this session + if storage := h.eventAggregator.GetStorage(state.storageID); storage != nil { + storage.Close() + } + h.eventAggregator.UnregisterStorage(state.storageID) + delete(h.sessions, sessionID) + } + } +} + +// getSessionID extracts the session ID from the request cookie +func (h *Handler) getSessionID(r *http.Request) (uuid.UUID, bool) { + cookie, err := r.Cookie(collector.SessionCookieName) + if err != nil { + return uuid.Nil, false + } + sessionID, err := uuid.FromString(cookie.Value) + if err != nil { + return uuid.Nil, false + } + return sessionID, true +} + +// getOrCreateSessionID gets existing session ID or creates a new one +func (h *Handler) getOrCreateSessionID(w http.ResponseWriter, r *http.Request) uuid.UUID { + if sessionID, ok := h.getSessionID(r); ok { + return sessionID + } + + // Create new session ID + sessionID := uuid.Must(uuid.NewV4()) + h.setSessionCookie(w, sessionID) + return sessionID +} + +// setSessionCookie sets the session cookie +func (h *Handler) setSessionCookie(w http.ResponseWriter, sessionID uuid.UUID) { + http.SetCookie(w, &http.Cookie{ + Name: collector.SessionCookieName, + Value: sessionID.String(), + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + }) +} + +// clearSessionCookie clears the session cookie +func (h *Handler) clearSessionCookie(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: collector.SessionCookieName, + Value: "", + Path: "/", + MaxAge: -1, + HttpOnly: true, + }) +} + +// getSessionStorage returns the storage for a session, or nil if not found +func (h *Handler) getSessionStorage(sessionID uuid.UUID) *collector.CaptureStorage { + h.sessionsMu.RLock() + state, exists := h.sessions[sessionID] + h.sessionsMu.RUnlock() + + if !exists { + return nil + } + + storage := h.eventAggregator.GetStorage(state.storageID) + if storage == nil { + return nil + } + + return storage.(*collector.CaptureStorage) +} + +// updateSessionActivity updates the last active time for a session +func (h *Handler) updateSessionActivity(sessionID uuid.UUID) { + h.sessionsMu.Lock() + if state, exists := h.sessions[sessionID]; exists { + state.lastActive = time.Now() + } + h.sessionsMu.Unlock() +} + func (h *Handler) root(w http.ResponseWriter, r *http.Request) { - idStr := r.URL.Query().Get("id") + sessionID, _ := h.getSessionID(r) + storage := h.getSessionStorage(sessionID) + var selectedEvent *collector.Event - if idStr != "" { + idStr := r.URL.Query().Get("id") + if idStr != "" && storage != nil { eventID, err := uuid.FromString(idStr) if err != nil { http.Error(w, "Invalid event id", http.StatusBadRequest) return } - event, exists := h.eventCollector.GetEvent(eventID) + event, exists := storage.GetEvent(eventID) if !exists { - http.Redirect(w, r, fmt.Sprintf("%s/", h.pathPrefix), http.StatusTemporaryRedirect) // TODO Build correct URL + http.Redirect(w, r, fmt.Sprintf("%s/", h.pathPrefix), http.StatusTemporaryRedirect) return - } else { - selectedEvent = event } + selectedEvent = event } - recentEvents := h.loadRecentEvents() + var recentEvents []*collector.Event + captureActive := false + captureMode := "session" + if storage != nil { + recentEvents = h.loadRecentEvents(storage) + captureActive = true + if storage.CaptureMode() == collector.CaptureModeGlobal { + captureMode = "global" + } + } templ.Handler( views.Dashboard(views.DashboardProps{ SelectedEvent: selectedEvent, Events: recentEvents, + CaptureActive: captureActive, + CaptureMode: captureMode, }), ).ServeHTTP(w, r) } func (h *Handler) getEventList(w http.ResponseWriter, r *http.Request) { - recentEvents := h.loadRecentEvents() + sessionID, _ := h.getSessionID(r) + storage := h.getSessionStorage(sessionID) + + var recentEvents []*collector.Event + if storage != nil { + recentEvents = h.loadRecentEvents(storage) + } selectedStr := r.URL.Query().Get("selected") var selectedEventID *uuid.UUID @@ -127,7 +325,10 @@ func (h *Handler) getEventList(w http.ResponseWriter, r *http.Request) { } func (h *Handler) clearEventList(w http.ResponseWriter, r *http.Request) { - h.eventCollector.Clear() + sessionID, _ := h.getSessionID(r) + if storage := h.getSessionStorage(sessionID); storage != nil { + storage.Clear() + } // Check if there's an id parameter in the current URL that needs to be removed to unselect an event currentURL, _ := url.Parse(r.Header.Get("HX-Current-URL")) @@ -151,6 +352,13 @@ func (h *Handler) clearEventList(w http.ResponseWriter, r *http.Request) { } func (h *Handler) getEventDetails(w http.ResponseWriter, r *http.Request) { + sessionID, _ := h.getSessionID(r) + storage := h.getSessionStorage(sessionID) + if storage == nil { + http.Error(w, "No capture session active", http.StatusNotFound) + return + } + idStr := r.PathValue("eventId") eventID, err := uuid.FromString(idStr) if err != nil { @@ -158,7 +366,7 @@ func (h *Handler) getEventDetails(w http.ResponseWriter, r *http.Request) { return } - event, exists := h.eventCollector.GetEvent(eventID) + event, exists := storage.GetEvent(eventID) if !exists { http.Error(w, "Event not found", http.StatusNotFound) return @@ -171,6 +379,21 @@ func (h *Handler) getEventDetails(w http.ResponseWriter, r *http.Request) { // getEventsSSE handles SSE connections for real-time log updates func (h *Handler) getEventsSSE(w http.ResponseWriter, r *http.Request) { + sessionID, hasSession := h.getSessionID(r) + if !hasSession { + http.Error(w, "No session cookie", http.StatusUnauthorized) + return + } + + storage := h.getSessionStorage(sessionID) + if storage == nil { + http.Error(w, "No capture session active", http.StatusNotFound) + return + } + + // Update activity for this session + h.updateSessionActivity(sessionID) + // Set SSE headers w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") @@ -181,20 +404,14 @@ func (h *Handler) getEventsSSE(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithCancel(r.Context()) defer cancel() - // Monitor for client disconnect - go func() { - <-ctx.Done() - // Context was canceled, connection is closed - }() - - // Create a notification channel for new logs - eventCh := h.eventCollector.Subscribe(ctx) + // Create a notification channel for new events from the user's storage + eventCh := storage.Subscribe(ctx) // Send a keep-alive message initially to ensure the connection is established fmt.Fprintf(w, "event: keepalive\ndata: connected\n\n") w.(http.Flusher).Flush() - // Listen for new logs and send them as SSE events + // Listen for new events and send them as SSE events for { select { case <-ctx.Done(): @@ -204,11 +421,14 @@ func (h *Handler) getEventsSSE(w http.ResponseWriter, r *http.Request) { return // Channel closed } + // Update activity on each event + h.updateSessionActivity(sessionID) + // Send as SSE event fmt.Fprintf(w, "event: new-event\n") fmt.Fprintf(w, "data: ") - views.EventListItem(&event, nil).Render(ctx, w) + views.EventListItem(event, nil).Render(ctx, w) fmt.Fprintf(w, "\n\n") @@ -217,8 +437,8 @@ func (h *Handler) getEventsSSE(w http.ResponseWriter, r *http.Request) { } } -func (h *Handler) loadRecentEvents() []*collector.Event { - recentEvents := h.eventCollector.GetEvents(h.truncateAfter) +func (h *Handler) loadRecentEvents(storage *collector.CaptureStorage) []*collector.Event { + recentEvents := storage.GetEvents(h.truncateAfter) slices.Reverse(recentEvents) return recentEvents @@ -226,6 +446,13 @@ func (h *Handler) loadRecentEvents() []*collector.Event { // downloadRequestBody handles downloading the request body for an event func (h *Handler) downloadRequestBody(w http.ResponseWriter, r *http.Request) { + sessionID, _ := h.getSessionID(r) + storage := h.getSessionStorage(sessionID) + if storage == nil { + http.Error(w, "No capture session active", http.StatusNotFound) + return + } + idStr := r.PathValue("eventId") eventID, err := uuid.FromString(idStr) if err != nil { @@ -233,7 +460,7 @@ func (h *Handler) downloadRequestBody(w http.ResponseWriter, r *http.Request) { return } - event, exists := h.eventCollector.GetEvent(eventID) + event, exists := storage.GetEvent(eventID) if !exists { http.Error(w, "Event not found", http.StatusNotFound) return @@ -274,6 +501,13 @@ func (h *Handler) downloadRequestBody(w http.ResponseWriter, r *http.Request) { // downloadResponseBody handles downloading the response body for an event func (h *Handler) downloadResponseBody(w http.ResponseWriter, r *http.Request) { + sessionID, _ := h.getSessionID(r) + storage := h.getSessionStorage(sessionID) + if storage == nil { + http.Error(w, "No capture session active", http.StatusNotFound) + return + } + idStr := r.PathValue("eventId") eventID, err := uuid.FromString(idStr) if err != nil { @@ -281,7 +515,7 @@ func (h *Handler) downloadResponseBody(w http.ResponseWriter, r *http.Request) { return } - event, exists := h.eventCollector.GetEvent(eventID) + event, exists := storage.GetEvent(eventID) if !exists { http.Error(w, "Event not found", http.StatusNotFound) return @@ -319,3 +553,145 @@ func (h *Handler) downloadResponseBody(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) w.Write(body) } + +// Capture control endpoints + +// CaptureStatusResponse is the response for GET /capture/status +type CaptureStatusResponse struct { + Active bool `json:"active"` + Mode string `json:"mode,omitempty"` // "session" or "global" +} + +// captureStart handles POST /capture/start - creates a new capture session +func (h *Handler) captureStart(w http.ResponseWriter, r *http.Request) { + // Get or create session ID + sessionID := h.getOrCreateSessionID(w, r) + + // Parse mode from request body (default to session mode) + mode := collector.CaptureModeSession + if r.FormValue("mode") == "global" { + mode = collector.CaptureModeGlobal + } + + // Check if already capturing + h.sessionsMu.Lock() + if state, exists := h.sessions[sessionID]; exists { + h.sessionsMu.Unlock() + // Already capturing, get current mode from storage + if storage := h.eventAggregator.GetStorage(state.storageID); storage != nil { + mode = storage.(*collector.CaptureStorage).CaptureMode() + } + h.respondWithCaptureState(w, r, true, mode) + return + } + + // Create new storage + storage := collector.NewCaptureStorage(sessionID, h.storageCapacity, mode) + + // Register with aggregator + h.eventAggregator.RegisterStorage(storage) + + // Track the session + h.sessions[sessionID] = &sessionState{ + storageID: storage.ID(), + lastActive: time.Now(), + } + h.sessionsMu.Unlock() + + h.respondWithCaptureState(w, r, true, mode) +} + +// captureStop handles POST /capture/stop - stops capture and removes storage +func (h *Handler) captureStop(w http.ResponseWriter, r *http.Request) { + sessionID, hasSession := h.getSessionID(r) + if !hasSession { + h.respondWithCaptureState(w, r, false, collector.CaptureModeSession) + return + } + + h.sessionsMu.Lock() + state, exists := h.sessions[sessionID] + if exists { + // Close and unregister storage + if storage := h.eventAggregator.GetStorage(state.storageID); storage != nil { + storage.Close() + } + h.eventAggregator.UnregisterStorage(state.storageID) + delete(h.sessions, sessionID) + } + h.sessionsMu.Unlock() + + h.respondWithCaptureState(w, r, false, collector.CaptureModeSession) +} + +// captureMode handles POST /capture/mode - changes capture mode +func (h *Handler) captureMode(w http.ResponseWriter, r *http.Request) { + sessionID, hasSession := h.getSessionID(r) + if !hasSession { + http.Error(w, "No session cookie", http.StatusUnauthorized) + return + } + + storage := h.getSessionStorage(sessionID) + if storage == nil { + http.Error(w, "No capture session active", http.StatusNotFound) + return + } + + // Parse mode from request + modeStr := r.FormValue("mode") + var mode collector.CaptureMode + switch modeStr { + case "session": + mode = collector.CaptureModeSession + case "global": + mode = collector.CaptureModeGlobal + default: + http.Error(w, "Invalid mode, must be 'session' or 'global'", http.StatusBadRequest) + return + } + + storage.SetCaptureMode(mode) + + h.respondWithCaptureState(w, r, true, mode) +} + +// captureStatus handles GET /capture/status - returns current capture state +func (h *Handler) captureStatus(w http.ResponseWriter, r *http.Request) { + sessionID, hasSession := h.getSessionID(r) + if !hasSession { + h.respondWithCaptureState(w, r, false, collector.CaptureModeSession) + return + } + + storage := h.getSessionStorage(sessionID) + if storage == nil { + h.respondWithCaptureState(w, r, false, collector.CaptureModeSession) + return + } + + h.respondWithCaptureState(w, r, true, storage.CaptureMode()) +} + +// respondWithCaptureState responds with capture state as HTML for HTMX or JSON for API +func (h *Handler) respondWithCaptureState(w http.ResponseWriter, r *http.Request, active bool, mode collector.CaptureMode) { + modeStr := "session" + if mode == collector.CaptureModeGlobal { + modeStr = "global" + } + + // Check if this is an HTMX request + if r.Header.Get("HX-Request") == "true" { + templ.Handler( + views.CaptureControls(views.CaptureState{ + Active: active, + Mode: modeStr, + }), + ).ServeHTTP(w, r) + return + } + + // Return JSON for API compatibility + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(CaptureStatusResponse{Active: active, Mode: modeStr}) +} diff --git a/dashboard/static/main.css b/dashboard/static/main.css index c80963c..d055778 100644 --- a/dashboard/static/main.css +++ b/dashboard/static/main.css @@ -25,6 +25,7 @@ --color-neutral-500: oklch(55.6% 0 0); --color-neutral-600: oklch(43.9% 0 0); --color-neutral-700: oklch(37.1% 0 0); + --color-neutral-900: oklch(20.5% 0 0); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; @@ -320,6 +321,9 @@ .grow { flex-grow: 1; } + .cursor-not-allowed { + cursor: not-allowed; + } .cursor-pointer { cursor: pointer; } @@ -440,6 +444,9 @@ .bg-neutral-200 { background-color: var(--color-neutral-200); } + .bg-neutral-900 { + background-color: var(--color-neutral-900); + } .bg-orange-400 { background-color: var(--color-orange-400); } @@ -571,6 +578,9 @@ .text-white { color: var(--color-white); } + .opacity-50 { + opacity: 50%; + } .ring { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); diff --git a/dashboard/views/button.go b/dashboard/views/button.go index 0704ab8..2b1565d 100644 --- a/dashboard/views/button.go +++ b/dashboard/views/button.go @@ -6,6 +6,7 @@ type ButtonVariant string type ButtonSize string const ( + ButtonVariantDefault ButtonVariant = "" ButtonVariantOutline ButtonVariant = "outline" ButtonVariantSecondary ButtonVariant = "secondary" @@ -14,9 +15,10 @@ const ( ) type ButtonProps struct { - Variant ButtonVariant - Size ButtonSize - Class string + Variant ButtonVariant + Size ButtonSize + Class string + Disabled bool } func buttonClasses(props ButtonProps) string { diff --git a/dashboard/views/dashboard.templ b/dashboard/views/dashboard.templ index 9e7b927..6e51282 100644 --- a/dashboard/views/dashboard.templ +++ b/dashboard/views/dashboard.templ @@ -1,21 +1,23 @@ package views import ( - "github.com/networkteam/devlog/collector" + "github.com/networkteam/devlog/collector" ) type DashboardProps struct { - SelectedEvent *collector.Event - Events []*collector.Event + SelectedEvent *collector.Event + Events []*collector.Event + CaptureActive bool + CaptureMode string // "session" or "global" } templ Dashboard(props DashboardProps) { - {{ eventListProps := EventListProps{Events: props.Events} }} - if props.SelectedEvent != nil { - {{ eventListProps.SelectedEventID = &props.SelectedEvent.ID }} - } - - @Layout() { + {{ eventListProps := EventListProps{Events: props.Events} }} + if props.SelectedEvent != nil { + {{ eventListProps.SelectedEventID = &props.SelectedEvent.ID }} + } + {{ capture := CaptureState{Active: props.CaptureActive, Mode: props.CaptureMode} }} + @Layout(capture) { @SplitLayout(EventList(eventListProps), EventDetailContainer(props.SelectedEvent)) } } diff --git a/dashboard/views/dashboard_templ.go b/dashboard/views/dashboard_templ.go index f714c5c..ef381da 100644 --- a/dashboard/views/dashboard_templ.go +++ b/dashboard/views/dashboard_templ.go @@ -15,6 +15,8 @@ import ( type DashboardProps struct { SelectedEvent *collector.Event Events []*collector.Event + CaptureActive bool + CaptureMode string // "session" or "global" } func Dashboard(props DashboardProps) templ.Component { @@ -42,6 +44,7 @@ func Dashboard(props DashboardProps) templ.Component { if props.SelectedEvent != nil { eventListProps.SelectedEventID = &props.SelectedEvent.ID } + capture := CaptureState{Active: props.CaptureActive, Mode: props.CaptureMode} templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) @@ -60,7 +63,7 @@ func Dashboard(props DashboardProps) templ.Component { } return nil }) - templ_7745c5c3_Err = Layout().Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Layout(capture).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/dashboard/views/header.templ b/dashboard/views/header.templ index d319dfe..1eacce0 100644 --- a/dashboard/views/header.templ +++ b/dashboard/views/header.templ @@ -2,28 +2,33 @@ package views import "fmt" -templ Header() { +type CaptureState struct { + Active bool + Mode string // "session" or "global" +} + +templ Header(capture CaptureState) {
-

devlog

+

devlog

-
+ @CaptureControls(capture) +
- @@ -32,16 +37,110 @@ templ Header() {
} +templ CaptureControls(capture CaptureState) { + {{ mode := capture.Mode }} + if mode == "" { + {{ mode = "session" }} + } +
+ + + @CaptureMode(mode, capture.Active) +
+} + +templ CaptureMode(mode string, capturing bool) { +
+ if capturing { + + + } else { + + + } +
+} + +templ iconPlay() { + + + +} + +templ iconStop() { + + + +} + templ iconDeleteRow() { - - - Delete Row Streamline Icon: https://streamlinehq.com - - - - - - - - + + + Delete Row Streamline Icon: https://streamlinehq.com + + + + + + + + } \ No newline at end of file diff --git a/dashboard/views/header_templ.go b/dashboard/views/header_templ.go index ff7a21d..3186c46 100644 --- a/dashboard/views/header_templ.go +++ b/dashboard/views/header_templ.go @@ -10,7 +10,12 @@ import templruntime "github.com/a-h/templ/runtime" import "fmt" -func Header() templ.Component { +type CaptureState struct { + Active bool + Mode string // "session" or "global" +} + +func Header(capture CaptureState) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -31,7 +36,15 @@ func Header() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

devlog

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

devlog

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = CaptureControls(capture).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -44,7 +57,7 @@ func Header() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -86,7 +99,7 @@ func Header() templ.Component { }) } -func iconDeleteRow() templ.Component { +func CaptureControls(capture CaptureState) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -107,7 +120,385 @@ func iconDeleteRow() templ.Component { templ_7745c5c3_Var5 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Delete Row Streamline Icon: https://streamlinehq.com ") + mode := capture.Mode + if mode == "" { + mode = "session" + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var7 = []any{buttonClasses( + ButtonProps{ + Variant: ButtonVariantDefault, + Size: ButtonSizeIcon, + Disabled: capture.Active, + })} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var10 = []any{buttonClasses( + ButtonProps{ + Variant: ButtonVariantOutline, + Size: ButtonSizeIcon, + Disabled: !capture.Active, + })} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = CaptureMode(mode, capture.Active).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func CaptureMode(mode string, capturing bool) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if capturing { + var templ_7745c5c3_Var14 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-neutral-900 text-white", mode == "session"), templ.KV("hover:bg-neutral-100", mode != "session")} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var17 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-neutral-300", templ.KV("bg-neutral-900 text-white", mode == "global"), templ.KV("hover:bg-neutral-100", mode != "global")} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + var templ_7745c5c3_Var20 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-neutral-900 text-white", mode == "session"), templ.KV("hover:bg-neutral-100", mode != "session")} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var22 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-neutral-300", templ.KV("bg-neutral-900 text-white", mode == "global"), templ.KV("hover:bg-neutral-100", mode != "global")} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func iconPlay() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var24 := templ.GetChildren(ctx) + if templ_7745c5c3_Var24 == nil { + templ_7745c5c3_Var24 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func iconStop() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var25 := templ.GetChildren(ctx) + if templ_7745c5c3_Var25 == nil { + templ_7745c5c3_Var25 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func iconDeleteRow() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var26 := templ.GetChildren(ctx) + if templ_7745c5c3_Var26 == nil { + templ_7745c5c3_Var26 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Delete Row Streamline Icon: https://streamlinehq.com ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/dashboard/views/layout.templ b/dashboard/views/layout.templ index 1a99d6c..06f8e7f 100644 --- a/dashboard/views/layout.templ +++ b/dashboard/views/layout.templ @@ -13,21 +13,21 @@ func cachebuster(path string) string { return path } -templ Layout() { +templ Layout(capture CaptureState) { devlog - - + + - + @chromaStyles() - @Header() + @Header(capture)
{ children... }
diff --git a/dashboard/views/layout_templ.go b/dashboard/views/layout_templ.go index 9bbd854..de75dad 100644 --- a/dashboard/views/layout_templ.go +++ b/dashboard/views/layout_templ.go @@ -21,7 +21,7 @@ func cachebuster(path string) string { return path } -func Layout() templ.Component { +func Layout(capture CaptureState) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -49,7 +49,7 @@ func Layout() templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s/static/favicon.ico", MustGetHandlerOptions(ctx).PathPrefix)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/layout.templ`, Line: 21, Col: 121} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/layout.templ`, Line: 21, Col: 122} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -62,7 +62,7 @@ func Layout() templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(cachebuster(fmt.Sprintf("%s/static/main.css", MustGetHandlerOptions(ctx).PathPrefix))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/layout.templ`, Line: 22, Col: 117} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/layout.templ`, Line: 22, Col: 118} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -80,7 +80,7 @@ func Layout() templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = Header().Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = Header(capture).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/devlog.go b/devlog.go index a140ceb..1f3a005 100644 --- a/devlog.go +++ b/devlog.go @@ -14,7 +14,9 @@ type Instance struct { httpClientCollector *collector.HTTPClientCollector httpServerCollector *collector.HTTPServerCollector dbQueryCollector *collector.DBQueryCollector - eventCollector *collector.EventCollector + eventAggregator *collector.EventAggregator + + dashboardHandler *dashboard.Handler } func (i *Instance) Close() { @@ -22,7 +24,10 @@ func (i *Instance) Close() { i.httpClientCollector.Close() i.httpServerCollector.Close() i.dbQueryCollector.Close() - i.eventCollector.Close() + if i.dashboardHandler != nil { + i.dashboardHandler.Close() + } + i.eventAggregator.Close() } type Options struct { @@ -53,13 +58,6 @@ type Options struct { // DBQueryOptions are the options for the database query collector. // Default: nil, will use collector.DefaultDBQueryOptions() DBQueryOptions *collector.DBQueryOptions - - // EventCapacity is the maximum number of events to keep. - // Default: 1000 - EventCapacity uint64 - // EventOptions are the options for the event collector. - // Default: nil, will use collector.DefaultEventOptions() - EventOptions *collector.EventOptions } // New creates a new devlog dashboard with default options. @@ -69,48 +67,43 @@ func New() *Instance { // NewWithOptions creates a new devlog dashboard with the specified options. // Default options are the zero value of Options. +// +// By default, no events are collected until a user starts a capture session +// through the dashboard. Events are collected per-user with isolation. func NewWithOptions(options Options) *Instance { - if options.EventCapacity == 0 { - options.EventCapacity = 1000 - } - - eventOptions := collector.DefaultEventOptions() - if options.EventOptions != nil { - eventOptions = *options.EventOptions - } - - eventCollector := collector.NewEventCollectorWithOptions(options.EventCapacity, eventOptions) + // Create the central EventAggregator (no storage by default) + eventAggregator := collector.NewEventAggregator() logOptions := collector.DefaultLogOptions() if options.LogOptions != nil { logOptions = *options.LogOptions } - logOptions.EventCollector = eventCollector + logOptions.EventAggregator = eventAggregator httpClientOptions := collector.DefaultHTTPClientOptions() if options.HTTPClientOptions != nil { httpClientOptions = *options.HTTPClientOptions } - httpClientOptions.EventCollector = eventCollector + httpClientOptions.EventAggregator = eventAggregator httpServerOptions := collector.DefaultHTTPServerOptions() if options.HTTPServerOptions != nil { httpServerOptions = *options.HTTPServerOptions } - httpServerOptions.EventCollector = eventCollector + httpServerOptions.EventAggregator = eventAggregator dbQueryOptions := collector.DefaultDBQueryOptions() if options.DBQueryOptions != nil { dbQueryOptions = *options.DBQueryOptions } - dbQueryOptions.EventCollector = eventCollector + dbQueryOptions.EventAggregator = eventAggregator instance := &Instance{ logCollector: collector.NewLogCollectorWithOptions(options.LogCapacity, logOptions), httpClientCollector: collector.NewHTTPClientCollectorWithOptions(options.HTTPClientCapacity, httpClientOptions), httpServerCollector: collector.NewHTTPServerCollectorWithOptions(options.HTTPServerCapacity, httpServerOptions), dbQueryCollector: collector.NewDBQueryCollectorWithOptions(options.DBQueryCapacity, dbQueryOptions), - eventCollector: eventCollector, + eventAggregator: eventAggregator, } return instance } @@ -138,11 +131,13 @@ func (i *Instance) CollectDBQuery() func(ctx context.Context, dbQuery collector. } func (i *Instance) DashboardHandler(pathPrefix string) http.Handler { - return dashboard.NewHandler( + handler := dashboard.NewHandler( dashboard.HandlerOptions{ - EventCollector: i.eventCollector, + EventAggregator: i.eventAggregator, PathPrefix: pathPrefix, }, ) + i.dashboardHandler = handler + return handler } diff --git a/devlog_e2e_test.go b/devlog_e2e_test.go index bcc51b2..9f67426 100644 --- a/devlog_e2e_test.go +++ b/devlog_e2e_test.go @@ -34,7 +34,6 @@ func TestE2E(t *testing.T) { CaptureResponseBody: true, }, DBQueryCapacity: 10, - EventCapacity: 10, }) mux := http.NewServeMux() From 76353f8812b05e241110ee4b8e274b6339e4481d Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 1 Dec 2025 10:46:41 +0100 Subject: [PATCH 03/22] ui: use tape like buttons --- dashboard/static/main.css | 92 ++++++++-- dashboard/styles/main.css | 18 ++ dashboard/views/button.go | 2 +- dashboard/views/header.templ | 105 +++++++---- dashboard/views/header_templ.go | 312 ++++++++++++++++++-------------- 5 files changed, 345 insertions(+), 184 deletions(-) diff --git a/dashboard/static/main.css b/dashboard/static/main.css index d055778..77af48a 100644 --- a/dashboard/static/main.css +++ b/dashboard/static/main.css @@ -8,6 +8,10 @@ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --color-red-50: oklch(97.1% 0.013 17.38); + --color-red-100: oklch(93.6% 0.032 17.717); + --color-red-200: oklch(88.5% 0.062 18.334); + --color-red-300: oklch(80.8% 0.114 19.571); + --color-red-400: oklch(70.4% 0.191 22.216); --color-red-500: oklch(63.7% 0.237 25.331); --color-red-600: oklch(57.7% 0.245 27.325); --color-red-700: oklch(50.5% 0.213 27.518); @@ -199,17 +203,6 @@ .visible { visibility: visible; } - .sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; - } .relative { position: relative; } @@ -321,9 +314,6 @@ .grow { flex-grow: 1; } - .cursor-not-allowed { - cursor: not-allowed; - } .cursor-pointer { cursor: pointer; } @@ -426,6 +416,18 @@ .border-neutral-300 { border-color: var(--color-neutral-300); } + .border-neutral-600 { + border-color: var(--color-neutral-600); + } + .border-red-200 { + border-color: var(--color-red-200); + } + .border-red-300 { + border-color: var(--color-red-300); + } + .border-red-600 { + border-color: var(--color-red-600); + } .border-transparent { border-color: transparent; } @@ -444,6 +446,9 @@ .bg-neutral-200 { background-color: var(--color-neutral-200); } + .bg-neutral-500 { + background-color: var(--color-neutral-500); + } .bg-neutral-900 { background-color: var(--color-neutral-900); } @@ -453,6 +458,12 @@ .bg-red-50 { background-color: var(--color-red-50); } + .bg-red-100 { + background-color: var(--color-red-100); + } + .bg-red-200 { + background-color: var(--color-red-200); + } .bg-red-500 { background-color: var(--color-red-500); } @@ -551,6 +562,9 @@ .text-green-600 { color: var(--color-green-600); } + .text-neutral-300 { + color: var(--color-neutral-300); + } .text-neutral-400 { color: var(--color-neutral-400); } @@ -566,6 +580,9 @@ .text-orange-600 { color: var(--color-orange-600); } + .text-red-300 { + color: var(--color-red-300); + } .text-red-500 { color: var(--color-red-500); } @@ -578,9 +595,6 @@ .text-white { color: var(--color-white); } - .opacity-50 { - opacity: 50%; - } .ring { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -633,6 +647,13 @@ } } } + .hover\:bg-red-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-red-100); + } + } + } .hover\:text-blue-800 { &:hover { @media (hover: hover) { @@ -640,6 +661,34 @@ } } } + .hover\:text-neutral-400 { + &:hover { + @media (hover: hover) { + color: var(--color-neutral-400); + } + } + } + .hover\:text-neutral-500 { + &:hover { + @media (hover: hover) { + color: var(--color-neutral-500); + } + } + } + .hover\:text-neutral-600 { + &:hover { + @media (hover: hover) { + color: var(--color-neutral-600); + } + } + } + .hover\:text-red-400 { + &:hover { + @media (hover: hover) { + color: var(--color-red-400); + } + } + } .focus-visible\:ring-1 { &:focus-visible { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); @@ -711,6 +760,15 @@ } } } +.glow-red { + box-shadow: 0 0 8px 2px rgba(239, 68, 68, 0.4); +} +.glow-gray { + box-shadow: 0 0 8px 2px rgba(115, 115, 115, 0.3); +} +.btn-pressed { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2); +} @property --tw-divide-y-reverse { syntax: "*"; inherits: false; diff --git a/dashboard/styles/main.css b/dashboard/styles/main.css index f1d8c73..ebfc711 100644 --- a/dashboard/styles/main.css +++ b/dashboard/styles/main.css @@ -1 +1,19 @@ @import "tailwindcss"; + +@theme { + --color-devlog-cyan: #06f5f7; +} + +/* Custom glow utilities for capture buttons */ +.glow-red { + box-shadow: 0 0 8px 2px rgba(239, 68, 68, 0.4); +} + +.glow-gray { + box-shadow: 0 0 8px 2px rgba(115, 115, 115, 0.3); +} + +/* Cassette player style pressed button */ +.btn-pressed { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2); +} diff --git a/dashboard/views/button.go b/dashboard/views/button.go index 2b1565d..bf32656 100644 --- a/dashboard/views/button.go +++ b/dashboard/views/button.go @@ -25,7 +25,7 @@ func buttonClasses(props ButtonProps) string { var classes []string // Base classes - classes = append(classes, "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50") + classes = append(classes, "cursor-pointer inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50") // Variant classes switch props.Variant { diff --git a/dashboard/views/header.templ b/dashboard/views/header.templ index 1eacce0..7dbeb12 100644 --- a/dashboard/views/header.templ +++ b/dashboard/views/header.templ @@ -1,6 +1,9 @@ package views -import "fmt" +import ( + "fmt" + "strings" +) type CaptureState struct { Active bool @@ -43,42 +46,74 @@ templ CaptureControls(capture CaptureState) { {{ mode = "session" }} }
- - + } @CaptureMode(mode, capture.Active)
} +// TapeButton renders a cassette-player style button (pressed/released states) +templ TapeButton(props TapeButtonProps, attrs templ.Attributes) { + +} + +type TapeButtonColor string + +const ( + TapeButtonColorRed TapeButtonColor = "red" + TapeButtonColorGray TapeButtonColor = "gray" +) + +type TapeButtonProps struct { + Pressed bool + Color TapeButtonColor +} + +func tapeButtonClasses(props TapeButtonProps) string { + var classes []string + + // Base classes + classes = append(classes, "cursor-pointer inline-flex items-center justify-center h-10 w-10 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2") + + // Color and state classes + switch props.Color { + case TapeButtonColorRed: + if props.Pressed { + classes = append(classes, "bg-red-200 border-red-300 text-red-700 glow-red btn-pressed") + } else { + classes = append(classes, "bg-red-50 border-red-200 text-red-300 glow-red hover:bg-red-100 hover:text-red-400") + } + default: // TapeButtonColorGray + if props.Pressed { + classes = append(classes, "bg-neutral-200 border-neutral-300 text-black glow-gray btn-pressed") + } else { + classes = append(classes, "bg-neutral-50 border-neutral-200 text-neutral-500 glow-gray hover:bg-neutral-100 hover:text-neutral-600") + } + } + + return strings.Join(classes, " ") +} + templ CaptureMode(mode string, capturing bool) {
if capturing { @@ -119,9 +154,9 @@ templ CaptureMode(mode string, capturing bool) {
} -templ iconPlay() { - - +templ iconRecord() { + + } diff --git a/dashboard/views/header_templ.go b/dashboard/views/header_templ.go index 3186c46..6058da4 100644 --- a/dashboard/views/header_templ.go +++ b/dashboard/views/header_templ.go @@ -8,7 +8,10 @@ package views import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -import "fmt" +import ( + "fmt" + "strings" +) type CaptureState struct { Active bool @@ -77,7 +80,7 @@ func Header(capture CaptureState) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%s/event-list", MustGetHandlerOptions(ctx).PathPrefix)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/header.templ`, Line: 29, Col: 84} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/header.templ`, Line: 32, Col: 84} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -131,7 +134,7 @@ func CaptureControls(capture CaptureState) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(mode) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/header.templ`, Line: 45, Col: 76} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/header.templ`, Line: 48, Col: 76} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -141,75 +144,98 @@ func CaptureControls(capture CaptureState) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 = []any{buttonClasses( - ButtonProps{ - Variant: ButtonVariantDefault, - Size: ButtonSizeIcon, - Disabled: capture.Active, - })} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() } - var templ_7745c5c3_Var10 = []any{buttonClasses( - ButtonProps{ - Variant: ButtonVariantOutline, - Size: ButtonSizeIcon, - Disabled: !capture.Active, - })} + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var9 := templ.GetChildren(ctx) + if templ_7745c5c3_Var9 == nil { + templ_7745c5c3_Var9 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + var templ_7745c5c3_Var10 = []any{tapeButtonClasses(props)} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, ">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = CaptureMode(mode, capture.Active).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = templ_7745c5c3_Var9.Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -269,6 +282,43 @@ func CaptureControls(capture CaptureState) templ.Component { }) } +type TapeButtonColor string + +const ( + TapeButtonColorRed TapeButtonColor = "red" + TapeButtonColorGray TapeButtonColor = "gray" +) + +type TapeButtonProps struct { + Pressed bool + Color TapeButtonColor +} + +func tapeButtonClasses(props TapeButtonProps) string { + var classes []string + + // Base classes + classes = append(classes, "cursor-pointer inline-flex items-center justify-center h-10 w-10 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2") + + // Color and state classes + switch props.Color { + case TapeButtonColorRed: + if props.Pressed { + classes = append(classes, "bg-red-200 border-red-300 text-red-700 glow-red btn-pressed") + } else { + classes = append(classes, "bg-red-50 border-red-200 text-red-300 glow-red hover:bg-red-100 hover:text-red-400") + } + default: // TapeButtonColorGray + if props.Pressed { + classes = append(classes, "bg-neutral-200 border-neutral-300 text-black glow-gray btn-pressed") + } else { + classes = append(classes, "bg-neutral-50 border-neutral-200 text-neutral-500 glow-gray hover:bg-neutral-100 hover:text-neutral-600") + } + } + + return strings.Join(classes, " ") +} + func CaptureMode(mode string, capturing bool) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context @@ -285,133 +335,133 @@ func CaptureMode(mode string, capturing bool) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var13 := templ.GetChildren(ctx) - if templ_7745c5c3_Var13 == nil { - templ_7745c5c3_Var13 = templ.NopComponent + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if capturing { - var templ_7745c5c3_Var14 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-neutral-900 text-white", mode == "session"), templ.KV("hover:bg-neutral-100", mode != "session")} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...) + var templ_7745c5c3_Var13 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-neutral-900 text-white", mode == "session"), templ.KV("hover:bg-neutral-100", mode != "session")} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" hx-target=\"#capture-controls\" hx-swap=\"outerHTML\">Session ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-neutral-300", templ.KV("bg-neutral-900 text-white", mode == "global"), templ.KV("hover:bg-neutral-100", mode != "global")} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...) + var templ_7745c5c3_Var16 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-neutral-300", templ.KV("bg-neutral-900 text-white", mode == "global"), templ.KV("hover:bg-neutral-100", mode != "global")} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" hx-target=\"#capture-controls\" hx-swap=\"outerHTML\">Global") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var20 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-neutral-900 text-white", mode == "session"), templ.KV("hover:bg-neutral-100", mode != "session")} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...) + var templ_7745c5c3_Var19 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-neutral-900 text-white", mode == "session"), templ.KV("hover:bg-neutral-100", mode != "session")} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" onclick=\"document.getElementById('capture-controls').dataset.mode='session'; this.classList.add('bg-neutral-900','text-white'); this.classList.remove('hover:bg-neutral-100'); this.nextElementSibling.classList.remove('bg-neutral-900','text-white'); this.nextElementSibling.classList.add('hover:bg-neutral-100');\">Session ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var22 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-neutral-300", templ.KV("bg-neutral-900 text-white", mode == "global"), templ.KV("hover:bg-neutral-100", mode != "global")} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var22...) + var templ_7745c5c3_Var21 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-neutral-300", templ.KV("bg-neutral-900 text-white", mode == "global"), templ.KV("hover:bg-neutral-100", mode != "global")} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" onclick=\"document.getElementById('capture-controls').dataset.mode='global'; this.classList.add('bg-neutral-900','text-white'); this.classList.remove('hover:bg-neutral-100'); this.previousElementSibling.classList.remove('bg-neutral-900','text-white'); this.previousElementSibling.classList.add('hover:bg-neutral-100');\">Global") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -419,7 +469,7 @@ func CaptureMode(mode string, capturing bool) templ.Component { }) } -func iconPlay() templ.Component { +func iconRecord() templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -435,12 +485,12 @@ func iconPlay() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var24 := templ.GetChildren(ctx) - if templ_7745c5c3_Var24 == nil { - templ_7745c5c3_Var24 = templ.NopComponent + templ_7745c5c3_Var23 := templ.GetChildren(ctx) + if templ_7745c5c3_Var23 == nil { + templ_7745c5c3_Var23 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -464,12 +514,12 @@ func iconStop() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var25 := templ.GetChildren(ctx) - if templ_7745c5c3_Var25 == nil { - templ_7745c5c3_Var25 = templ.NopComponent + templ_7745c5c3_Var24 := templ.GetChildren(ctx) + if templ_7745c5c3_Var24 == nil { + templ_7745c5c3_Var24 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -493,12 +543,12 @@ func iconDeleteRow() templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var26 := templ.GetChildren(ctx) - if templ_7745c5c3_Var26 == nil { - templ_7745c5c3_Var26 = templ.NopComponent + templ_7745c5c3_Var25 := templ.GetChildren(ctx) + if templ_7745c5c3_Var25 == nil { + templ_7745c5c3_Var25 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Delete Row Streamline Icon: https://streamlinehq.com ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Delete Row Streamline Icon: https://streamlinehq.com ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From b53786ea7ee0dfc271dd73c4076bf14536d003ac Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 1 Dec 2025 11:27:17 +0100 Subject: [PATCH 04/22] ui: dark header top bar --- dashboard/static/devlog-logo-dark.svg | 30 ++++ dashboard/static/main.css | 236 ++++++++++++++++++++------ dashboard/styles/main.css | 20 ++- dashboard/views/button.go | 9 +- dashboard/views/header.templ | 106 ++++++++---- dashboard/views/header_templ.go | 129 +++++++++----- 6 files changed, 390 insertions(+), 140 deletions(-) create mode 100644 dashboard/static/devlog-logo-dark.svg diff --git a/dashboard/static/devlog-logo-dark.svg b/dashboard/static/devlog-logo-dark.svg new file mode 100644 index 0000000..9c232d2 --- /dev/null +++ b/dashboard/static/devlog-logo-dark.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dashboard/static/main.css b/dashboard/static/main.css index 77af48a..89a7c59 100644 --- a/dashboard/static/main.css +++ b/dashboard/static/main.css @@ -8,13 +8,13 @@ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; --color-red-50: oklch(97.1% 0.013 17.38); - --color-red-100: oklch(93.6% 0.032 17.717); - --color-red-200: oklch(88.5% 0.062 18.334); - --color-red-300: oklch(80.8% 0.114 19.571); --color-red-400: oklch(70.4% 0.191 22.216); --color-red-500: oklch(63.7% 0.237 25.331); --color-red-600: oklch(57.7% 0.245 27.325); --color-red-700: oklch(50.5% 0.213 27.518); + --color-red-800: oklch(44.4% 0.177 26.899); + --color-red-900: oklch(39.6% 0.141 25.723); + --color-red-950: oklch(25.8% 0.092 26.042); --color-orange-400: oklch(75% 0.183 55.934); --color-orange-600: oklch(64.6% 0.222 41.116); --color-green-600: oklch(62.7% 0.194 149.214); @@ -29,7 +29,7 @@ --color-neutral-500: oklch(55.6% 0 0); --color-neutral-600: oklch(43.9% 0 0); --color-neutral-700: oklch(37.1% 0 0); - --color-neutral-900: oklch(20.5% 0 0); + --color-neutral-800: oklch(26.9% 0 0); --color-black: #000; --color-white: #fff; --spacing: 0.25rem; @@ -42,16 +42,16 @@ --text-base--line-height: calc(1.5 / 1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75 / 1.125); - --text-xl: 1.25rem; - --text-xl--line-height: calc(1.75 / 1.25); --font-weight-medium: 500; --font-weight-semibold: 600; - --font-weight-bold: 700; --radius-md: 0.375rem; --default-transition-duration: 150ms; --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); --default-font-family: var(--font-sans); --default-mono-font-family: var(--font-mono); + --color-devlog-cyan: #04F3F8; + --color-header-bg: #0D0A29; + --color-header-border: #1e2a4a; } } @layer base { @@ -410,33 +410,51 @@ border-left-style: var(--tw-border-style); border-left-width: 1px; } + .border-header-border { + border-color: var(--color-header-border); + } .border-neutral-200 { border-color: var(--color-neutral-200); } .border-neutral-300 { border-color: var(--color-neutral-300); } + .border-neutral-500 { + border-color: var(--color-neutral-500); + } .border-neutral-600 { border-color: var(--color-neutral-600); } - .border-red-200 { - border-color: var(--color-red-200); - } - .border-red-300 { - border-color: var(--color-red-300); - } .border-red-600 { border-color: var(--color-red-600); } + .border-red-800 { + border-color: var(--color-red-800); + } .border-transparent { border-color: transparent; } .bg-black { background-color: var(--color-black); } + .bg-devlog-cyan\/20 { + background-color: color-mix(in srgb, #04F3F8 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-devlog-cyan) 20%, transparent); + } + } .bg-green-600 { background-color: var(--color-green-600); } + .bg-header-bg { + background-color: var(--color-header-bg); + } + .bg-header-bg\/50 { + background-color: color-mix(in srgb, #0D0A29 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-header-bg) 50%, transparent); + } + } .bg-neutral-50 { background-color: var(--color-neutral-50); } @@ -446,11 +464,14 @@ .bg-neutral-200 { background-color: var(--color-neutral-200); } - .bg-neutral-500 { - background-color: var(--color-neutral-500); + .bg-neutral-700 { + background-color: var(--color-neutral-700); } - .bg-neutral-900 { - background-color: var(--color-neutral-900); + .bg-neutral-800\/50 { + background-color: color-mix(in srgb, oklch(26.9% 0 0) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-neutral-800) 50%, transparent); + } } .bg-orange-400 { background-color: var(--color-orange-400); @@ -458,18 +479,27 @@ .bg-red-50 { background-color: var(--color-red-50); } - .bg-red-100 { - background-color: var(--color-red-100); - } - .bg-red-200 { - background-color: var(--color-red-200); - } .bg-red-500 { background-color: var(--color-red-500); } + .bg-red-800 { + background-color: var(--color-red-800); + } + .bg-red-950\/50 { + background-color: color-mix(in srgb, oklch(25.8% 0.092 26.042) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-red-950) 50%, transparent); + } + } .bg-white { background-color: var(--color-white); } + .bg-white\/10 { + background-color: color-mix(in srgb, #fff 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 10%, transparent); + } + } .p-2 { padding: calc(var(--spacing) * 2); } @@ -521,18 +551,10 @@ font-size: var(--text-sm); line-height: var(--tw-leading, var(--text-sm--line-height)); } - .text-xl { - font-size: var(--text-xl); - line-height: var(--tw-leading, var(--text-xl--line-height)); - } .text-xs { font-size: var(--text-xs); line-height: var(--tw-leading, var(--text-xs--line-height)); } - .font-bold { - --tw-font-weight: var(--font-weight-bold); - font-weight: var(--font-weight-bold); - } .font-medium { --tw-font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium); @@ -559,6 +581,9 @@ .text-blue-600 { color: var(--color-blue-600); } + .text-devlog-cyan { + color: var(--color-devlog-cyan); + } .text-green-600 { color: var(--color-green-600); } @@ -580,8 +605,8 @@ .text-orange-600 { color: var(--color-orange-600); } - .text-red-300 { - color: var(--color-red-300); + .text-red-400 { + color: var(--color-red-400); } .text-red-500 { color: var(--color-red-500); @@ -603,14 +628,17 @@ outline-style: var(--tw-outline-style); outline-width: 1px; } + .filter { + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } .transition-colors { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } - .placeholder\:text-neutral-500 { + .placeholder\:text-neutral-400 { &::placeholder { - color: var(--color-neutral-500); + color: var(--color-neutral-400); } } .hover\:bg-black\/90 { @@ -647,38 +675,57 @@ } } } - .hover\:bg-red-100 { + .hover\:bg-neutral-700\/50 { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(37.1% 0 0) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-neutral-700) 50%, transparent); + } + } + } + } + .hover\:bg-red-900\/50 { &:hover { @media (hover: hover) { - background-color: var(--color-red-100); + background-color: color-mix(in srgb, oklch(39.6% 0.141 25.723) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-red-900) 50%, transparent); + } } } } - .hover\:text-blue-800 { + .hover\:bg-white\/10 { &:hover { @media (hover: hover) { - color: var(--color-blue-800); + background-color: color-mix(in srgb, #fff 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 10%, transparent); + } } } } - .hover\:text-neutral-400 { + .hover\:bg-white\/20 { &:hover { @media (hover: hover) { - color: var(--color-neutral-400); + background-color: color-mix(in srgb, #fff 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 20%, transparent); + } } } } - .hover\:text-neutral-500 { + .hover\:text-blue-800 { &:hover { @media (hover: hover) { - color: var(--color-neutral-500); + color: var(--color-blue-800); } } } - .hover\:text-neutral-600 { + .hover\:text-neutral-300 { &:hover { @media (hover: hover) { - color: var(--color-neutral-600); + color: var(--color-neutral-300); } } } @@ -689,6 +736,13 @@ } } } + .hover\:text-white { + &:hover { + @media (hover: hover) { + color: var(--color-white); + } + } + } .focus-visible\:ring-1 { &:focus-visible { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); @@ -701,9 +755,9 @@ box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } } - .focus-visible\:ring-black { + .focus-visible\:ring-devlog-cyan { &:focus-visible { - --tw-ring-color: var(--color-black); + --tw-ring-color: var(--color-devlog-cyan); } } .focus-visible\:ring-offset-2 { @@ -743,6 +797,16 @@ align-items: center; } } + .sm\:justify-between { + @media (width >= 40rem) { + justify-content: space-between; + } + } + .sm\:gap-6 { + @media (width >= 40rem) { + gap: calc(var(--spacing) * 6); + } + } .md\:p-4 { @media (width >= 48rem) { padding: calc(var(--spacing) * 4); @@ -761,13 +825,19 @@ } } .glow-red { - box-shadow: 0 0 8px 2px rgba(239, 68, 68, 0.4); + box-shadow: 0 0 10px 3px rgba(239, 68, 68, 0.5); } .glow-gray { - box-shadow: 0 0 8px 2px rgba(115, 115, 115, 0.3); + box-shadow: 0 0 8px 2px rgba(150, 150, 150, 0.3); } .btn-pressed { - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.5); +} +.btn-pressed.glow-gray { + background-color: color-mix(in srgb, #0D0A29 40%, rgb(96 96 96)); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in srgb, var(--color-header-bg) 40%, rgb(96 96 96)); + } } @property --tw-divide-y-reverse { syntax: "*"; @@ -853,6 +923,59 @@ inherits: false; initial-value: solid; } +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} @layer properties { @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { *, ::before, ::after, ::backdrop { @@ -874,6 +997,19 @@ --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; --tw-outline-style: solid; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; } } } diff --git a/dashboard/styles/main.css b/dashboard/styles/main.css index ebfc711..33a0a33 100644 --- a/dashboard/styles/main.css +++ b/dashboard/styles/main.css @@ -1,19 +1,27 @@ @import "tailwindcss"; @theme { - --color-devlog-cyan: #06f5f7; + --color-devlog-cyan: #04F3F8; + --color-devlog-cyan-glow: #63FCFF; + --color-header-bg: #0D0A29; + --color-header-border: #1e2a4a; } -/* Custom glow utilities for capture buttons */ +/* Custom glow utilities for capture buttons - enhanced for dark background */ .glow-red { - box-shadow: 0 0 8px 2px rgba(239, 68, 68, 0.4); + box-shadow: 0 0 10px 3px rgba(239, 68, 68, 0.5); } .glow-gray { - box-shadow: 0 0 8px 2px rgba(115, 115, 115, 0.3); + box-shadow: 0 0 8px 2px rgba(150, 150, 150, 0.3); } -/* Cassette player style pressed button */ +/* Cassette player style pressed button - deeper shadow for dark theme */ .btn-pressed { - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.5); +} + +/* Dark navy tint for pressed gray button */ +.btn-pressed.glow-gray { + background-color: color-mix(in srgb, var(--color-header-bg) 40%, rgb(96 96 96)); } diff --git a/dashboard/views/button.go b/dashboard/views/button.go index bf32656..a22c9d0 100644 --- a/dashboard/views/button.go +++ b/dashboard/views/button.go @@ -6,9 +6,10 @@ type ButtonVariant string type ButtonSize string const ( - ButtonVariantDefault ButtonVariant = "" - ButtonVariantOutline ButtonVariant = "outline" - ButtonVariantSecondary ButtonVariant = "secondary" + ButtonVariantDefault ButtonVariant = "" + ButtonVariantOutline ButtonVariant = "outline" + ButtonVariantOutlineDark ButtonVariant = "outline-dark" + ButtonVariantSecondary ButtonVariant = "secondary" ButtonSizeSm ButtonSize = "sm" ButtonSizeIcon ButtonSize = "icon" @@ -31,6 +32,8 @@ func buttonClasses(props ButtonProps) string { switch props.Variant { case ButtonVariantOutline: classes = append(classes, "border border-neutral-200 bg-white hover:bg-neutral-200 text-black") + case ButtonVariantOutlineDark: + classes = append(classes, "border border-header-border bg-white/10 hover:bg-white/20 text-white") case ButtonVariantSecondary: classes = append(classes, "bg-neutral-200 text-black hover:bg-neutral-200/80") default: // DefaultVariant diff --git a/dashboard/views/header.templ b/dashboard/views/header.templ index 7dbeb12..1bca3c7 100644 --- a/dashboard/views/header.templ +++ b/dashboard/views/header.templ @@ -11,21 +11,24 @@ type CaptureState struct { } templ Header(capture CaptureState) { -
-
-

devlog

+
+
+ @devlogLogo()
-
+
@CaptureControls(capture)
+ /* + */ + @@ -172,10 +177,37 @@ templ iconDeleteRow() { Delete Row Streamline Icon: https://streamlinehq.com - - - - + + + + + + +} + +templ devlogLogo() { + + + + + + + + + + + + + } \ No newline at end of file diff --git a/dashboard/views/header_templ.go b/dashboard/views/header_templ.go index 6058da4..1b9ad85 100644 --- a/dashboard/views/header_templ.go +++ b/dashboard/views/header_templ.go @@ -39,7 +39,15 @@ func Header(capture CaptureState) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "

devlog

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = devlogLogo().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -47,20 +55,20 @@ func Header(capture CaptureState) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var2 = []any{buttonClasses( ButtonProps{ - Variant: ButtonVariantOutline, + Variant: ButtonVariantOutlineDark, Size: ButtonSizeIcon, })} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -127,20 +135,20 @@ func CaptureControls(capture CaptureState) templ.Component { if mode == "" { mode = "session" } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -196,11 +204,15 @@ func CaptureControls(capture CaptureState) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } templ_7745c5c3_Err = CaptureMode(mode, capture.Active).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -235,7 +247,7 @@ func TapeButton(props TapeButtonProps, attrs templ.Attributes) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -300,19 +312,19 @@ func tapeButtonClasses(props TapeButtonProps) string { // Base classes classes = append(classes, "cursor-pointer inline-flex items-center justify-center h-10 w-10 rounded-md border transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2") - // Color and state classes + // Color and state classes - optimized for dark header background switch props.Color { case TapeButtonColorRed: if props.Pressed { - classes = append(classes, "bg-red-200 border-red-300 text-red-700 glow-red btn-pressed") + classes = append(classes, "bg-red-800 border-red-600 text-red-400 glow-red btn-pressed") } else { - classes = append(classes, "bg-red-50 border-red-200 text-red-300 glow-red hover:bg-red-100 hover:text-red-400") + classes = append(classes, "bg-red-950/50 border-red-800 text-red-500 glow-red hover:bg-red-900/50 hover:text-red-400") } default: // TapeButtonColorGray if props.Pressed { - classes = append(classes, "bg-neutral-200 border-neutral-300 text-black glow-gray btn-pressed") + classes = append(classes, "bg-neutral-700 border-neutral-500 text-neutral-300 glow-gray btn-pressed") } else { - classes = append(classes, "bg-neutral-50 border-neutral-200 text-neutral-500 glow-gray hover:bg-neutral-100 hover:text-neutral-600") + classes = append(classes, "bg-neutral-800/50 border-neutral-600 text-neutral-400 glow-gray hover:bg-neutral-700/50 hover:text-neutral-300") } } @@ -340,17 +352,17 @@ func CaptureMode(mode string, capturing bool) templ.Component { templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if capturing { - var templ_7745c5c3_Var13 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-neutral-900 text-white", mode == "session"), templ.KV("hover:bg-neutral-100", mode != "session")} + var templ_7745c5c3_Var13 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-devlog-cyan/20 text-devlog-cyan", mode == "session"), templ.KV("text-neutral-400 hover:bg-white/10 hover:text-white", mode != "session")} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var13...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" hx-target=\"#capture-controls\" hx-swap=\"outerHTML\">Session ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-neutral-300", templ.KV("bg-neutral-900 text-white", mode == "global"), templ.KV("hover:bg-neutral-100", mode != "global")} + var templ_7745c5c3_Var16 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-header-border", templ.KV("bg-devlog-cyan/20 text-devlog-cyan", mode == "global"), templ.KV("text-neutral-400 hover:bg-white/10 hover:text-white", mode != "global")} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" hx-target=\"#capture-controls\" hx-swap=\"outerHTML\">Global") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var19 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-neutral-900 text-white", mode == "session"), templ.KV("hover:bg-neutral-100", mode != "session")} + var templ_7745c5c3_Var19 = []any{"px-3 py-2 cursor-pointer transition-colors", templ.KV("bg-devlog-cyan/20 text-devlog-cyan", mode == "session"), templ.KV("text-neutral-400 hover:bg-white/10 hover:text-white", mode != "session")} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" onclick=\"document.getElementById('capture-controls').dataset.mode='session'; this.classList.add('bg-devlog-cyan/20','text-devlog-cyan'); this.classList.remove('text-neutral-400','hover:bg-white/10','hover:text-white'); this.nextElementSibling.classList.remove('bg-devlog-cyan/20','text-devlog-cyan'); this.nextElementSibling.classList.add('text-neutral-400','hover:bg-white/10','hover:text-white');\">Session ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var21 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-neutral-300", templ.KV("bg-neutral-900 text-white", mode == "global"), templ.KV("hover:bg-neutral-100", mode != "global")} + var templ_7745c5c3_Var21 = []any{"px-3 py-2 cursor-pointer transition-colors border-l border-header-border", templ.KV("bg-devlog-cyan/20 text-devlog-cyan", mode == "global"), templ.KV("text-neutral-400 hover:bg-white/10 hover:text-white", mode != "global")} templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var21...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" onclick=\"document.getElementById('capture-controls').dataset.mode='global'; this.classList.add('bg-devlog-cyan/20','text-devlog-cyan'); this.classList.remove('text-neutral-400','hover:bg-white/10','hover:text-white'); this.previousElementSibling.classList.remove('bg-devlog-cyan/20','text-devlog-cyan'); this.previousElementSibling.classList.add('text-neutral-400','hover:bg-white/10','hover:text-white');\">Global") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -490,7 +502,7 @@ func iconRecord() templ.Component { templ_7745c5c3_Var23 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -519,7 +531,7 @@ func iconStop() templ.Component { templ_7745c5c3_Var24 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -548,7 +560,36 @@ func iconDeleteRow() templ.Component { templ_7745c5c3_Var25 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Delete Row Streamline Icon: https://streamlinehq.com ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "Delete Row Streamline Icon: https://streamlinehq.com ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + +func devlogLogo() templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var26 := templ.GetChildren(ctx) + if templ_7745c5c3_Var26 == nil { + templ_7745c5c3_Var26 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From 1da301ad34850bd5dbb05d9a1764978e20c15061 Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 1 Dec 2025 12:08:55 +0100 Subject: [PATCH 05/22] ui: add new icon --- dashboard/static/android-chrome-192x192.png | Bin 0 -> 34414 bytes dashboard/static/android-chrome-512x512.png | Bin 0 -> 174060 bytes dashboard/static/apple-touch-icon.png | Bin 0 -> 30939 bytes dashboard/static/assets.go | 2 +- dashboard/static/favicon-16x16.png | Bin 0 -> 661 bytes dashboard/static/favicon-32x32.png | Bin 0 -> 1862 bytes dashboard/static/favicon.ico | Bin 15086 -> 15406 bytes dashboard/static/main.css | 67 ++-------------- dashboard/static/site.webmanifest | 1 + dashboard/styles/main.css | 4 +- dashboard/views/header.templ | 13 +--- dashboard/views/header_templ.go | 76 +++++++++---------- dashboard/views/layout.templ | 5 ++ dashboard/views/layout_templ.go | 80 ++++++++++++++++---- 14 files changed, 119 insertions(+), 129 deletions(-) create mode 100644 dashboard/static/android-chrome-192x192.png create mode 100644 dashboard/static/android-chrome-512x512.png create mode 100644 dashboard/static/apple-touch-icon.png create mode 100644 dashboard/static/favicon-16x16.png create mode 100644 dashboard/static/favicon-32x32.png create mode 100644 dashboard/static/site.webmanifest diff --git a/dashboard/static/android-chrome-192x192.png b/dashboard/static/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..e48afd0497d21f54033abf5e39461a67ebbb23d1 GIT binary patch literal 34414 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 literal 0 HcmV?d00001 diff --git a/dashboard/static/apple-touch-icon.png b/dashboard/static/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..72a289dbb7fd48863db50659188146da795f8289 GIT binary patch literal 30939 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 literal 0 HcmV?d00001 diff --git a/dashboard/static/favicon-32x32.png b/dashboard/static/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..7ec1e912ebd9675dbb7e5c8e01a7bb65d5cfe1f4 GIT binary patch literal 1862 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 diff --git a/dashboard/static/main.css b/dashboard/static/main.css index 89a7c59..87251a1 100644 --- a/dashboard/static/main.css +++ b/dashboard/static/main.css @@ -33,13 +33,10 @@ --color-black: #000; --color-white: #fff; --spacing: 0.25rem; - --container-sm: 24rem; --text-xs: 0.75rem; --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; --text-sm--line-height: calc(1.25 / 0.875); - --text-base: 1rem; - --text-base--line-height: calc(1.5 / 1); --text-lg: 1.125rem; --text-lg--line-height: calc(1.75 / 1.125); --font-weight-medium: 500; @@ -203,9 +200,6 @@ .visible { visibility: visible; } - .relative { - position: relative; - } .static { position: static; } @@ -248,9 +242,6 @@ .mb-2 { margin-bottom: calc(var(--spacing) * 2); } - .mb-3 { - margin-bottom: calc(var(--spacing) * 3); - } .mb-4 { margin-bottom: calc(var(--spacing) * 4); } @@ -305,9 +296,6 @@ .w-full { width: 100%; } - .max-w-sm { - max-width: var(--container-sm); - } .flex-1 { flex: 1; } @@ -341,6 +329,9 @@ .justify-center { justify-content: center; } + .justify-end { + justify-content: flex-end; + } .gap-1 { gap: calc(var(--spacing) * 1); } @@ -539,10 +530,6 @@ .font-mono { font-family: var(--font-mono); } - .text-base { - font-size: var(--text-base); - line-height: var(--tw-leading, var(--text-base--line-height)); - } .text-lg { font-size: var(--text-lg); line-height: var(--tw-leading, var(--text-lg--line-height)); @@ -636,11 +623,6 @@ transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); transition-duration: var(--tw-duration, var(--default-transition-duration)); } - .placeholder\:text-neutral-400 { - &::placeholder { - color: var(--color-neutral-400); - } - } .hover\:bg-black\/90 { &:hover { @media (hover: hover) { @@ -743,23 +725,12 @@ } } } - .focus-visible\:ring-1 { - &:focus-visible { - --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); - box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - } - } .focus-visible\:ring-2 { &:focus-visible { --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); } } - .focus-visible\:ring-devlog-cyan { - &:focus-visible { - --tw-ring-color: var(--color-devlog-cyan); - } - } .focus-visible\:ring-offset-2 { &:focus-visible { --tw-ring-offset-width: 2px; @@ -777,31 +748,11 @@ pointer-events: none; } } - .disabled\:cursor-not-allowed { - &:disabled { - cursor: not-allowed; - } - } .disabled\:opacity-50 { &:disabled { opacity: 50%; } } - .sm\:flex-row { - @media (width >= 40rem) { - flex-direction: row; - } - } - .sm\:items-center { - @media (width >= 40rem) { - align-items: center; - } - } - .sm\:justify-between { - @media (width >= 40rem) { - justify-content: space-between; - } - } .sm\:gap-6 { @media (width >= 40rem) { gap: calc(var(--spacing) * 6); @@ -812,12 +763,6 @@ padding: calc(var(--spacing) * 4); } } - .md\:text-sm { - @media (width >= 48rem) { - font-size: var(--text-sm); - line-height: var(--tw-leading, var(--text-sm--line-height)); - } - } .\[\.selected\]\:bg-blue-50 { &:is(.selected) { background-color: var(--color-blue-50); @@ -831,12 +776,12 @@ box-shadow: 0 0 8px 2px rgba(150, 150, 150, 0.3); } .btn-pressed { - box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.5); + box-shadow: inset 2px 4px 4px rgba(0, 0, 0, 0.5); } .btn-pressed.glow-gray { - background-color: color-mix(in srgb, #0D0A29 40%, rgb(96 96 96)); + background-color: color-mix(in srgb, #0D0A29 40%, rgb(144 144 144)); @supports (color: color-mix(in lab, red, red)) { - background-color: color-mix(in srgb, var(--color-header-bg) 40%, rgb(96 96 96)); + background-color: color-mix(in srgb, var(--color-header-bg) 40%, rgb(144 144 144)); } } @property --tw-divide-y-reverse { diff --git a/dashboard/static/site.webmanifest b/dashboard/static/site.webmanifest new file mode 100644 index 0000000..403fb90 --- /dev/null +++ b/dashboard/static/site.webmanifest @@ -0,0 +1 @@ +{"name":"devlog","short_name":"devlog","icons":[{"src":"android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#0D0A29","background_color":"#0D0A29","display":"standalone"} diff --git a/dashboard/styles/main.css b/dashboard/styles/main.css index 33a0a33..a107292 100644 --- a/dashboard/styles/main.css +++ b/dashboard/styles/main.css @@ -18,10 +18,10 @@ /* Cassette player style pressed button - deeper shadow for dark theme */ .btn-pressed { - box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.5); + box-shadow: inset 2px 4px 4px rgba(0, 0, 0, 0.5); } /* Dark navy tint for pressed gray button */ .btn-pressed.glow-gray { - background-color: color-mix(in srgb, var(--color-header-bg) 40%, rgb(96 96 96)); + background-color: color-mix(in srgb, var(--color-header-bg) 40%, rgb(144 144 144)); } diff --git a/dashboard/views/header.templ b/dashboard/views/header.templ index 1bca3c7..6080a78 100644 --- a/dashboard/views/header.templ +++ b/dashboard/views/header.templ @@ -12,19 +12,10 @@ type CaptureState struct { templ Header(capture CaptureState) {

-
+
@devlogLogo() -
-
@CaptureControls(capture) -
- /* - - */ - +
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -135,20 +131,20 @@ func CaptureControls(capture CaptureState) templ.Component { if mode == "" { mode = "session" } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\">
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -204,7 +200,7 @@ func CaptureControls(capture CaptureState) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -212,7 +208,7 @@ func CaptureControls(capture CaptureState) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -247,7 +243,7 @@ func TapeButton(props TapeButtonProps, attrs templ.Attributes) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -352,7 +348,7 @@ func CaptureMode(mode string, capturing bool) templ.Component { templ_7745c5c3_Var12 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -362,7 +358,7 @@ func CaptureMode(mode string, capturing bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" hx-target=\"#capture-controls\" hx-swap=\"outerHTML\">Session ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -397,7 +393,7 @@ func CaptureMode(mode string, capturing bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" hx-target=\"#capture-controls\" hx-swap=\"outerHTML\">Global") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -433,7 +429,7 @@ func CaptureMode(mode string, capturing bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" onclick=\"document.getElementById('capture-controls').dataset.mode='session'; this.classList.add('bg-devlog-cyan/20','text-devlog-cyan'); this.classList.remove('text-neutral-400','hover:bg-white/10','hover:text-white'); this.nextElementSibling.classList.remove('bg-devlog-cyan/20','text-devlog-cyan'); this.nextElementSibling.classList.add('text-neutral-400','hover:bg-white/10','hover:text-white');\">Session ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -455,7 +451,7 @@ func CaptureMode(mode string, capturing bool) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" onclick=\"document.getElementById('capture-controls').dataset.mode='global'; this.classList.add('bg-devlog-cyan/20','text-devlog-cyan'); this.classList.remove('text-neutral-400','hover:bg-white/10','hover:text-white'); this.previousElementSibling.classList.remove('bg-devlog-cyan/20','text-devlog-cyan'); this.previousElementSibling.classList.add('text-neutral-400','hover:bg-white/10','hover:text-white');\">Global") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -502,7 +498,7 @@ func iconRecord() templ.Component { templ_7745c5c3_Var23 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -531,7 +527,7 @@ func iconStop() templ.Component { templ_7745c5c3_Var24 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -560,7 +556,7 @@ func iconDeleteRow() templ.Component { templ_7745c5c3_Var25 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "Delete Row Streamline Icon: https://streamlinehq.com ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "Delete Row Streamline Icon: https://streamlinehq.com ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -589,7 +585,7 @@ func devlogLogo() templ.Component { templ_7745c5c3_Var26 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/dashboard/views/layout.templ b/dashboard/views/layout.templ index 06f8e7f..ca6455b 100644 --- a/dashboard/views/layout.templ +++ b/dashboard/views/layout.templ @@ -19,6 +19,11 @@ templ Layout(capture CaptureState) { devlog + + + + + diff --git a/dashboard/views/layout_templ.go b/dashboard/views/layout_templ.go index de75dad..b06293b 100644 --- a/dashboard/views/layout_templ.go +++ b/dashboard/views/layout_templ.go @@ -55,20 +55,72 @@ func Layout(capture CaptureState) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -76,7 +128,7 @@ func Layout(capture CaptureState) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -84,7 +136,7 @@ func Layout(capture CaptureState) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -92,30 +144,30 @@ func Layout(capture CaptureState) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if url := os.Getenv("REFRESH_LIVE_RELOAD_SCRIPT_URL"); url != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } From 912a2d52e53a7672448a0a650ad8ed82799daa3b Mon Sep 17 00:00:00 2001 From: Christopher Hlubek Date: Mon, 1 Dec 2025 18:00:18 +0100 Subject: [PATCH 06/22] ui: improve styling of event list --- collector/event_aggregator_test.go | 10 +- collector/event_storage.go | 5 +- collector/event_storage_test.go | 18 +- collector/http_server.go | 24 +- collector/http_server_test.go | 4 +- collector/session_context.go | 23 +- collector/session_context_test.go | 36 +- dashboard/handler.go | 304 +++++++++++---- dashboard/static/main.css | 28 ++ dashboard/styles/main.css | 1 + dashboard/views/dashboard.templ | 17 +- dashboard/views/dashboard_templ.go | 56 ++- dashboard/views/event-list.templ | 86 +++-- dashboard/views/event-list_templ.go | 530 ++++++++++++++------------ dashboard/views/header.templ | 18 +- dashboard/views/header_templ.go | 26 +- dashboard/views/helper.go | 22 ++ dashboard/views/layout.templ | 24 +- dashboard/views/layout_templ.go | 58 +-- dashboard/views/split-layout.templ | 2 +- dashboard/views/split-layout_templ.go | 2 +- 21 files changed, 831 insertions(+), 463 deletions(-) diff --git a/collector/event_aggregator_test.go b/collector/event_aggregator_test.go index c1c611b..ba541de 100644 --- a/collector/event_aggregator_test.go +++ b/collector/event_aggregator_test.go @@ -30,7 +30,7 @@ func TestEventAggregator_ShouldCapture_SessionModeMatch(t *testing.T) { storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) aggregator.RegisterStorage(storage) - ctx := collector.WithSessionID(context.Background(), sessionID) + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{sessionID}) assert.True(t, aggregator.ShouldCapture(ctx)) } @@ -44,7 +44,7 @@ func TestEventAggregator_ShouldCapture_SessionModeNoMatch(t *testing.T) { storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) aggregator.RegisterStorage(storage) - ctx := collector.WithSessionID(context.Background(), otherSessionID) + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{otherSessionID}) assert.False(t, aggregator.ShouldCapture(ctx)) } @@ -62,7 +62,7 @@ func TestEventAggregator_ShouldCapture_GlobalMode(t *testing.T) { // Even with a different session ID otherSessionID := uuid.Must(uuid.NewV4()) - ctx := collector.WithSessionID(context.Background(), otherSessionID) + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{otherSessionID}) assert.True(t, aggregator.ShouldCapture(ctx)) } @@ -81,7 +81,7 @@ func TestEventAggregator_CollectEvent_DispatchesToMatchingStorages(t *testing.T) // Event with session A should go to both storages // (A matches session, B is global) - ctx := collector.WithSessionID(context.Background(), sessionA) + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{sessionA}) aggregator.CollectEvent(ctx, "test event") eventsA := storageA.GetEvents(10) @@ -126,7 +126,7 @@ func TestEventAggregator_CollectEvent_NoCapture_NoDispatch(t *testing.T) { // Event with different session should not be captured otherSessionID := uuid.Must(uuid.NewV4()) - ctx := collector.WithSessionID(context.Background(), otherSessionID) + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{otherSessionID}) aggregator.CollectEvent(ctx, "test event") events := storage.GetEvents(10) diff --git a/collector/event_storage.go b/collector/event_storage.go index e3d23a4..31181f3 100644 --- a/collector/event_storage.go +++ b/collector/event_storage.go @@ -2,6 +2,7 @@ package collector import ( "context" + "slices" "github.com/gofrs/uuid" ) @@ -92,11 +93,11 @@ func (s *CaptureStorage) ShouldCapture(ctx context.Context) bool { case CaptureModeGlobal: return true case CaptureModeSession: - ctxSessionID, ok := SessionIDFromContext(ctx) + sessionIDs, ok := SessionIDsFromContext(ctx) if !ok { return false } - return ctxSessionID == s.sessionID + return slices.Contains(sessionIDs, s.sessionID) default: return false } diff --git a/collector/event_storage_test.go b/collector/event_storage_test.go index 7f8ffea..2249e60 100644 --- a/collector/event_storage_test.go +++ b/collector/event_storage_test.go @@ -29,7 +29,7 @@ func TestCaptureStorage_ShouldCapture_SessionMode_WrongSession(t *testing.T) { storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) defer storage.Close() - ctx := collector.WithSessionID(context.Background(), otherSessionID) + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{otherSessionID}) assert.False(t, storage.ShouldCapture(ctx)) } @@ -39,7 +39,19 @@ func TestCaptureStorage_ShouldCapture_SessionMode_MatchingSession(t *testing.T) storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) defer storage.Close() - ctx := collector.WithSessionID(context.Background(), sessionID) + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{sessionID}) + + assert.True(t, storage.ShouldCapture(ctx)) +} + +func TestCaptureStorage_ShouldCapture_SessionMode_MultipleSessionsInContext(t *testing.T) { + sessionID := uuid.Must(uuid.NewV4()) + otherSessionID := uuid.Must(uuid.NewV4()) + storage := collector.NewCaptureStorage(sessionID, 100, collector.CaptureModeSession) + defer storage.Close() + + // Context has multiple session IDs, including the one we're looking for + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{otherSessionID, sessionID}) assert.True(t, storage.ShouldCapture(ctx)) } @@ -54,7 +66,7 @@ func TestCaptureStorage_ShouldCapture_GlobalMode_AlwaysTrue(t *testing.T) { // Even with a different session ID otherSessionID := uuid.Must(uuid.NewV4()) - ctx := collector.WithSessionID(context.Background(), otherSessionID) + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{otherSessionID}) assert.True(t, storage.ShouldCapture(ctx)) } diff --git a/collector/http_server.go b/collector/http_server.go index f3adc1b..3ae7ca6 100644 --- a/collector/http_server.go +++ b/collector/http_server.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "net/http" + "strings" "sync" "time" @@ -17,8 +18,9 @@ func parseUUID(s string) (uuid.UUID, error) { return uuid.FromString(s) } -// SessionCookieName is the name of the cookie used to identify capture sessions -const SessionCookieName = "devlog_session" +// SessionCookiePrefix is the prefix for cookies used to identify capture sessions. +// Each session gets its own cookie named "devlog_session_{uuid}". +const SessionCookiePrefix = "devlog_session_" // HTTPServerOptions configures the HTTP server collector type HTTPServerOptions struct { @@ -133,13 +135,21 @@ func (c *HTTPServerCollector) Middleware(next http.Handler) http.Handler { ctx := r.Context() - // Extract session cookie and add to context - if cookie, err := r.Cookie(SessionCookieName); err == nil { - if sessionID, err := parseUUID(cookie.Value); err == nil { - ctx = WithSessionID(ctx, sessionID) - r = r.WithContext(ctx) + // Extract session IDs from cookies and add to context + // Each session has its own cookie named "devlog_session_{uuid}" + var sessionIDs []uuid.UUID + for _, cookie := range r.Cookies() { + if strings.HasPrefix(cookie.Name, SessionCookiePrefix) { + idStr := strings.TrimPrefix(cookie.Name, SessionCookiePrefix) + if id, err := parseUUID(idStr); err == nil { + sessionIDs = append(sessionIDs, id) + } } } + if len(sessionIDs) > 0 { + ctx = WithSessionIDs(ctx, sessionIDs) + r = r.WithContext(ctx) + } // Check if we should capture this request (using EventAggregator) // Default to true for backward compatibility when neither aggregator nor collector is set diff --git a/collector/http_server_test.go b/collector/http_server_test.go index c3f2077..f1042c1 100644 --- a/collector/http_server_test.go +++ b/collector/http_server_test.go @@ -941,8 +941,8 @@ func TestHTTPServerCollector_WithEventAggregator_SessionMode_Match(t *testing.T) req, err := http.NewRequest(http.MethodGet, server.URL+"/test", nil) require.NoError(t, err) req.AddCookie(&http.Cookie{ - Name: collector.SessionCookieName, - Value: sessionID.String(), + Name: collector.SessionCookiePrefix + sessionID.String(), + Value: "1", }) resp, err := client.Do(req) diff --git a/collector/session_context.go b/collector/session_context.go index 929e4e4..754e36a 100644 --- a/collector/session_context.go +++ b/collector/session_context.go @@ -6,20 +6,21 @@ import ( "github.com/gofrs/uuid" ) -type sessionIDKeyType struct{} +type sessionIDsKeyType struct{} -var sessionIDKey = sessionIDKeyType{} +var sessionIDsKey = sessionIDsKeyType{} -// WithSessionID returns a new context with the session ID added. -func WithSessionID(ctx context.Context, sessionID uuid.UUID) context.Context { - return context.WithValue(ctx, sessionIDKey, sessionID) +// WithSessionIDs returns a new context with the session IDs added. +// Multiple session IDs are used when multiple dashboard tabs have capture enabled. +func WithSessionIDs(ctx context.Context, sessionIDs []uuid.UUID) context.Context { + return context.WithValue(ctx, sessionIDsKey, sessionIDs) } -// SessionIDFromContext retrieves the session ID from the context. -// Returns the session ID and true if found, or uuid.Nil and false if not set. -func SessionIDFromContext(ctx context.Context) (uuid.UUID, bool) { - if sessionID, ok := ctx.Value(sessionIDKey).(uuid.UUID); ok { - return sessionID, true +// SessionIDsFromContext retrieves the session IDs from the context. +// Returns the session IDs and true if found, or nil and false if not set. +func SessionIDsFromContext(ctx context.Context) ([]uuid.UUID, bool) { + if sessionIDs, ok := ctx.Value(sessionIDsKey).([]uuid.UUID); ok { + return sessionIDs, true } - return uuid.Nil, false + return nil, false } diff --git a/collector/session_context_test.go b/collector/session_context_test.go index ae47781..7f70223 100644 --- a/collector/session_context_test.go +++ b/collector/session_context_test.go @@ -10,32 +10,46 @@ import ( "github.com/networkteam/devlog/collector" ) -func TestWithSessionID_AddsToContext(t *testing.T) { +func TestWithSessionIDs_AddsToContext(t *testing.T) { ctx := context.Background() sessionID := uuid.Must(uuid.NewV4()) - newCtx := collector.WithSessionID(ctx, sessionID) + newCtx := collector.WithSessionIDs(ctx, []uuid.UUID{sessionID}) - retrievedID, ok := collector.SessionIDFromContext(newCtx) + retrievedIDs, ok := collector.SessionIDsFromContext(newCtx) assert.True(t, ok) - assert.Equal(t, sessionID, retrievedID) + assert.Equal(t, []uuid.UUID{sessionID}, retrievedIDs) } -func TestSessionIDFromContext_NotSet(t *testing.T) { +func TestWithSessionIDs_MultipleIDs(t *testing.T) { ctx := context.Background() + sessionID1 := uuid.Must(uuid.NewV4()) + sessionID2 := uuid.Must(uuid.NewV4()) - retrievedID, ok := collector.SessionIDFromContext(ctx) + newCtx := collector.WithSessionIDs(ctx, []uuid.UUID{sessionID1, sessionID2}) + + retrievedIDs, ok := collector.SessionIDsFromContext(newCtx) + assert.True(t, ok) + assert.Len(t, retrievedIDs, 2) + assert.Contains(t, retrievedIDs, sessionID1) + assert.Contains(t, retrievedIDs, sessionID2) +} + +func TestSessionIDsFromContext_NotSet(t *testing.T) { + ctx := context.Background() + + retrievedIDs, ok := collector.SessionIDsFromContext(ctx) assert.False(t, ok) - assert.Equal(t, uuid.Nil, retrievedID) + assert.Nil(t, retrievedIDs) } -func TestSessionIDFromContext_Set(t *testing.T) { +func TestSessionIDsFromContext_Set(t *testing.T) { sessionID := uuid.Must(uuid.NewV4()) - ctx := collector.WithSessionID(context.Background(), sessionID) + ctx := collector.WithSessionIDs(context.Background(), []uuid.UUID{sessionID}) - retrievedID, ok := collector.SessionIDFromContext(ctx) + retrievedIDs, ok := collector.SessionIDsFromContext(ctx) assert.True(t, ok) - assert.Equal(t, sessionID, retrievedID) + assert.Equal(t, []uuid.UUID{sessionID}, retrievedIDs) } diff --git a/dashboard/handler.go b/dashboard/handler.go index 340cdaf..7acdc80 100644 --- a/dashboard/handler.go +++ b/dashboard/handler.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" "net/http" - "net/url" + "os" "slices" "sync" "time" @@ -92,42 +92,47 @@ func NewHandler(options HandlerOptions) *Handler { pathPrefix: options.PathPrefix, - mux: setHandlerOptions(options, truncateAfter, mux), + mux: mux, } // Start cleanup goroutine go handler.sessionCleanupLoop() - mux.HandleFunc("GET /{$}", handler.root) - mux.HandleFunc("GET /event-list", handler.getEventList) - mux.HandleFunc("DELETE /event-list", handler.clearEventList) - mux.HandleFunc("GET /event/{eventId}", handler.getEventDetails) - mux.HandleFunc("GET /events-sse", handler.getEventsSSE) - mux.HandleFunc("GET /download/request-body/{eventId}", handler.downloadRequestBody) - mux.HandleFunc("GET /download/response-body/{eventId}", handler.downloadResponseBody) + // Static assets (no session required) + mux.Handle("GET /static/", http.StripPrefix("/static", http.FileServerFS(static.Assets))) - // Capture control endpoints - mux.HandleFunc("POST /capture/start", handler.captureStart) - mux.HandleFunc("POST /capture/stop", handler.captureStop) - mux.HandleFunc("POST /capture/mode", handler.captureMode) - mux.HandleFunc("GET /capture/status", handler.captureStatus) + // Root redirect - creates new session and redirects + mux.HandleFunc("GET /{$}", handler.rootRedirect) + + // Session-scoped routes under /s/{sid}/ (the /s/ prefix avoids conflicts with /static/) + mux.HandleFunc("GET /s/{sid}/{$}", handler.root) + mux.HandleFunc("GET /s/{sid}/event-list", handler.getEventList) + mux.HandleFunc("DELETE /s/{sid}/event-list", handler.clearEventList) + mux.HandleFunc("GET /s/{sid}/event/{eventId}", handler.getEventDetails) + mux.HandleFunc("GET /s/{sid}/events-sse", handler.getEventsSSE) + mux.HandleFunc("GET /s/{sid}/download/request-body/{eventId}", handler.downloadRequestBody) + mux.HandleFunc("GET /s/{sid}/download/response-body/{eventId}", handler.downloadResponseBody) - mux.Handle("/static/", http.StripPrefix("/static", http.FileServerFS(static.Assets))) + // Capture control endpoints + mux.HandleFunc("POST /s/{sid}/capture/start", handler.captureStart) + mux.HandleFunc("POST /s/{sid}/capture/stop", handler.captureStop) + mux.HandleFunc("POST /s/{sid}/capture/mode", handler.captureMode) + mux.HandleFunc("GET /s/{sid}/capture/status", handler.captureStatus) + mux.HandleFunc("POST /s/{sid}/capture/cleanup", handler.captureCleanup) return handler } -func setHandlerOptions(options HandlerOptions, truncateAfter uint64, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - ctx = views.WithHandlerOptions(ctx, views.HandlerOptions{ - PathPrefix: options.PathPrefix, - TruncateAfter: truncateAfter, - }) - r = r.WithContext(ctx) - - next.ServeHTTP(w, r) +// withHandlerOptions is a helper to set HandlerOptions in context before rendering +func (h *Handler) withHandlerOptions(r *http.Request, sessionID string, captureActive bool, captureMode string) *http.Request { + ctx := views.WithHandlerOptions(r.Context(), views.HandlerOptions{ + PathPrefix: h.pathPrefix, + TruncateAfter: h.truncateAfter, + SessionID: sessionID, + CaptureActive: captureActive, + CaptureMode: captureMode, }) + return r.WithContext(ctx) } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -173,6 +178,7 @@ func (h *Handler) cleanupIdleSessions() { for sessionID, state := range h.sessions { if now.Sub(state.lastActive) > h.idleTimeout { + fmt.Fprintf(os.Stderr, "[DEBUG] %s: cleanupIdleSessions: cleaning up session %s, idle for %v\n", time.Now().Format(time.DateTime), sessionID, now.Sub(state.lastActive)) // Clean up this session if storage := h.eventAggregator.GetStorage(state.storageID); storage != nil { storage.Close() @@ -183,36 +189,24 @@ func (h *Handler) cleanupIdleSessions() { } } -// getSessionID extracts the session ID from the request cookie +// getSessionID extracts the session ID from the URL path parameter func (h *Handler) getSessionID(r *http.Request) (uuid.UUID, bool) { - cookie, err := r.Cookie(collector.SessionCookieName) - if err != nil { + sidStr := r.PathValue("sid") + if sidStr == "" { return uuid.Nil, false } - sessionID, err := uuid.FromString(cookie.Value) + sessionID, err := uuid.FromString(sidStr) if err != nil { return uuid.Nil, false } return sessionID, true } -// getOrCreateSessionID gets existing session ID or creates a new one -func (h *Handler) getOrCreateSessionID(w http.ResponseWriter, r *http.Request) uuid.UUID { - if sessionID, ok := h.getSessionID(r); ok { - return sessionID - } - - // Create new session ID - sessionID := uuid.Must(uuid.NewV4()) - h.setSessionCookie(w, sessionID) - return sessionID -} - -// setSessionCookie sets the session cookie +// setSessionCookie sets the session cookie for event filtering func (h *Handler) setSessionCookie(w http.ResponseWriter, sessionID uuid.UUID) { http.SetCookie(w, &http.Cookie{ - Name: collector.SessionCookieName, - Value: sessionID.String(), + Name: collector.SessionCookiePrefix + sessionID.String(), + Value: "1", Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, @@ -220,9 +214,9 @@ func (h *Handler) setSessionCookie(w http.ResponseWriter, sessionID uuid.UUID) { } // clearSessionCookie clears the session cookie -func (h *Handler) clearSessionCookie(w http.ResponseWriter) { +func (h *Handler) clearSessionCookie(w http.ResponseWriter, sessionID uuid.UUID) { http.SetCookie(w, &http.Cookie{ - Name: collector.SessionCookieName, + Name: collector.SessionCookiePrefix + sessionID.String(), Value: "", Path: "/", MaxAge: -1, @@ -257,10 +251,55 @@ func (h *Handler) updateSessionActivity(sessionID uuid.UUID) { h.sessionsMu.Unlock() } +// createSession creates a new capture session and returns the storage +func (h *Handler) createSession(sessionID uuid.UUID, mode collector.CaptureMode) *collector.CaptureStorage { + storage := collector.NewCaptureStorage(sessionID, h.storageCapacity, mode) + h.eventAggregator.RegisterStorage(storage) + + h.sessionsMu.Lock() + h.sessions[sessionID] = &sessionState{ + storageID: storage.ID(), + lastActive: time.Now(), + } + h.sessionsMu.Unlock() + + return storage +} + +// rootRedirect redirects to a new session +func (h *Handler) rootRedirect(w http.ResponseWriter, r *http.Request) { + sessionID := uuid.Must(uuid.NewV4()) + http.Redirect(w, r, fmt.Sprintf("%s/s/%s/", h.pathPrefix, sessionID), http.StatusTemporaryRedirect) +} + func (h *Handler) root(w http.ResponseWriter, r *http.Request) { - sessionID, _ := h.getSessionID(r) + sessionID, ok := h.getSessionID(r) + if !ok { + http.Error(w, "Invalid session ID", http.StatusBadRequest) + return + } + storage := h.getSessionStorage(sessionID) + // Check URL params for capture state - allows recreating session from URL + captureParam := r.URL.Query().Get("capture") + modeParam := r.URL.Query().Get("mode") + if modeParam == "" { + modeParam = "session" // default + } + + // Recreate session from URL params if needed + if storage == nil && captureParam == "true" { + mode := collector.CaptureModeSession + if modeParam == "global" { + mode = collector.CaptureModeGlobal + } + storage = h.createSession(sessionID, mode) + if mode == collector.CaptureModeSession { + h.setSessionCookie(w, sessionID) + } + } + var selectedEvent *collector.Event idStr := r.URL.Query().Get("id") if idStr != "" && storage != nil { @@ -271,7 +310,7 @@ func (h *Handler) root(w http.ResponseWriter, r *http.Request) { } event, exists := storage.GetEvent(eventID) if !exists { - http.Redirect(w, r, fmt.Sprintf("%s/", h.pathPrefix), http.StatusTemporaryRedirect) + http.Redirect(w, r, fmt.Sprintf("%s/s/%s/", h.pathPrefix, sessionID), http.StatusTemporaryRedirect) return } selectedEvent = event @@ -279,15 +318,21 @@ func (h *Handler) root(w http.ResponseWriter, r *http.Request) { var recentEvents []*collector.Event captureActive := false - captureMode := "session" + captureMode := modeParam if storage != nil { + h.updateSessionActivity(sessionID) recentEvents = h.loadRecentEvents(storage) captureActive = true if storage.CaptureMode() == collector.CaptureModeGlobal { captureMode = "global" + } else { + captureMode = "session" + // Re-set session cookie for session mode (cleared on beforeunload) + h.setSessionCookie(w, sessionID) } } + r = h.withHandlerOptions(r, sessionID.String(), captureActive, captureMode) templ.Handler( views.Dashboard(views.DashboardProps{ SelectedEvent: selectedEvent, @@ -303,10 +348,18 @@ func (h *Handler) getEventList(w http.ResponseWriter, r *http.Request) { storage := h.getSessionStorage(sessionID) var recentEvents []*collector.Event + captureActive := false + captureMode := "session" if storage != nil { recentEvents = h.loadRecentEvents(storage) + captureActive = true + if storage.CaptureMode() == collector.CaptureModeGlobal { + captureMode = "global" + } } + r = h.withHandlerOptions(r, sessionID.String(), captureActive, captureMode) + selectedStr := r.URL.Query().Get("selected") var selectedEventID *uuid.UUID if selectedStr != "" { @@ -320,34 +373,39 @@ func (h *Handler) getEventList(w http.ResponseWriter, r *http.Request) { views.EventList(views.EventListProps{ Events: recentEvents, SelectedEventID: selectedEventID, + CaptureActive: captureActive, + CaptureMode: captureMode, }), ).ServeHTTP(w, r) } func (h *Handler) clearEventList(w http.ResponseWriter, r *http.Request) { sessionID, _ := h.getSessionID(r) - if storage := h.getSessionStorage(sessionID); storage != nil { + storage := h.getSessionStorage(sessionID) + if storage != nil { storage.Clear() } - // Check if there's an id parameter in the current URL that needs to be removed to unselect an event - currentURL, _ := url.Parse(r.Header.Get("HX-Current-URL")) - if currentURL != nil && currentURL.Query().Get("id") != "" { - // Build URL preserving all query parameters except 'id' - query := r.URL.Query() - query.Del("id") - - redirectURL := fmt.Sprintf("%s/", h.pathPrefix) - if len(query) > 0 { - redirectURL += "?" + query.Encode() - } + // Keep capture active if storage exists + captureActive := storage != nil + captureMode := "session" + if storage != nil && storage.CaptureMode() == collector.CaptureModeGlobal { + captureMode = "global" + } - // Use HTMX header to update the URL client-side without the id parameter - w.Header().Set("HX-Push-Url", redirectURL) + r = h.withHandlerOptions(r, sessionID.String(), captureActive, captureMode) + opts := views.HandlerOptions{ + PathPrefix: h.pathPrefix, + SessionID: sessionID.String(), + CaptureActive: captureActive, + CaptureMode: captureMode, } + // Update URL to remove id parameter but preserve capture state + w.Header().Set("HX-Push-Url", opts.BuildEventDetailURL("")) + templ.Handler( - views.SplitLayout(views.EventList(views.EventListProps{}), views.EventDetailContainer(nil)), + views.SplitLayout(views.EventList(views.EventListProps{CaptureActive: captureActive, CaptureMode: captureMode}), views.EventDetailContainer(nil)), ).ServeHTTP(w, r) } @@ -359,6 +417,13 @@ func (h *Handler) getEventDetails(w http.ResponseWriter, r *http.Request) { return } + captureActive := true + captureMode := "session" + if storage.CaptureMode() == collector.CaptureModeGlobal { + captureMode = "global" + } + r = h.withHandlerOptions(r, sessionID.String(), captureActive, captureMode) + idStr := r.PathValue("eventId") eventID, err := uuid.FromString(idStr) if err != nil { @@ -381,15 +446,33 @@ func (h *Handler) getEventDetails(w http.ResponseWriter, r *http.Request) { func (h *Handler) getEventsSSE(w http.ResponseWriter, r *http.Request) { sessionID, hasSession := h.getSessionID(r) if !hasSession { - http.Error(w, "No session cookie", http.StatusUnauthorized) + http.Error(w, "Invalid session ID", http.StatusBadRequest) return } storage := h.getSessionStorage(sessionID) if storage == nil { - http.Error(w, "No capture session active", http.StatusNotFound) - return + // Session was cleaned up - recreate it (fresh and empty) + // Use mode from query param, default to session mode + mode := collector.CaptureModeSession + if r.URL.Query().Get("mode") == "global" { + mode = collector.CaptureModeGlobal + } + + storage = h.createSession(sessionID, mode) + + // Set cookie if session mode + if mode == collector.CaptureModeSession { + h.setSessionCookie(w, sessionID) + } + } + + // Set handler options in context for template rendering + captureMode := "session" + if storage.CaptureMode() == collector.CaptureModeGlobal { + captureMode = "global" } + r = h.withHandlerOptions(r, sessionID.String(), true, captureMode) // Update activity for this session h.updateSessionActivity(sessionID) @@ -411,11 +494,22 @@ func (h *Handler) getEventsSSE(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "event: keepalive\ndata: connected\n\n") w.(http.Flusher).Flush() + // Create a ticker to keep the session alive and send keepalive messages + // This prevents idle timeout while SSE connection is open + keepaliveTicker := time.NewTicker(h.idleTimeout / 2) + defer keepaliveTicker.Stop() + // Listen for new events and send them as SSE events for { select { case <-ctx.Done(): return // Client disconnected + case <-keepaliveTicker.C: + // Keep session alive while SSE is connected + h.updateSessionActivity(sessionID) + // Send keepalive to client + fmt.Fprintf(w, "event: keepalive\ndata: ping\n\n") + w.(http.Flusher).Flush() case event, ok := <-eventCh: if !ok { return // Channel closed @@ -564,8 +658,11 @@ type CaptureStatusResponse struct { // captureStart handles POST /capture/start - creates a new capture session func (h *Handler) captureStart(w http.ResponseWriter, r *http.Request) { - // Get or create session ID - sessionID := h.getOrCreateSessionID(w, r) + sessionID, ok := h.getSessionID(r) + if !ok { + http.Error(w, "Invalid session ID", http.StatusBadRequest) + return + } // Parse mode from request body (default to session mode) mode := collector.CaptureModeSession @@ -581,7 +678,7 @@ func (h *Handler) captureStart(w http.ResponseWriter, r *http.Request) { if storage := h.eventAggregator.GetStorage(state.storageID); storage != nil { mode = storage.(*collector.CaptureStorage).CaptureMode() } - h.respondWithCaptureState(w, r, true, mode) + h.respondWithCaptureState(w, r, sessionID, true, mode) return } @@ -598,14 +695,19 @@ func (h *Handler) captureStart(w http.ResponseWriter, r *http.Request) { } h.sessionsMu.Unlock() - h.respondWithCaptureState(w, r, true, mode) + // Set session cookie for event filtering (if session mode) + if mode == collector.CaptureModeSession { + h.setSessionCookie(w, sessionID) + } + + h.respondWithCaptureState(w, r, sessionID, true, mode) } // captureStop handles POST /capture/stop - stops capture and removes storage func (h *Handler) captureStop(w http.ResponseWriter, r *http.Request) { sessionID, hasSession := h.getSessionID(r) if !hasSession { - h.respondWithCaptureState(w, r, false, collector.CaptureModeSession) + h.respondWithCaptureState(w, r, sessionID, false, collector.CaptureModeSession) return } @@ -621,14 +723,17 @@ func (h *Handler) captureStop(w http.ResponseWriter, r *http.Request) { } h.sessionsMu.Unlock() - h.respondWithCaptureState(w, r, false, collector.CaptureModeSession) + // Clear session cookie + h.clearSessionCookie(w, sessionID) + + h.respondWithCaptureState(w, r, sessionID, false, collector.CaptureModeSession) } // captureMode handles POST /capture/mode - changes capture mode func (h *Handler) captureMode(w http.ResponseWriter, r *http.Request) { sessionID, hasSession := h.getSessionID(r) if !hasSession { - http.Error(w, "No session cookie", http.StatusUnauthorized) + http.Error(w, "Invalid session ID", http.StatusBadRequest) return } @@ -651,30 +756,40 @@ func (h *Handler) captureMode(w http.ResponseWriter, r *http.Request) { return } + oldMode := storage.CaptureMode() storage.SetCaptureMode(mode) - h.respondWithCaptureState(w, r, true, mode) + // Handle cookie based on mode change + if mode == collector.CaptureModeSession && oldMode != collector.CaptureModeSession { + // Switching to session mode: set cookie + h.setSessionCookie(w, sessionID) + } else if mode == collector.CaptureModeGlobal && oldMode == collector.CaptureModeSession { + // Switching from session to global: clear cookie + h.clearSessionCookie(w, sessionID) + } + + h.respondWithCaptureState(w, r, sessionID, true, mode) } // captureStatus handles GET /capture/status - returns current capture state func (h *Handler) captureStatus(w http.ResponseWriter, r *http.Request) { sessionID, hasSession := h.getSessionID(r) if !hasSession { - h.respondWithCaptureState(w, r, false, collector.CaptureModeSession) + h.respondWithCaptureState(w, r, sessionID, false, collector.CaptureModeSession) return } storage := h.getSessionStorage(sessionID) if storage == nil { - h.respondWithCaptureState(w, r, false, collector.CaptureModeSession) + h.respondWithCaptureState(w, r, sessionID, false, collector.CaptureModeSession) return } - h.respondWithCaptureState(w, r, true, storage.CaptureMode()) + h.respondWithCaptureState(w, r, sessionID, true, storage.CaptureMode()) } // respondWithCaptureState responds with capture state as HTML for HTMX or JSON for API -func (h *Handler) respondWithCaptureState(w http.ResponseWriter, r *http.Request, active bool, mode collector.CaptureMode) { +func (h *Handler) respondWithCaptureState(w http.ResponseWriter, r *http.Request, sessionID uuid.UUID, active bool, mode collector.CaptureMode) { modeStr := "session" if mode == collector.CaptureModeGlobal { modeStr = "global" @@ -682,6 +797,20 @@ func (h *Handler) respondWithCaptureState(w http.ResponseWriter, r *http.Request // Check if this is an HTMX request if r.Header.Get("HX-Request") == "true" { + r = h.withHandlerOptions(r, sessionID.String(), active, modeStr) + opts := views.HandlerOptions{ + PathPrefix: h.pathPrefix, + SessionID: sessionID.String(), + CaptureActive: active, + CaptureMode: modeStr, + } + + // Trigger event list refresh via HTMX response header + w.Header().Set("HX-Trigger", "capture-state-changed") + + // Update browser URL to reflect capture state + w.Header().Set("HX-Push-Url", opts.BuildEventDetailURL("")) + templ.Handler( views.CaptureControls(views.CaptureState{ Active: active, @@ -695,3 +824,20 @@ func (h *Handler) respondWithCaptureState(w http.ResponseWriter, r *http.Request w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(CaptureStatusResponse{Active: active, Mode: modeStr}) } + +// captureCleanup handles POST /capture/cleanup - called via sendBeacon on tab close/reload +func (h *Handler) captureCleanup(w http.ResponseWriter, r *http.Request) { + sessionID, ok := h.getSessionID(r) + if !ok { + // Silent success for beacon - no session to clean up + w.WriteHeader(http.StatusOK) + return + } + + // Only clear session cookie - don't delete storage + // Storage will be cleaned up by idle timeout or explicit stop + // Cookie will be re-set on page load if session is still active + h.clearSessionCookie(w, sessionID) + + w.WriteHeader(http.StatusOK) +} diff --git a/dashboard/static/main.css b/dashboard/static/main.css index 87251a1..a6a2a12 100644 --- a/dashboard/static/main.css +++ b/dashboard/static/main.css @@ -49,6 +49,7 @@ --color-devlog-cyan: #04F3F8; --color-header-bg: #0D0A29; --color-header-border: #1e2a4a; + --color-sidebar-bg: #5c5a67; } } @layer base { @@ -233,6 +234,12 @@ .mt-2 { margin-top: calc(var(--spacing) * 2); } + .mt-3 { + margin-top: calc(var(--spacing) * 3); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } .mt-6 { margin-top: calc(var(--spacing) * 6); } @@ -344,6 +351,9 @@ .gap-4 { gap: calc(var(--spacing) * 4); } + .gap-5 { + gap: calc(var(--spacing) * 5); + } .gap-x-2 { column-gap: calc(var(--spacing) * 2); } @@ -455,6 +465,9 @@ .bg-neutral-200 { background-color: var(--color-neutral-200); } + .bg-neutral-300 { + background-color: var(--color-neutral-300); + } .bg-neutral-700 { background-color: var(--color-neutral-700); } @@ -482,6 +495,9 @@ background-color: color-mix(in oklab, var(--color-red-950) 50%, transparent); } } + .bg-sidebar-bg { + background-color: var(--color-sidebar-bg); + } .bg-white { background-color: var(--color-white); } @@ -491,6 +507,12 @@ background-color: color-mix(in oklab, var(--color-white) 10%, transparent); } } + .bg-white\/50 { + background-color: color-mix(in srgb, #fff 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-white) 50%, transparent); + } + } .p-2 { padding: calc(var(--spacing) * 2); } @@ -518,6 +540,12 @@ .py-2 { padding-block: calc(var(--spacing) * 2); } + .pl-1 { + padding-left: calc(var(--spacing) * 1); + } + .pl-2 { + padding-left: calc(var(--spacing) * 2); + } .text-center { text-align: center; } diff --git a/dashboard/styles/main.css b/dashboard/styles/main.css index a107292..848855a 100644 --- a/dashboard/styles/main.css +++ b/dashboard/styles/main.css @@ -5,6 +5,7 @@ --color-devlog-cyan-glow: #63FCFF; --color-header-bg: #0D0A29; --color-header-border: #1e2a4a; + --color-sidebar-bg: #5c5a67; } /* Custom glow utilities for capture buttons - enhanced for dark background */ diff --git a/dashboard/views/dashboard.templ b/dashboard/views/dashboard.templ index 6e51282..ea76d5f 100644 --- a/dashboard/views/dashboard.templ +++ b/dashboard/views/dashboard.templ @@ -1,6 +1,7 @@ package views import ( + "fmt" "github.com/networkteam/devlog/collector" ) @@ -12,12 +13,24 @@ type DashboardProps struct { } templ Dashboard(props DashboardProps) { - {{ eventListProps := EventListProps{Events: props.Events} }} + {{ eventListProps := EventListProps{Events: props.Events, CaptureActive: props.CaptureActive, CaptureMode: props.CaptureMode} }} if props.SelectedEvent != nil { {{ eventListProps.SelectedEventID = &props.SelectedEvent.ID }} } {{ capture := CaptureState{Active: props.CaptureActive, Mode: props.CaptureMode} }} @Layout(capture) { - @SplitLayout(EventList(eventListProps), EventDetailContainer(props.SelectedEvent)) + @SplitLayout(EventListContainer(eventListProps), EventDetailContainer(props.SelectedEvent)) } } + +templ EventListContainer(props EventListProps) { + {{ opts := MustGetHandlerOptions(ctx) }} +
+ @EventList(props) +
+} diff --git a/dashboard/views/dashboard_templ.go b/dashboard/views/dashboard_templ.go index ef381da..fc83f25 100644 --- a/dashboard/views/dashboard_templ.go +++ b/dashboard/views/dashboard_templ.go @@ -9,6 +9,7 @@ import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" import ( + "fmt" "github.com/networkteam/devlog/collector" ) @@ -40,7 +41,7 @@ func Dashboard(props DashboardProps) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - eventListProps := EventListProps{Events: props.Events} + eventListProps := EventListProps{Events: props.Events, CaptureActive: props.CaptureActive, CaptureMode: props.CaptureMode} if props.SelectedEvent != nil { eventListProps.SelectedEventID = &props.SelectedEvent.ID } @@ -57,7 +58,7 @@ func Dashboard(props DashboardProps) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = SplitLayout(EventList(eventListProps), EventDetailContainer(props.SelectedEvent)).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = SplitLayout(EventListContainer(eventListProps), EventDetailContainer(props.SelectedEvent)).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -71,4 +72,55 @@ func Dashboard(props DashboardProps) templ.Component { }) } +func EventListContainer(props EventListProps) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var3 := templ.GetChildren(ctx) + if templ_7745c5c3_Var3 == nil { + templ_7745c5c3_Var3 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + opts := MustGetHandlerOptions(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = EventList(props).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + return nil + }) +} + var _ = templruntime.GeneratedTemplate diff --git a/dashboard/views/event-list.templ b/dashboard/views/event-list.templ index 16f9f04..eeb764f 100644 --- a/dashboard/views/event-list.templ +++ b/dashboard/views/event-list.templ @@ -17,33 +17,42 @@ import ( type EventListProps struct { SelectedEventID *uuid.UUID Events []*collector.Event + CaptureActive bool + CaptureMode string // "session" or "global" } templ EventList(props EventListProps) { -
    + for _, event := range props.Events { + @EventListItem(event, props.SelectedEventID) } - " - > - for _, event := range props.Events { - @EventListItem(event, props.SelectedEventID) - } -
+ + } else { +
+

Click Record to start capturing events

+
+ } } @@ -66,21 +75,32 @@ func isEventSelected(event *collector.Event, selectedEventID *uuid.UUID) bool { templ linkListItem(event *collector.Event, selectedEventID *uuid.UUID) { {{ isSelected := isEventSelected(event, selectedEventID) }} + {{ opts := MustGetHandlerOptions(ctx) }}
{ children... }
} +templ childEventList(children []*collector.Event, selectedEventID *uuid.UUID) { + if len(children) > 0 { +
    + for _, subEvent := range children { + @EventListItem(subEvent, selectedEventID) + } +
+ } +} + templ tagList(tags map[string]string) {
for _, key := range slices.Sorted(maps.Keys(tags)) { @@ -123,13 +143,7 @@ templ HTTPRequestListItem(event *collector.Event, selectedEventID *uuid.UUID) { @tagList(request.Tags) } } - if len(event.Children) > 0 { -
    - for _, subEvent := range event.Children { - @EventListItem(subEvent, selectedEventID) - } -
- } + @childEventList(event.Children, selectedEventID) } @@ -163,13 +177,7 @@ templ HTTPServerRequestListItem(event *collector.Event, selectedEventID *uuid.UU @tagList(request.Tags) } } - if len(event.Children) > 0 { -
    - for _, subEvent := range event.Children { - @EventListItem(subEvent, selectedEventID) - } -
- } + @childEventList(event.Children, selectedEventID) } diff --git a/dashboard/views/event-list_templ.go b/dashboard/views/event-list_templ.go index 18fb0c1..dab04e5 100644 --- a/dashboard/views/event-list_templ.go +++ b/dashboard/views/event-list_templ.go @@ -25,6 +25,8 @@ import ( type EventListProps struct { SelectedEventID *uuid.UUID Events []*collector.Event + CaptureActive bool + CaptureMode string // "session" or "global" } func EventList(props EventListProps) templ.Component { @@ -48,45 +50,53 @@ func EventList(props EventListProps) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, event := range props.Events { - templ_7745c5c3_Err = EventListItem(event, props.SelectedEventID).Render(ctx, templ_7745c5c3_Buffer) + opts := MustGetHandlerOptions(ctx) + if props.CaptureActive { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
      ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, event := range props.Events { + templ_7745c5c3_Err = EventListItem(event, props.SelectedEventID).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "

    Click Record to start capturing events

    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err } return nil }) @@ -165,28 +175,29 @@ func linkListItem(event *collector.Event, selectedEventID *uuid.UUID) templ.Comp } ctx = templ.ClearChildren(ctx) isSelected := isEventSelected(event, selectedEventID) + opts := MustGetHandlerOptions(ctx) var templ_7745c5c3_Var6 = []any{ - "p-3 hover:bg-neutral-100 cursor-pointer transition-colors [.selected]:bg-blue-50", + "p-3 bg-white hover:bg-neutral-100 cursor-pointer transition-colors [.selected]:bg-blue-50", templ.KV("selected", isSelected), } templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-swap=\"outerHTML\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -233,7 +244,7 @@ func linkListItem(event *collector.Event, selectedEventID *uuid.UUID) templ.Comp if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -241,7 +252,7 @@ func linkListItem(event *collector.Event, selectedEventID *uuid.UUID) templ.Comp }) } -func tagList(tags map[string]string) templ.Component { +func childEventList(children []*collector.Event, selectedEventID *uuid.UUID) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -262,43 +273,84 @@ func tagList(tags map[string]string) templ.Component { templ_7745c5c3_Var11 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "
") + if len(children) > 0 { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
    ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, subEvent := range children { + templ_7745c5c3_Err = EventListItem(subEvent, selectedEventID).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + return nil + }) +} + +func tagList(tags map[string]string) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr + } + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var12 := templ.GetChildren(ctx) + if templ_7745c5c3_Var12 == nil { + templ_7745c5c3_Var12 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, key := range slices.Sorted(maps.Keys(tags)) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 string - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(key) + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 88, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 108, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(tags[key]) + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(tags[key]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 88, Col: 126} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 108, Col: 126} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -322,18 +374,18 @@ func HTTPRequestListItem(event *collector.Event, selectedEventID *uuid.UUID) tem }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var14 := templ.GetChildren(ctx) - if templ_7745c5c3_Var14 == nil { - templ_7745c5c3_Var14 = templ.NopComponent + templ_7745c5c3_Var15 := templ.GetChildren(ctx) + if templ_7745c5c3_Var15 == nil { + templ_7745c5c3_Var15 = templ.NopComponent } ctx = templ.ClearChildren(ctx) request := event.Data.(collector.HTTPClientRequest) parsedURL, _ := url.Parse(request.URL) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "
  • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var15 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var16 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -345,120 +397,120 @@ func HTTPRequestListItem(event *collector.Event, selectedEventID *uuid.UUID) tem }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 = []any{badgeClasses(BadgeProps{ + var templ_7745c5c3_Var17 = []any{badgeClasses(BadgeProps{ Variant: BadgeVariantOutline, })} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...) + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(request.Method) + var templ_7745c5c3_Var19 string + templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(request.Method) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 106, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 126, Col: 40} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var19 = []any{badgeClasses(BadgeProps{ + var templ_7745c5c3_Var20 = []any{badgeClasses(BadgeProps{ Variant: BadgeVariantSuccess, })} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...) + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(request.StatusCode)) + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(request.StatusCode)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 113, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 133, Col: 58} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(parsedURL.Path) + var templ_7745c5c3_Var24 string + templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(parsedURL.Path) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 120, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 140, Col: 72} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var24 string - templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(parsedURL.Host) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(parsedURL.Host) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 121, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 141, Col: 73} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -470,27 +522,15 @@ func HTTPRequestListItem(event *collector.Event, selectedEventID *uuid.UUID) tem } return nil }) - templ_7745c5c3_Err = linkListItem(event, selectedEventID).Render(templ.WithChildren(ctx, templ_7745c5c3_Var15), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = linkListItem(event, selectedEventID).Render(templ.WithChildren(ctx, templ_7745c5c3_Var16), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if len(event.Children) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
      ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, subEvent := range event.Children { - templ_7745c5c3_Err = EventListItem(subEvent, selectedEventID).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + templ_7745c5c3_Err = childEventList(event.Children, selectedEventID).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -514,17 +554,17 @@ func HTTPServerRequestListItem(event *collector.Event, selectedEventID *uuid.UUI }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var25 := templ.GetChildren(ctx) - if templ_7745c5c3_Var25 == nil { - templ_7745c5c3_Var25 = templ.NopComponent + templ_7745c5c3_Var26 := templ.GetChildren(ctx) + if templ_7745c5c3_Var26 == nil { + templ_7745c5c3_Var26 = templ.NopComponent } ctx = templ.ClearChildren(ctx) request := event.Data.(collector.HTTPServerRequest) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
  • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var26 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var27 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -536,107 +576,107 @@ func HTTPServerRequestListItem(event *collector.Event, selectedEventID *uuid.UUI }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var27 = []any{badgeClasses(BadgeProps{ + var templ_7745c5c3_Var28 = []any{badgeClasses(BadgeProps{ Variant: BadgeVariantOutline, })} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var27...) + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var29 string - templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(request.Method) + var templ_7745c5c3_Var30 string + templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(request.Method) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 147, Col: 40} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 161, Col: 40} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var30 = []any{badgeClasses(BadgeProps{ + var templ_7745c5c3_Var31 = []any{badgeClasses(BadgeProps{ Variant: BadgeVariantSuccess, })} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var30...) + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var31...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(request.StatusCode)) + var templ_7745c5c3_Var33 string + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(strconv.Itoa(request.StatusCode)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 154, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 168, Col: 58} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\">
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var34 string - templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(request.Path) + var templ_7745c5c3_Var35 string + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(request.Path) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 161, Col: 70} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 175, Col: 70} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -648,27 +688,15 @@ func HTTPServerRequestListItem(event *collector.Event, selectedEventID *uuid.UUI } return nil }) - templ_7745c5c3_Err = linkListItem(event, selectedEventID).Render(templ.WithChildren(ctx, templ_7745c5c3_Var26), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = linkListItem(event, selectedEventID).Render(templ.WithChildren(ctx, templ_7745c5c3_Var27), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if len(event.Children) > 0 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
      ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, subEvent := range event.Children { - templ_7745c5c3_Err = EventListItem(subEvent, selectedEventID).Render(ctx, templ_7745c5c3_Buffer) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
    ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + templ_7745c5c3_Err = childEventList(event.Children, selectedEventID).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -692,17 +720,17 @@ func LogListItem(event *collector.Event, selectedEventID *uuid.UUID) templ.Compo }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var35 := templ.GetChildren(ctx) - if templ_7745c5c3_Var35 == nil { - templ_7745c5c3_Var35 = templ.NopComponent + templ_7745c5c3_Var36 := templ.GetChildren(ctx) + if templ_7745c5c3_Var36 == nil { + templ_7745c5c3_Var36 = templ.NopComponent } ctx = templ.ClearChildren(ctx) record := event.Data.(slog.Record) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
  • ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Var36 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var37 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -714,116 +742,116 @@ func LogListItem(event *collector.Event, selectedEventID *uuid.UUID) templ.Compo }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var37 = []any{badgeClasses(BadgeProps{ + var templ_7745c5c3_Var38 = []any{badgeClasses(BadgeProps{ Variant: logLevelToBadgeVariant(record.Level), })} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var37...) + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var38...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var39 string - templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(record.Level) + var templ_7745c5c3_Var40 string + templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(record.Level) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 187, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 195, Col: 38} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var41 string - templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(record.Message) + var templ_7745c5c3_Var42 string + templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(record.Message) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 194, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 202, Col: 72} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for attr := range iterSlogAttrs(record) { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var42 string - templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(attr.Key) + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(attr.Key) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 198, Col: 73} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 206, Col: 73} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var43 string - templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(attr.Value.String()) + var templ_7745c5c3_Var44 string + templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(attr.Value.String()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 198, Col: 149} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 206, Col: 149} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = linkListItem(event, selectedEventID).Render(templ.WithChildren(ctx, templ_7745c5c3_Var36), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = linkListItem(event, selectedEventID).Render(templ.WithChildren(ctx, templ_7745c5c3_Var37), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
  • ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -847,13 +875,13 @@ func DBQueryListItem(event *collector.Event, selectedEventID *uuid.UUID) templ.C }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var44 := templ.GetChildren(ctx) - if templ_7745c5c3_Var44 == nil { - templ_7745c5c3_Var44 = templ.NopComponent + templ_7745c5c3_Var45 := templ.GetChildren(ctx) + if templ_7745c5c3_Var45 == nil { + templ_7745c5c3_Var45 = templ.NopComponent } ctx = templ.ClearChildren(ctx) query := event.Data.(collector.DBQuery) - templ_7745c5c3_Var45 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_Var46 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) if !templ_7745c5c3_IsBuffer { @@ -865,78 +893,78 @@ func DBQueryListItem(event *collector.Event, selectedEventID *uuid.UUID) templ.C }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if len(query.Query) > 100 { - var templ_7745c5c3_Var46 string - templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(query.Query[:100]) + var templ_7745c5c3_Var47 string + templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(query.Query[:100]) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 211, Col: 35} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 219, Col: 35} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "...") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "...") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var47 string - templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(query.Query) + var templ_7745c5c3_Var48 string + templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(query.Query) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 213, Col: 29} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 221, Col: 29} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "
    Duration: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
    Duration: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var48 string - templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2fms", float64(query.Duration.Microseconds())/1000)) + var templ_7745c5c3_Var49 string + templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2fms", float64(query.Duration.Microseconds())/1000)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 217, Col: 90} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 225, Col: 90} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if query.Error != nil { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "Error: ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "Error: ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var49 string - templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(query.Error.Error()) + var templ_7745c5c3_Var50 string + templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinStringErrs(query.Error.Error()) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 219, Col: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/event-list.templ`, Line: 227, Col: 71} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "
    ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "
    ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } return nil }) - templ_7745c5c3_Err = linkListItem(event, selectedEventID).Render(templ.WithChildren(ctx, templ_7745c5c3_Var45), templ_7745c5c3_Buffer) + templ_7745c5c3_Err = linkListItem(event, selectedEventID).Render(templ.WithChildren(ctx, templ_7745c5c3_Var46), templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/dashboard/views/header.templ b/dashboard/views/header.templ index 6080a78..9411c7c 100644 --- a/dashboard/views/header.templ +++ b/dashboard/views/header.templ @@ -11,6 +11,7 @@ type CaptureState struct { } templ Header(capture CaptureState) { + {{ opts := MustGetHandlerOptions(ctx) }}
    @devlogLogo() @@ -23,7 +24,7 @@ templ Header(capture CaptureState) { Size: ButtonSizeIcon, }) } title="Clear list" - hx-delete={ fmt.Sprintf("%s/event-list", MustGetHandlerOptions(ctx).PathPrefix) } + hx-delete={ fmt.Sprintf("%s/s/%s/event-list", opts.PathPrefix, opts.SessionID) } hx-target="#split-layout" hx-swap="outerHTML" > @@ -35,6 +36,7 @@ templ Header(capture CaptureState) { } templ CaptureControls(capture CaptureState) { + {{ opts := MustGetHandlerOptions(ctx) }} {{ mode := capture.Mode }} if mode == "" { {{ mode = "session" }} @@ -43,15 +45,16 @@ templ CaptureControls(capture CaptureState) {
    @TapeButton(TapeButtonProps{Pressed: capture.Active, Color: TapeButtonColorRed}, templ.Attributes{ "title": "Start capture", - "hx-post": fmt.Sprintf("%s/capture/start", MustGetHandlerOptions(ctx).PathPrefix), + "hx-post": fmt.Sprintf("%s/s/%s/capture/start", opts.PathPrefix, opts.SessionID), "hx-vals": "js:{mode: document.getElementById('capture-controls').dataset.mode}", - "hx-on::after-request": "if(event.detail.successful) htmx.trigger('#event-list', 'sse:reconnect')", + "hx-on::after-request": "if(event.detail.successful) htmx.trigger('#event-list-container', 'capture-state-changed')", }) { @iconRecord() } @TapeButton(TapeButtonProps{Pressed: !capture.Active, Color: TapeButtonColorGray}, templ.Attributes{ - "title": "Stop capture", - "hx-post": fmt.Sprintf("%s/capture/stop", MustGetHandlerOptions(ctx).PathPrefix), + "title": "Stop capture", + "hx-post": fmt.Sprintf("%s/s/%s/capture/stop", opts.PathPrefix, opts.SessionID), + "hx-on::after-request": "if(event.detail.successful) htmx.trigger('#event-list-container', 'capture-state-changed')", }) { @iconStop() } @@ -111,12 +114,13 @@ func tapeButtonClasses(props TapeButtonProps) string { } templ CaptureMode(mode string, capturing bool) { + {{ opts := MustGetHandlerOptions(ctx) }}
    if capturing {