namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging; public class SiteEventLogOptions { /// Number of days to retain site event log entries before purge; default 30. public int RetentionDays { get; set; } = 30; /// /// Maximum size in megabytes of site_events AND ITS FOUR INDEXES before the oldest /// entries are trimmed; default 1024 MB. /// /// /// /// This is not a cap on the database file (register row 81, decision record /// docs/plans/2026-08-17-site-event-log-cap-semantics-decision.md). site_events /// shares one SQLite file at LocalDb:Path with nine other tables plus LocalDb's /// __localdb_oplog / __localdb_row_version bookkeeping. Measuring the whole file /// made the trim unwinnable on a replication-enabled node — each batch of deletes generated /// an equal number of oplog rows and tombstones in the same file, so the measured size never /// fell and the trim stopped 2.5x over cap. Worse, it made the event log's budget a function /// of the replication peer's health, in the direction where a replication fault destroys /// operational history. /// /// /// Consequence for sizing a site's data volume: this value alone does not bound it. LocalDb /// replication bookkeeping is separate overhead on the same disk, bounded by /// LocalDb:Replication:MaxOplogRows and TombstoneRetention. Budget the sum. /// /// /// The footprint is MODELLED from sampled row width rather than measured exactly: SQLite's /// dbstat virtual table would give an exact per-table page count but is not compiled /// into the bundled native library. Measured accuracy is −0.8 % to +12.8 %, deliberately /// biased high. See EventLogPurgeService.EstimateSiteEventsSize. /// /// public int MaxStorageMb { get; set; } = 1024; /// /// MIGRATION-ONLY now that no longer reads /// this: site_events lives in the consolidated site database at /// LocalDb:Path. The only remaining consumer is /// SiteLocalDbLegacyMigrator, which uses it to locate a pre-consolidation /// site_events.db and copy it in once. Safe to delete from a config once that /// node's migration has run. /// public string DatabasePath { get; set; } = "site_events.db"; /// Maximum number of rows returned per paginated query; default 500. public int QueryPageSize { get; set; } = 500; /// /// Hard upper bound on a caller-supplied PageSize. A /// misbehaving or hostile central client that requests int.MaxValue would /// otherwise force the query to materialise the entire log into a single list while /// holding the shared write lock. Silent clamp; default 500 matches /// . /// public int MaxQueryPageSize { get; set; } = 500; /// Interval between purge runs; default 24 hours. public TimeSpan PurgeInterval { get; set; } = TimeSpan.FromHours(24); /// /// Ceiling on how many site_events rows ONE purge tick may delete, across the /// retention purge and the storage-cap trim combined. Default 250 000; 0 means /// unlimited (the pre-pacing behaviour, kept as an escape hatch). /// /// /// /// Why a ceiling exists at all. site_events is CDC-captured on a /// replication-enabled node and there is deliberately no purge-exemption path, so a purge /// queues one oplog row per deleted event. Measured worst case (register row 34, /// docs/plans/2026-08-15-site-events-purge-burst-measurement.md): a 1 GB event log /// holds ~2.73 million rows at 393 bytes/row, and expiring all of them in one tick queued /// 2 731 947 oplog rows — 2.7x over LocalDb's MaxOplogRows default of /// 1 000 000. Crossing that ceiling makes LocalDb's maintenance tick prune the oplog /// and set needs_snapshot, which costs a FULL snapshot resync of all ten replicated /// tables. The ceiling keeps a single tick's burst safely inside that budget (250 000 is /// a 4x margin) while keeps retention converging. /// /// /// The remainder is not dropped: a tick that hits the ceiling schedules the next tick after /// instead of , so a backlog /// drains over minutes rather than days while every individual burst stays bounded. /// /// public int MaxPurgedRowsPerTick { get; set; } = 250_000; /// /// Delay before the next purge tick when the previous tick stopped at /// ; default 5 minutes. Ignored when the ceiling was not /// reached — an ordinary tick waits . /// /// /// Sized so the peer drains one tick's burst before the next one lands: 250 000 tombstones /// measured at ~10 800 rows/s over a loopback pair (≈23 s), leaving an order of magnitude of /// headroom for a real inter-node link. A worst-case 2.73 M-row backlog therefore converges /// in ~11 ticks ≈ 55 minutes instead of 11 days at the daily interval. /// public TimeSpan PurgeCatchUpInterval { get; set; } = TimeSpan.FromMinutes(5); /// /// Bound on the background write queue. Default 10 000 events. /// Overflow uses BoundedChannelFullMode.DropOldest — callers never block; the /// dropped event's Task is faulted and FailedWriteCount is incremented /// so the drop is observable. /// public int WriteQueueCapacity { get; set; } = 10_000; /// /// Widest time window a keyword search may scan; default 7 days. The keyword filter /// is a leading-wildcard LIKE over message/source that full-scans the /// window, so an unbounded search against a log at its storage cap walks millions of rows. /// A keyword query with no From (or a wider range) is clamped server-side to /// [To ?? now - window, To ?? now] rather than rejected, so existing callers keep /// working (review 2026-10-21 02/P1). /// public TimeSpan KeywordSearchMaxWindow { get; set; } = TimeSpan.FromDays(7); }