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
+167
View File
@@ -0,0 +1,167 @@
package main
import (
"context"
"flag"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/archivmail/config"
"github.com/archivmail/internal/api"
"github.com/archivmail/internal/audit"
"github.com/archivmail/internal/auth"
imapstore "github.com/archivmail/internal/imap"
"github.com/archivmail/internal/index"
"github.com/archivmail/internal/smtpd"
"github.com/archivmail/internal/storage"
"github.com/archivmail/internal/userstore"
)
func main() {
configPath := flag.String("config", "/etc/archivmail/config.yml", "path to config file")
flag.Parse()
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", "path", *configPath, "err", err)
os.Exit(1)
}
// Storage
mailStore, err := storage.New(cfg.Storage.StorePath)
if err != nil {
logger.Error("storage init failed", "err", err)
os.Exit(1)
}
// Index
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()
// User store
users, err := userstore.New(cfg.Database.DSN())
if err != nil {
logger.Error("userstore init failed", "err", err)
os.Exit(1)
}
defer users.Close()
// Audit log
audlog, err := audit.New(cfg.Database.DSN(), cfg.Audit.LogPath, logger)
if err != nil {
logger.Error("audit init failed", "err", err)
os.Exit(1)
}
defer audlog.Close()
// Seed default users on first run
if err := seedDefaultUsers(users, logger); err != nil {
logger.Error("seed users failed", "err", err)
}
// Auth manager
authMgr := auth.New(users, nil, cfg.API.Secret)
// API server
apiCfg := config.APIConfig{
Bind: cfg.API.Bind,
Secret: cfg.API.Secret,
}
srv := api.New(apiCfg, mailStore, idx, authMgr, users, audlog, logger)
bind := cfg.API.Bind
if bind == "" {
bind = fmt.Sprintf(":%d", cfg.Server.APIPort)
}
httpServer := &http.Server{
Addr: bind,
Handler: srv,
}
// Start SMTP daemon
if cfg.SMTP.Bind == "" {
cfg.SMTP.Bind = fmt.Sprintf(":%d", cfg.Server.SMTPPort)
}
smtpDaemon := smtpd.New(cfg.SMTP, mailStore, logger)
if err := smtpDaemon.Start(); err != nil {
logger.Error("SMTP daemon failed to start", "err", err)
os.Exit(1)
}
defer smtpDaemon.Stop()
// Wire SMTP daemon into API server for status endpoint
srv.SetSMTPDaemon(smtpDaemon)
// IMAP store + importer
imapSt, err := imapstore.New(cfg.Database.DSN(), cfg.API.Secret)
if err != nil {
logger.Error("imap store init failed", "err", err)
os.Exit(1)
}
defer imapSt.Close()
imapImp := imapstore.NewImporter(imapSt, mailStore, idx, logger)
srv.SetImap(imapSt, imapImp)
// Start HTTP API
go func() {
logger.Info("starting API server", "addr", bind)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("API server error", "err", err)
}
}()
// Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
httpServer.Shutdown(ctx)
}
// seedDefaultUsers creates default admin and auditor accounts if no users exist yet.
func seedDefaultUsers(users *userstore.Store, logger *slog.Logger) error {
all, err := users.List("")
if err != nil {
return fmt.Errorf("list users: %w", err)
}
if len(all) > 0 {
return nil // already seeded
}
defaults := []userstore.CreateUserRequest{
{Username: "admin", Email: "admin@archivmail.local", Password: "archivmailrockz", Role: userstore.RoleAdmin},
{Username: "auditor", Email: "auditor@archivmail.local", Password: "archivmailrockz", Role: userstore.RoleAuditor},
}
for _, req := range defaults {
if _, err := users.Create(req); err != nil {
return fmt.Errorf("create default user %s: %w", req.Username, err)
}
logger.Info("created default user", "username", req.Username, "role", req.Role)
}
logger.Warn("default users created — change passwords immediately!", "admin", "admin", "auditor", "auditor")
return nil
}