using Disco.Plugins.ServiceTracker.Models; using System; using System.IO; namespace Disco.Plugins.ServiceTracker.Services { /// /// Static in-memory cache for the dashboard model. /// Instead of running full EF queries + JSON reads + Google Sheet fetches on every page load, /// this checks file modification timestamps to detect changes. /// If nothing changed, returns the cached model instantly (near-zero CPU/memory cost). /// public static class DashboardCache { private static DashboardViewModel _cachedModel; private static DateTime _cacheBuiltAt = DateTime.MinValue; private static DateTime _ticketsFileTime = DateTime.MinValue; private static DateTime _externalFileTime = DateTime.MinValue; private static DateTime _sheetCacheTime = DateTime.MinValue; private static DateTime _configFileTime = DateTime.MinValue; private static string _lastSortBy; private static string _lastFilter; private static readonly object _lock = new object(); /// /// Maximum age of cache in seconds before forced rebuild, even if no files changed. /// Acts as a safety net for database-only changes (e.g. Disco job opened/closed externally). /// public static int MaxCacheAgeSeconds = 30; /// /// Returns cached model if still valid, or null if rebuild is needed. /// A rebuild is needed when: /// - Cache doesn't exist yet /// - Any data file has been modified since last build /// - Cache is older than MaxCacheAgeSeconds /// - Sort/filter parameters changed /// public static DashboardViewModel GetIfValid(string dataDirectory, string sortBy, string filterKey) { lock (_lock) { if (_cachedModel == null) return null; // Different sort/filter = must rebuild if (_lastSortBy != sortBy || _lastFilter != filterKey) return null; // Check age var age = (DateTime.Now - _cacheBuiltAt).TotalSeconds; if (age > MaxCacheAgeSeconds) return null; // Check if any data files have been modified if (HasFileChanged(Path.Combine(dataDirectory, "tickets.json"), ref _ticketsFileTime)) return null; if (HasFileChanged(Path.Combine(dataDirectory, "external_tickets.json"), ref _externalFileTime)) return null; if (HasFileChanged(Path.Combine(dataDirectory, "sheet_cache.csv"), ref _sheetCacheTime)) return null; if (HasFileChanged(Path.Combine(dataDirectory, "config.json"), ref _configFileTime)) return null; return _cachedModel; } } /// /// Store a freshly built model in the cache. /// public static void Store(DashboardViewModel model, string dataDirectory, string sortBy, string filterKey) { lock (_lock) { _cachedModel = model; _cacheBuiltAt = DateTime.Now; _lastSortBy = sortBy; _lastFilter = filterKey; // Snapshot current file times _ticketsFileTime = GetFileTime(Path.Combine(dataDirectory, "tickets.json")); _externalFileTime = GetFileTime(Path.Combine(dataDirectory, "external_tickets.json")); _sheetCacheTime = GetFileTime(Path.Combine(dataDirectory, "sheet_cache.csv")); _configFileTime = GetFileTime(Path.Combine(dataDirectory, "config.json")); } } /// /// Force invalidate the cache (called after writes if needed). /// public static void Invalidate() { lock (_lock) { _cachedModel = null; _cacheBuiltAt = DateTime.MinValue; } } private static bool HasFileChanged(string path, ref DateTime lastKnownTime) { var currentTime = GetFileTime(path); if (currentTime != lastKnownTime) { lastKnownTime = currentTime; return true; } return false; } private static DateTime GetFileTime(string path) { try { if (File.Exists(path)) return File.GetLastWriteTimeUtc(path); } catch { } return DateTime.MinValue; } } }