-
+
+
@leftContent
diff --git a/dashboard/views/split-layout_templ.go b/dashboard/views/split-layout_templ.go
index cb32e1b..a347ab7 100644
--- a/dashboard/views/split-layout_templ.go
+++ b/dashboard/views/split-layout_templ.go
@@ -29,7 +29,7 @@ func SplitLayout(leftContent, rightContent templ.Component) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/dashboard/views/usage_panel.templ b/dashboard/views/usage_panel.templ
new file mode 100644
index 0000000..7602710
--- /dev/null
+++ b/dashboard/views/usage_panel.templ
@@ -0,0 +1,47 @@
+package views
+
+import "fmt"
+
+templ UsagePanel() {
+ {{ opts := MustGetHandlerOptions(ctx) }}
+ ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func iconDatabase() 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_Var7 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var7 == nil {
+ templ_7745c5c3_Var7 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func iconUsers() 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_Var8 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var8 == nil {
+ templ_7745c5c3_Var8 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+var _ = templruntime.GeneratedTemplate
diff --git a/devlog.go b/devlog.go
index a140ceb..bb7ec27 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,44 +24,28 @@ 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 {
- // LogCapacity is the maximum number of log entries to keep.
- // Default: 0
- LogCapacity uint64
// LogOptions are the options for the log collector.
// Default: nil, will use collector.DefaultLogOptions()
LogOptions *collector.LogOptions
- // HTTPClientCapacity is the maximum number of HTTP client requests (outgoing) to keep.
- // Default: 0
- HTTPClientCapacity uint64
// HTTPClientOptions are the options for the HTTP client collector.
// Default: nil, will use collector.DefaultHTTPClientOptions()
HTTPClientOptions *collector.HTTPClientOptions
- // HTTPServerCapacity is the maximum number of HTTP server requests (incoming) to keep.
- // Default: 0
- HTTPServerCapacity uint64
// HTTPServerOptions are the options for the HTTP server collector.
// Default: nil, will use collector.DefaultHTTPServerOptions()
HTTPServerOptions *collector.HTTPServerOptions
- // DBQueryCapacity is the maximum number of database queries to keep.
- // Default: 0
- DBQueryCapacity uint64
// 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 +55,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,
+ logCollector: collector.NewLogCollectorWithOptions(logOptions),
+ httpClientCollector: collector.NewHTTPClientCollectorWithOptions(httpClientOptions),
+ httpServerCollector: collector.NewHTTPServerCollectorWithOptions(httpServerOptions),
+ dbQueryCollector: collector.NewDBQueryCollectorWithOptions(dbQueryOptions),
+ eventAggregator: eventAggregator,
}
return instance
}
@@ -137,12 +118,17 @@ func (i *Instance) CollectDBQuery() func(ctx context.Context, dbQuery collector.
return i.dbQueryCollector.Collect
}
-func (i *Instance) DashboardHandler(pathPrefix string) http.Handler {
- return dashboard.NewHandler(
- dashboard.HandlerOptions{
- EventCollector: i.eventCollector,
-
- PathPrefix: pathPrefix,
- },
- )
+// DashboardHandler creates a dashboard handler mounted at the given path prefix.
+// Use functional options from the dashboard package to customize behavior:
+//
+// dlog.DashboardHandler("/_devlog",
+// dashboard.WithStorageCapacity(5000),
+// dashboard.WithSessionIdleTimeout(time.Minute),
+// )
+func (i *Instance) DashboardHandler(pathPrefix string, opts ...dashboard.HandlerOption) http.Handler {
+ // Prepend WithPathPrefix to user-provided options
+ allOpts := append([]dashboard.HandlerOption{dashboard.WithPathPrefix(pathPrefix)}, opts...)
+ handler := dashboard.NewHandler(i.eventAggregator, allOpts...)
+ i.dashboardHandler = handler
+ return handler
}
diff --git a/devlog_e2e_test.go b/devlog_e2e_test.go
index bcc51b2..c34be4f 100644
--- a/devlog_e2e_test.go
+++ b/devlog_e2e_test.go
@@ -19,22 +19,16 @@ func TestE2E(t *testing.T) {
runtime.ReadMemStats(&memBefore)
dlog := devlog.NewWithOptions(devlog.Options{
- LogCapacity: 50,
- LogOptions: nil,
- HTTPClientCapacity: 10,
HTTPClientOptions: &collector.HTTPClientOptions{
MaxBodySize: 1024 * 1024,
CaptureRequestBody: true,
CaptureResponseBody: true,
},
- HTTPServerCapacity: 10,
HTTPServerOptions: &collector.HTTPServerOptions{
MaxBodySize: 1024 * 1024,
CaptureRequestBody: true,
CaptureResponseBody: true,
},
- DBQueryCapacity: 10,
- EventCapacity: 10,
})
mux := http.NewServeMux()
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 |
diff --git a/go.mod b/go.mod
index 61bc5f0..0233ce3 100644
--- a/go.mod
+++ b/go.mod
@@ -22,6 +22,7 @@ require (
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/natefinch/atomic v1.0.1 // indirect
@@ -29,6 +30,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/r3labs/sse/v2 v2.10.0 // indirect
github.com/rjeczalik/notify v0.9.3 // indirect
+ github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/spf13/cobra v1.9.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
@@ -39,6 +41,7 @@ require (
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.33.0 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 60ff1b1..3433fe5 100644
--- a/go.sum
+++ b/go.sum
@@ -22,6 +22,7 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -50,6 +51,9 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -67,10 +71,9 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
github.com/networkteam/refresh v1.15.0 h1:8qobXuU29Ic08WkJC+lt4GHtoe9gDHTSEmgBqG5S0rg=
github.com/networkteam/refresh v1.15.0/go.mod h1:J6iKbX8RO9eERGJ5yYm0gXQLy5MrsgtwiQJF7K80uoE=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -81,6 +84,9 @@ github.com/r3labs/sse/v2 v2.10.0/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktE
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -141,8 +147,8 @@ gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/go.work b/go.work
index 6302030..d38b1cd 100644
--- a/go.work
+++ b/go.work
@@ -2,6 +2,7 @@ go 1.23.8
use (
.
+ ./acceptance
./dbadapter/sqllogger
./example
)
diff --git a/go.work.sum b/go.work.sum
index 8566aae..16a7b35 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -5,14 +5,17 @@ github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a h1:2KLQMJ8msqoPHIPDu
github.com/aphistic/sweet v0.2.0 h1:I4z+fAUqvKfvZV/CHi5dV0QuwbmIvYYFDjG0Ss5QpAs=
github.com/aws/aws-sdk-go v1.20.6 h1:kmy4Gvdlyez1fV4kw5RYxZzWKVyuHZHgPWeU/YvRsV4=
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo=
+github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
+github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
+github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7 h1:K//n/AqR5HjG3qxbrBCL4vJPW0MVFSs9CPK1OOJdRME=
@@ -20,10 +23,11 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
-github.com/networkteam/go-sqllogger v0.4.0 h1:9hx7Zppj5KQ/l1vSEGLwe1s3viJbQKvyiN2+awPdvAs=
-github.com/networkteam/go-sqllogger v0.4.0/go.mod h1:2ByE01zTVNuAlPdVhP1g+ARpr8kk3PesK/mCYgO4s1I=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
+github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 h1:J1//5K/6QF10cZ59zLcVNFGmBfiSrH8Cho/lNrViK9s=
+github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/rogpeppe/fastuuid v1.1.0 h1:INyGLmTCMGFr6OVIb977ghJvABML2CMVjPoRfNDdYDo=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
@@ -33,12 +37,17 @@ github.com/smartystreets/gunit v1.0.0 h1:RyPDUFcJbvtXlhJPk7v+wnxZRY2EUokhEYl2EJO
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
+github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tj/go-buffer v1.1.0 h1:Lo2OsPHlIxXF24zApe15AbK3bJLAOvkkxEA6Ux4c47M=
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2 h1:eGaGNxrtoZf/mBURsnNQKDR7u50Klgcf2eFDQEnc8Bc=
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b h1:m74UWYy+HBs+jMFR9mdZU6shPewugMyH5+GV6LNgW8w=
github.com/tj/go-spin v1.1.0 h1:lhdWZsvImxvZ3q1C5OIB7d72DuOwP4O2NdBg9PyzNds=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
@@ -61,5 +70,6 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+ Loading...
+
+}
+
+templ UsagePanelContent(memory string, sessions int, maxSessions int) {
+
+
+}
+
+templ iconDatabase() {
+
+}
+
+templ iconUsers() {
+
+}
diff --git a/dashboard/views/usage_panel_templ.go b/dashboard/views/usage_panel_templ.go
new file mode 100644
index 0000000..aa68ed6
--- /dev/null
+++ b/dashboard/views/usage_panel_templ.go
@@ -0,0 +1,197 @@
+// Code generated by templ - DO NOT EDIT.
+
+// templ: version: v0.3.865
+package views
+
+//lint:file-ignore SA4006 This context is only used if a nested component is present.
+
+import "github.com/a-h/templ"
+import templruntime "github.com/a-h/templ/runtime"
+
+import "fmt"
+
+func UsagePanel() 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_Var1 := templ.GetChildren(ctx)
+ if templ_7745c5c3_Var1 == nil {
+ templ_7745c5c3_Var1 = templ.NopComponent
+ }
+ ctx = templ.ClearChildren(ctx)
+ opts := MustGetHandlerOptions(ctx)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
+ @iconDatabase()
+ { memory }
+
+
+ @iconUsers()
+
+ if maxSessions > 0 {
+ { fmt.Sprintf("%d/%d", sessions, maxSessions) }
+ } else {
+ { fmt.Sprintf("%d", sessions) }
+ }
+
+
+ Loading...
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ return nil
+ })
+}
+
+func UsagePanelContent(memory string, sessions int, maxSessions int) 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)
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = iconDatabase().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
+ }
+ var templ_7745c5c3_Var4 string
+ templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(memory)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/usage_panel.templ`, Line: 22, Col: 42}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = iconUsers().Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if maxSessions > 0 {
+ var templ_7745c5c3_Var5 string
+ templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", sessions, maxSessions))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/usage_panel.templ`, Line: 28, Col: 50}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ } else {
+ var templ_7745c5c3_Var6 string
+ templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", sessions))
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard/views/usage_panel.templ`, Line: 30, Col: 34}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "