Files
bot-pontos/backend/internal/app/maintenance.go
2026-10-23 22:31:02 -03:00

125 lines
3.5 KiB
Go

package app
import (
"context"
"botpontos/internal/adapters/postgres"
"botpontos/internal/domain"
)
const stateRetentionLastRun = "retention_last_run"
// Maintenance é o ciclo de 5 min: falha os envios travados e, uma vez por dia
// no horário configurado, aplica a retenção de dados (9.10).
func (s *Service) Maintenance(ctx context.Context) error {
stuck, err := s.store.FailStuckSending(ctx, s.now().Add(-s.cfg.Jobs.SendingTimeout))
if err != nil {
return err
}
for _, n := range stuck {
s.audit(ctx, WorkerActor, &n.UserID, domain.EventInterruptedSending, false,
map[string]any{"notification_id": n.ID, "type": string(n.Type)})
}
return s.applyRetentionIfDue(ctx)
}
func (s *Service) applyRetentionIfDue(ctx context.Context) error {
now := s.now().In(s.location)
if now.Hour() != s.cfg.Jobs.RetentionHour {
return nil
}
today := s.TodayDate()
var last struct {
Day string `json:"day"`
}
if found, err := s.store.GetState(ctx, stateRetentionLastRun, &last); err != nil {
return err
} else if found && last.Day == today {
return nil
}
audit, err := s.store.DeleteAuditBefore(ctx, now.Add(-s.cfg.Jobs.RetentionAudit))
if err != nil {
return err
}
notificationsCutoff := domain.LocalDay(now.Add(-s.cfg.Jobs.RetentionNotifications), s.location)
notifications, err := s.store.DeleteNotificationsBefore(ctx, notificationsCutoff)
if err != nil {
return err
}
days, err := s.store.DeleteUserDaysBefore(ctx, notificationsCutoff)
if err != nil {
return err
}
sessions, err := s.store.DeleteSessionsEndedBefore(ctx, now.Add(-s.cfg.Jobs.RetentionSessions))
if err != nil {
return err
}
last.Day = today
if err := s.store.SetState(ctx, stateRetentionLastRun, last); err != nil {
return err
}
s.audit(ctx, WorkerActor, nil, domain.EventRetentionApplied, true, map[string]any{
"audit_logs": audit, "notifications": notifications, "user_days": days, "web_sessions": sessions,
})
return nil
}
// RotateKey recifra com a chave atual as credenciais gravadas com outra versão.
func (s *Service) RotateKey(ctx context.Context) (int, error) {
creds, err := s.store.ListCredentials(ctx)
if err != nil {
return 0, err
}
current := s.cipher.CurrentVersion()
rotated := 0
for _, c := range creds {
if c.KeyVersion == current {
continue
}
plain, err := s.cipher.Decrypt(c.EncryptedPassword, c.KeyVersion)
if err != nil {
return rotated, err
}
encrypted, version, err := s.cipher.Encrypt(plain)
if err != nil {
return rotated, err
}
if err := s.store.UpdateCredentialCipher(ctx, c.UserID, encrypted, version); err != nil {
return rotated, err
}
rotated++
}
s.audit(ctx, CLIActor, nil, domain.EventEncryptionKeyRotated, true, map[string]any{"credentials": rotated, "version": current})
return rotated, nil
}
// Overview reúne os dados do dashboard do painel.
type Overview struct {
Counts postgres.Dashboard
OutageSince *string
HolidayFallback *string
}
func (s *Service) Overview(ctx context.Context) (Overview, error) {
var o Overview
counts, err := s.store.Dashboard(ctx, s.TodayDate())
if err != nil {
return o, err
}
o.Counts = counts
var outage outageState
if found, _ := s.store.GetState(ctx, stateOutage, &outage); found {
since := outage.Since.Format("2006-01-02T15:04:05Z07:00")
o.OutageSince = &since
}
var fallback struct {
Day string `json:"day"`
}
if found, _ := s.store.GetState(ctx, stateHolidayFallbackNotified, &fallback); found && fallback.Day == s.TodayDate() {
o.HolidayFallback = &fallback.Day
}
return o, nil
}