2026-06-24 16:08:48 +08:00
|
|
|
package app
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const maxMaildirRecentErrors = 10
|
|
|
|
|
|
|
|
|
|
type maildirSyncCounts struct {
|
|
|
|
|
FilesScanned int `json:"filesScanned"`
|
|
|
|
|
Imported int `json:"imported"`
|
|
|
|
|
Backfilled int `json:"backfilled"`
|
|
|
|
|
Cleaned int `json:"cleaned"`
|
|
|
|
|
FileErrors int `json:"fileErrors"`
|
|
|
|
|
fileErrorDetails []string `json:"-"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c maildirSyncCounts) total() int {
|
|
|
|
|
return c.Imported + c.Backfilled + c.Cleaned
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type maildirSyncRun struct {
|
|
|
|
|
StartedAt time.Time `json:"startedAt"`
|
|
|
|
|
FinishedAt *time.Time `json:"finishedAt,omitempty"`
|
|
|
|
|
DurationMs int64 `json:"durationMs"`
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
Error string `json:"error,omitempty"`
|
|
|
|
|
Counts maildirSyncCounts `json:"counts"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type maildirSyncHealthResponse struct {
|
|
|
|
|
Configured bool `json:"configured"`
|
|
|
|
|
Enabled bool `json:"enabled"`
|
|
|
|
|
Root string `json:"root"`
|
|
|
|
|
ScanSeconds int `json:"scanSeconds"`
|
|
|
|
|
WorkerStarted bool `json:"workerStarted"`
|
|
|
|
|
Running bool `json:"running"`
|
|
|
|
|
LastRun *maildirSyncRun `json:"lastRun,omitempty"`
|
|
|
|
|
LastError string `json:"lastError,omitempty"`
|
|
|
|
|
NextRunAt *time.Time `json:"nextRunAt,omitempty"`
|
|
|
|
|
RecentErrors []string `json:"recentErrors"`
|
|
|
|
|
Summary maildirSyncCounts `json:"summary"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type maildirSyncHealthTracker struct {
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
workerStarted bool
|
|
|
|
|
running bool
|
|
|
|
|
current *maildirSyncRun
|
|
|
|
|
lastRun *maildirSyncRun
|
|
|
|
|
lastError string
|
|
|
|
|
nextRunAt *time.Time
|
|
|
|
|
recentErrors []string
|
|
|
|
|
summary maildirSyncCounts
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func newMaildirSyncHealthTracker() *maildirSyncHealthTracker {
|
|
|
|
|
return &maildirSyncHealthTracker{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *maildirSyncHealthTracker) markWorkerStarted(nextRunAt *time.Time) {
|
|
|
|
|
if h == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
defer h.mu.Unlock()
|
|
|
|
|
h.workerStarted = true
|
|
|
|
|
h.nextRunAt = cloneTimePtr(nextRunAt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *maildirSyncHealthTracker) markWorkerStopped() {
|
|
|
|
|
if h == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
defer h.mu.Unlock()
|
|
|
|
|
h.workerStarted = false
|
|
|
|
|
h.nextRunAt = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *maildirSyncHealthTracker) markRunStarted(startedAt time.Time) {
|
|
|
|
|
if h == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
defer h.mu.Unlock()
|
|
|
|
|
run := &maildirSyncRun{StartedAt: startedAt.UTC(), Status: "running"}
|
|
|
|
|
h.running = true
|
|
|
|
|
h.current = run
|
|
|
|
|
h.lastRun = cloneMaildirSyncRun(run)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *maildirSyncHealthTracker) markRunFinished(finishedAt time.Time, counts maildirSyncCounts, err error, nextRunAt *time.Time) {
|
|
|
|
|
if h == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
defer h.mu.Unlock()
|
|
|
|
|
run := h.current
|
|
|
|
|
if run == nil {
|
|
|
|
|
run = &maildirSyncRun{StartedAt: finishedAt.UTC()}
|
|
|
|
|
}
|
|
|
|
|
finished := finishedAt.UTC()
|
|
|
|
|
run.FinishedAt = &finished
|
|
|
|
|
run.DurationMs = finished.Sub(run.StartedAt).Milliseconds()
|
|
|
|
|
run.Counts = counts
|
|
|
|
|
run.Status = "success"
|
|
|
|
|
run.Error = ""
|
|
|
|
|
if err != nil {
|
|
|
|
|
run.Status = "error"
|
|
|
|
|
run.Error = err.Error()
|
|
|
|
|
h.lastError = run.Error
|
|
|
|
|
h.pushRecentError(run.Error)
|
|
|
|
|
} else if counts.FileErrors > 0 {
|
|
|
|
|
run.Status = "partial"
|
|
|
|
|
if len(counts.fileErrorDetails) > 0 {
|
|
|
|
|
run.Error = counts.fileErrorDetails[0]
|
|
|
|
|
h.lastError = run.Error
|
|
|
|
|
}
|
|
|
|
|
for _, detail := range counts.fileErrorDetails {
|
|
|
|
|
h.pushRecentError(detail)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
h.lastError = ""
|
|
|
|
|
}
|
|
|
|
|
h.summary.FilesScanned += counts.FilesScanned
|
|
|
|
|
h.summary.Imported += counts.Imported
|
|
|
|
|
h.summary.Backfilled += counts.Backfilled
|
|
|
|
|
h.summary.Cleaned += counts.Cleaned
|
|
|
|
|
h.summary.FileErrors += counts.FileErrors
|
|
|
|
|
h.running = false
|
|
|
|
|
h.current = nil
|
|
|
|
|
h.lastRun = cloneMaildirSyncRun(run)
|
|
|
|
|
h.nextRunAt = cloneTimePtr(nextRunAt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *maildirSyncHealthTracker) snapshot(cfg Config) maildirSyncHealthResponse {
|
|
|
|
|
root := strings.TrimSpace(cfg.MaildirRoot)
|
|
|
|
|
scanSeconds := cfg.MaildirScanSeconds
|
|
|
|
|
if scanSeconds <= 0 {
|
|
|
|
|
scanSeconds = 30
|
|
|
|
|
}
|
|
|
|
|
out := maildirSyncHealthResponse{
|
|
|
|
|
Configured: root != "",
|
|
|
|
|
Enabled: root != "",
|
|
|
|
|
Root: root,
|
|
|
|
|
ScanSeconds: scanSeconds,
|
|
|
|
|
}
|
|
|
|
|
if h == nil {
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
defer h.mu.Unlock()
|
|
|
|
|
out.WorkerStarted = h.workerStarted
|
|
|
|
|
out.Running = h.running
|
|
|
|
|
out.LastRun = cloneMaildirSyncRun(h.lastRun)
|
|
|
|
|
out.LastError = h.lastError
|
|
|
|
|
out.NextRunAt = cloneTimePtr(h.nextRunAt)
|
|
|
|
|
out.RecentErrors = append([]string(nil), h.recentErrors...)
|
|
|
|
|
out.Summary = h.summary
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *maildirSyncHealthTracker) pushRecentError(value string) {
|
|
|
|
|
value = strings.TrimSpace(value)
|
|
|
|
|
if value == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.recentErrors = append([]string{value}, h.recentErrors...)
|
|
|
|
|
if len(h.recentErrors) > maxMaildirRecentErrors {
|
|
|
|
|
h.recentErrors = h.recentErrors[:maxMaildirRecentErrors]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cloneMaildirSyncRun(in *maildirSyncRun) *maildirSyncRun {
|
|
|
|
|
if in == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
out := *in
|
|
|
|
|
out.FinishedAt = cloneTimePtr(in.FinishedAt)
|
|
|
|
|
return &out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func cloneTimePtr(in *time.Time) *time.Time {
|
|
|
|
|
if in == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
out := in.UTC()
|
|
|
|
|
return &out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (a *App) handleMaildirSyncHealth(w http.ResponseWriter, r *http.Request) {
|
2026-08-03 19:51:27 +08:00
|
|
|
respondJSON(w, http.StatusOK, a.maildirHealth.snapshot(a.config()))
|
2026-06-24 16:08:48 +08:00
|
|
|
}
|