feat(PROJ-17): Admin Dashboard Systemauslastung immer anzeigen

- Systemauslastungs-Sektion wird immer gerendert (nicht nur bei Erfolg)
- Fehlermeldung wenn /api/admin/system/stats nicht erreichbar ist
- Feature-Status auf In Review gesetzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-14 11:43:19 +01:00
parent a893084a88
commit d360c9a5ba
68 changed files with 11938 additions and 435 deletions
+148
View File
@@ -0,0 +1,148 @@
package main
import (
"flag"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/archivmail/config"
"github.com/archivmail/internal/index"
"github.com/archivmail/internal/storage"
"github.com/archivmail/pkg/mailparser"
)
func main() {
configPath := flag.String("config", "/etc/archivmail/config.yml", "path to config file")
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "usage: archivmail-import --config <path> <directory-or-file>")
os.Exit(1)
}
target := args[0]
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
cfg, err := config.Load(*configPath)
if err != nil {
logger.Error("failed to load config", "err", err)
os.Exit(1)
}
mailStore, err := storage.New(cfg.Storage.StorePath)
if err != nil {
logger.Error("storage init failed", "err", err)
os.Exit(1)
}
indexBackend := cfg.Index.Backend
if indexBackend == "" {
indexBackend = "xapian"
}
batchSize := cfg.Index.BatchSize
if batchSize <= 0 {
batchSize = 100
}
idx, err := index.New(cfg.Index.Path, batchSize, indexBackend)
if err != nil {
logger.Error("index init failed", "err", err)
os.Exit(1)
}
defer idx.Close()
var emlFiles []string
info, err := os.Stat(target)
if err != nil {
logger.Error("target not found", "path", target, "err", err)
os.Exit(1)
}
if info.IsDir() {
err = filepath.Walk(target, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.IsDir() && strings.HasSuffix(strings.ToLower(fi.Name()), ".eml") {
emlFiles = append(emlFiles, path)
}
return nil
})
if err != nil {
logger.Error("walk failed", "err", err)
os.Exit(1)
}
} else {
emlFiles = []string{target}
}
logger.Info("found EML files", "count", len(emlFiles))
imported := 0
skipped := 0
errors := 0
for i, path := range emlFiles {
raw, err := os.ReadFile(path)
if err != nil {
logger.Error("read file failed", "path", path, "err", err)
errors++
continue
}
pm, err := mailparser.Parse(raw)
if err != nil {
logger.Error("parse failed", "path", path, "err", err)
errors++
continue
}
id, err := mailStore.Save(raw, pm.Date)
if err != nil {
logger.Error("save failed", "path", path, "err", err)
errors++
continue
}
// Build attachment names list
var attachNames []string
for _, att := range pm.Attachments {
attachNames = append(attachNames, att.Filename)
}
doc := index.MailDocument{
ID: id,
From: pm.From,
To: strings.Join(pm.To, " "),
Subject: pm.Subject,
Body: pm.TextBody + " " + pm.HTMLBody,
AttachNames: strings.Join(attachNames, " "),
HasAttachment: len(pm.Attachments) > 0,
Date: pm.Date,
Size: int64(len(raw)),
}
if err := idx.IndexSync(doc); err != nil {
logger.Error("index failed", "id", id, "err", err)
errors++
continue
}
imported++
if (i+1)%100 == 0 || i+1 == len(emlFiles) {
fmt.Printf("Progress: %d/%d (imported: %d, skipped: %d, errors: %d)\n",
i+1, len(emlFiles), imported, skipped, errors)
}
}
logger.Info("import complete",
"total", len(emlFiles),
"imported", imported,
"skipped", skipped,
"errors", errors,
)
}