M9.1: wire sync logging into all mutation paths

- Add LogClosedDayDelete and LogClosedWeekDelete to SyncStore
- Inject syncStore into EntryService; log Start, Stop, StopByID,
  Update, CreateInterval, Delete, AutoStopStalledEntries
- Inject syncStore into DayService; log CloseDay, MarkDay, ReopenDay,
  and the recomputeWeek closed-week upsert
- Inject syncStore into SettingsService; log Upsert, UpdateSettings,
  DeleteSettings
- Add LogClosedWeek/LogClosedWeekDelete calls in WeekService.CloseWeek
  and ReopenWeek
- Update main.go and all service test helpers for new constructor signatures
- All Go tests and 19 Vitest tests pass
This commit is contained in:
2026-04-30 22:57:02 +02:00
parent d8366f5c25
commit a8a4ea0d4f
9 changed files with 82 additions and 30 deletions

View File

@@ -26,22 +26,25 @@ var (
// EntryService handles business logic for time entries.
type EntryService struct {
entries *store.EntryStore
closedDays *store.ClosedDayStore
settings *store.SettingsStore
tz *time.Location
entries *store.EntryStore
closedDays *store.ClosedDayStore
settings *store.SettingsStore
syncStore *store.SyncStore
tz *time.Location
}
func NewEntryService(
entries *store.EntryStore,
closedDays *store.ClosedDayStore,
settings *store.SettingsStore,
syncStore *store.SyncStore,
tz *time.Location,
) *EntryService {
return &EntryService{
entries: entries,
closedDays: closedDays,
settings: settings,
syncStore: syncStore,
tz: tz,
}
}
@@ -94,6 +97,7 @@ func (s *EntryService) Start(ctx context.Context, note string) (*domain.Entry, e
if err := s.entries.Create(ctx, e); err != nil {
return nil, err
}
_ = s.syncStore.LogEntry(ctx, e)
return e, nil
}
@@ -132,7 +136,6 @@ func (s *EntryService) stopEntry(ctx context.Context, e *domain.Entry, autoStopp
startDayKey := s.dayKeyForMs(e.StartTime)
endDayKey := s.dayKeyForMs(endMs)
if endDayKey != startDayKey {
// Cross-midnight: cap at end of start day
endMs = s.midnightEndMs(e.StartTime)
autoStopped = true
}
@@ -144,6 +147,7 @@ func (s *EntryService) stopEntry(ctx context.Context, e *domain.Entry, autoStopp
if err := s.entries.Update(ctx, e); err != nil {
return nil, err
}
_ = s.syncStore.LogEntry(ctx, e)
return e, nil
}
@@ -188,7 +192,6 @@ func (s *EntryService) Update(ctx context.Context, id string, input UpdateEntryI
if input.StartTime != nil {
newDayKey := s.dayKeyForMs(*input.StartTime)
// Reject if the new start_time moves the entry into the future.
if newDayKey > s.dayKeyForMs(s.nowMs()) {
return nil, ErrFutureDay
}
@@ -196,7 +199,6 @@ func (s *EntryService) Update(ctx context.Context, id string, input UpdateEntryI
e.DayKey = newDayKey
}
if input.EndTime != nil {
// Validate same-day
startDayKey := s.dayKeyForMs(e.StartTime)
endDayKey := s.dayKeyForMs(*input.EndTime)
if startDayKey != endDayKey {
@@ -215,6 +217,7 @@ func (s *EntryService) Update(ctx context.Context, id string, input UpdateEntryI
if err := s.entries.Update(ctx, e); err != nil {
return nil, err
}
_ = s.syncStore.LogEntry(ctx, e)
return e, nil
}
@@ -226,7 +229,6 @@ type CreateIntervalInput struct {
}
// CreateInterval adds a completed interval with explicit start and end times.
// Rules: same-day, end > start, day not closed, day not in the future.
func (s *EntryService) CreateInterval(ctx context.Context, input CreateIntervalInput) (*domain.Entry, error) {
if input.EndTime <= input.StartTime {
return nil, fmt.Errorf("end_time must be after start_time")
@@ -238,13 +240,11 @@ func (s *EntryService) CreateInterval(ctx context.Context, input CreateIntervalI
return nil, ErrCrossesMidnight
}
// Reject future intervals.
todayKey := s.dayKeyForMs(s.nowMs())
if startDayKey > todayKey {
return nil, ErrFutureDay
}
// Check day is not closed.
closed, err := s.closedDays.GetByDayKey(ctx, startDayKey)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
@@ -265,18 +265,34 @@ func (s *EntryService) CreateInterval(ctx context.Context, input CreateIntervalI
if err := s.entries.Create(ctx, e); err != nil {
return nil, err
}
_ = s.syncStore.LogEntry(ctx, e)
return e, nil
}
// Delete soft-deletes an entry.
func (s *EntryService) Delete(ctx context.Context, id string) error {
nowMs := s.nowMs()
return s.entries.SoftDelete(ctx, id, nowMs)
if err := s.entries.SoftDelete(ctx, id, nowMs); err != nil {
return err
}
_ = s.syncStore.LogEntryDelete(ctx, id)
return nil
}
// AutoStopStalledEntries stops any running entries whose day_key is before today.
// Called by the midnight background goroutine.
func (s *EntryService) AutoStopStalledEntries(ctx context.Context) ([]string, error) {
today := s.dayKeyForMs(s.nowMs())
return s.entries.StopAllRunningBefore(ctx, today, s.nowMs(), s.nowMs())
ids, err := s.entries.StopAllRunningBefore(ctx, today, s.nowMs(), s.nowMs())
if err != nil {
return nil, err
}
// Log each auto-stopped entry to sync.
for _, id := range ids {
e, err := s.entries.GetByID(ctx, id)
if err == nil {
_ = s.syncStore.LogEntry(ctx, e)
}
}
return ids, nil
}