Phase 7: monitoring UI — send log, queue, mail.log tail
Three HTMX-polled monitoring screens (spec 7.2.11-13): send log with server-side domain/application filters and pagination, Postfix queue (postqueue -p), and a mail.log tail. Fragment endpoints return HTML snippets, not JSON (spec 7.1); all output is auto-escaped via html/template (spec 7.6.7). Adds store.QuerySendLog/CountSendLog/ListApplicationLogins, postfix.Queue(), and logtail.TailLines (a point-in-time reverse read, independent of the background follow loop). Verified on the dev server: gofmt/vet/test green, docker build green, container e2e (filters, 60-row pagination, <script> escaping, real postqueue/mail.log output, existing Reload button unaffected). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ package logtail
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"log"
|
||||
@@ -118,6 +119,52 @@ func retentionLoop(ctx context.Context, st StatusStore, retentionDays int) {
|
||||
}
|
||||
}
|
||||
|
||||
// TailLines returns up to n of the most recent lines from path, for the
|
||||
// panel's mail.log monitoring view (spec 7.2.13). It is a one-shot,
|
||||
// point-in-time read on request — unrelated to the background follow loop
|
||||
// above — that reads backwards in chunks so it stays cheap against a
|
||||
// multi-megabyte log rather than reading the whole file every poll.
|
||||
func TailLines(path string, n int) ([]string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
const chunkSize = 8192
|
||||
var (
|
||||
buf []byte
|
||||
offset = info.Size()
|
||||
)
|
||||
for offset > 0 && bytes.Count(buf, []byte("\n")) <= n {
|
||||
size := int64(chunkSize)
|
||||
if size > offset {
|
||||
size = offset
|
||||
}
|
||||
offset -= size
|
||||
chunk := make([]byte, size)
|
||||
if _, err := f.ReadAt(chunk, offset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(chunk, buf...)
|
||||
}
|
||||
|
||||
text := strings.TrimRight(string(buf), "\n")
|
||||
if text == "" {
|
||||
return nil, nil
|
||||
}
|
||||
lines := strings.Split(text, "\n")
|
||||
if len(lines) > n {
|
||||
lines = lines[len(lines)-n:]
|
||||
}
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
// follow tails path line by line, calling handle for each complete line, until
|
||||
// ctx is cancelled. It starts at end-of-file (so a restart does not reprocess
|
||||
// history) and reopens the file when it is rotated (inode change from
|
||||
|
||||
Reference in New Issue
Block a user