From d776001b356341994134bafe95ea14ba4a8665aa Mon Sep 17 00:00:00 2001 From: jessikitty Date: Sat, 9 May 2026 14:17:36 +1000 Subject: [PATCH] fix: hash RAW cell text for stable IDs, add rawIssue to hash, use DateTime.MinValue not DateTime.Now for unparseable dates --- Services/GoogleSheetService.cs | 116 ++++++++++++--------------------- 1 file changed, 40 insertions(+), 76 deletions(-) diff --git a/Services/GoogleSheetService.cs b/Services/GoogleSheetService.cs index 49463e3..2e8d05a 100644 --- a/Services/GoogleSheetService.cs +++ b/Services/GoogleSheetService.cs @@ -28,7 +28,6 @@ namespace Disco.Plugins.ServiceTracker.Services } string csvData = null; - if (File.Exists(_cachePath)) { var cacheAge = DateTime.Now - File.GetLastWriteTime(_cachePath); @@ -46,50 +45,25 @@ namespace Disco.Plugins.ServiceTracker.Services url = "https://docs.google.com/spreadsheets/d/e/" + id + "/pub?output=csv&gid=" + _config.GId; else url = "https://docs.google.com/spreadsheets/d/" + id + "/export?format=csv&gid=" + _config.GId; - using (var client = new WebClient()) { client.Encoding = Encoding.UTF8; client.Headers.Add("User-Agent", "Mozilla/5.0 DiscoServiceTracker/1.0"); csvData = client.DownloadString(url); } - if (csvData != null && csvData.TrimStart().StartsWith(" - /// Generates a deterministic hash from the row's key fields. - /// Same timestamp + email + task always produces the same number. + /// Hash from RAW cell text - same spreadsheet cells always produce the same number. + /// Uses raw strings, never parsed values, so DateTime.Now never contaminates the hash. /// - private int GenerateStableHash(DateTime timestamp, string email, string task) + private int GenerateStableHash(string rawTimestamp, string rawEmail, string rawTask, string rawIssue) { - var key = timestamp.ToString("yyyyMMddHHmmss") + "|" - + (email ?? "").ToLower().Trim() + "|" - + (task ?? "").ToLower().Trim(); + var key = (rawTimestamp ?? "").Trim().ToLower() + "|" + + (rawEmail ?? "").Trim().ToLower() + "|" + + (rawTask ?? "").Trim().ToLower() + "|" + + (rawIssue ?? "").Trim().ToLower(); - // Use a simple but stable hash (DJB2-like) uint hash = 5381; for (int i = 0; i < key.Length; i++) hash = ((hash << 5) + hash) + (uint)key[i]; - - return (int)(hash % 800000) + 100000; // Range: 100000-899999 + return (int)(hash % 800000) + 100000; } - /// - /// Returns the hash as an ID, resolving collisions by incrementing. - /// private int StableIdFromHash(int baseId, HashSet used) { int id = baseId; - while (used.Contains(id)) - id++; + while (used.Contains(id)) id++; return id; } + /// Returns raw untrimmed cell value for hashing. Never returns null - returns empty string. + private string RawGet(List fields, int index) + { + if (index < 0 || index >= fields.Count) return ""; + return fields[index] ?? ""; + } + public static string MapPriority(string raw) { if (string.IsNullOrEmpty(raw)) return "medium"; @@ -200,11 +169,6 @@ namespace Disco.Plugins.ServiceTracker.Services if (lower.Contains("medium") || lower.Contains("3-5 day")) return "medium"; if (lower.Contains("low") || lower.Contains("1 week") || lower.Contains("week+")) return "low"; if (lower.Contains("scheduled") || lower.Contains("planned")) return "scheduled"; - // Emoji fallback - if (raw.Contains("\U0001F534")) return "critical"; - if (raw.Contains("\U0001F7E0")) return "high"; - if (raw.Contains("\U0001F7E1")) return "medium"; - if (raw.Contains("\U0001F7E2")) return "low"; return "medium"; } @@ -218,10 +182,10 @@ namespace Disco.Plugins.ServiceTracker.Services private DateTime SafeGetDate(List fields, int index) { var val = SafeGet(fields, index); - if (val == null) return DateTime.Now; + if (val == null) return DateTime.MinValue; DateTime dt; if (DateTime.TryParse(val, out dt)) return dt; - return DateTime.Now; + return DateTime.MinValue; } private DateTime? SafeGetDateNullable(List fields, int index)