feat: ticketing sync workers with idempotent import and orphan flagging (phase 6)

- task domain model + exhaustive §4.4 status machine tests (single source of truth)
- per-customer polling workers reconciled every 30s from the customers
  collection; panic-safe runs; manual sync-now trigger; status provider
  for the admin panel
- §5.3 upsert keyed on (system, key, customerId): refreshes content while
  status=imported, records upstream_changed timeline + notification after
- attachments cached into GridFS via connector-authorized downloads
- orphaned tickets flagged with timeline + consultant notification, never
  deleted; idempotent across polls
- consultant identity from users.extra.ticketingIdentities per system
- notifications store (insert/list/unread-count/mark-read)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
etalon
2026-06-12 19:03:13 +02:00
parent 458ba6a7ee
commit 4e01b64bbd
12 changed files with 1461 additions and 1 deletions
+13
View File
@@ -22,6 +22,19 @@ func (s *Server) SetPerformerInfo(b BreakerInfo) { s.performer = b }
// SetSyncTrigger wires the sync manager's "sync now" entry point.
func (s *Server) SetSyncTrigger(f func(customerID string)) { s.syncTrigger = f }
// Publish forwards a live event to connected clients. Until the WebSocket
// hub lands this is a metrics-only no-op, so subsystems can emit events
// unconditionally.
func (s *Server) Publish(channel, event string, payload any) {
s.metrics.Inc("events_published_total", 1)
if s.publishFn != nil {
s.publishFn(channel, event, payload)
}
}
// SetPublishFn wires the actual hub broadcast.
func (s *Server) SetPublishFn(f func(channel, event string, payload any)) { s.publishFn = f }
func (s *Server) SetSyncStatusProvider(p StatusProvider) {
s.syncProvider = p
}
+1
View File
@@ -38,6 +38,7 @@ type Server struct {
syncProvider StatusProvider
jobsProvider StatusProvider
syncTrigger func(customerID string)
publishFn func(channel, event string, payload any)
}
func New(cfg *config.Config, log *slog.Logger, reg *metrics.Registry, st *store.Store, fs *files.Store) *Server {