package webapp import ( "strings" "sync" ) // LogBuffer is a small in-memory ring buffer the webapp uses to // expose the last N worker log lines on the Logs page. The worker // process feeds it via slog/JSON or by calling Append directly. // // Capacity is bounded; old entries are evicted FIFO. type LogBuffer struct { mu sync.RWMutex buf []string capN int offset int full bool } // NewLogBuffer returns an empty LogBuffer that holds up to capN // lines. capN <= 0 falls back to a sensible default. func NewLogBuffer(capN int) *LogBuffer { if capN <= 0 { capN = 5000 } return &LogBuffer{buf: make([]string, 0, capN), capN: capN} } // Append adds a single line to the buffer. Newlines are stripped so // multi-line log records do not split into separate rows. func (l *LogBuffer) Append(line string) { if l == nil { return } l.mu.Lock() defer l.mu.Unlock() line = strings.TrimRight(line, "\n") if line == "" { return } if len(l.buf) < l.capN { l.buf = append(l.buf, line) return } l.full = true l.buf[l.offset] = line l.offset = (l.offset + 1) % l.capN } // Tail returns the most recent n lines in chronological order. If n // is larger than the buffer capacity, only the held lines are // returned. n <= 0 returns an empty slice. func (l *LogBuffer) Tail(n int) []string { if l == nil || n <= 0 { return nil } l.mu.RLock() defer l.mu.RUnlock() size := len(l.buf) if size == 0 { return nil } if n > size { n = size } out := make([]string, n) if !l.full { // Buffer not yet wrapped: just slice the tail. copy(out, l.buf[size-n:]) return out } // Buffer is wrapped. The oldest line lives at l.offset; the // newest line lives at (offset - 1 + capN) % capN. idx := (l.offset - n + l.capN) % l.capN for i := 0; i < n; i++ { out[i] = l.buf[idx] idx = (idx + 1) % l.capN } return out } // Size reports the current number of stored lines. func (l *LogBuffer) Size() int { if l == nil { return 0 } l.mu.RLock() defer l.mu.RUnlock() return len(l.buf) } // Cap reports the maximum number of lines the buffer holds. func (l *LogBuffer) Cap() int { if l == nil { return 0 } return l.capN }