From 0d5543a37100587cc1454696898f839d87366623 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Mon, 11 May 2026 09:03:00 +1000 Subject: [PATCH] feat: add static DashboardCache - file-timestamp-based cache invalidation for near-zero cost refreshes --- Services/DashboardCache.cs | 115 +++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 Services/DashboardCache.cs diff --git a/Services/DashboardCache.cs b/Services/DashboardCache.cs new file mode 100644 index 0000000..4aca88b --- /dev/null +++ b/Services/DashboardCache.cs @@ -0,0 +1,115 @@ +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; + } + } +}