From d697e02f04331b1191407dec7b86c8b8ec897b06 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Tue, 19 May 2026 13:14:09 +0200 Subject: [PATCH] chore(backend,web): bump feedback comment limit 500 -> 4096 chars --- .../SubmitOffboardingFeedbackFunctionTests.cs | 10 +++++++--- .../Admin/SubmitOffboardingFeedbackFunction.cs | 12 +++++++++--- .../Functions/Feedback/FeedbackFunction.cs | 8 +++++--- .../app/settings/components/OffboardingSection.tsx | 2 +- .../components/FeedbackBubble.tsx | 8 ++++++-- 5 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/SubmitOffboardingFeedbackFunctionTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/SubmitOffboardingFeedbackFunctionTests.cs index d25c50e2..93e1af4e 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/SubmitOffboardingFeedbackFunctionTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/SubmitOffboardingFeedbackFunctionTests.cs @@ -59,15 +59,19 @@ public sealed class SubmitOffboardingFeedbackFunctionTests } [Fact] - public async Task Submit_TrimsAndTruncatesComment_To500Chars() + public async Task Submit_TrimsAndTruncatesComment_To4096Chars() { + // Departing tenants often have more context to share than the in-app 500-char + // limit allows; the offboarding endpoint accepts up to 4096 chars (still well + // under the Azure Tables 64 KB property cap). Anything beyond is silently + // truncated rather than rejected so the user's submit doesn't fail outright. var h = Harness.WithInProgressMarker(); - var longComment = new string('x', 600); + var longComment = new string('x', 5000); var result = await h.Sut.ProcessSubmitAsync(TenantId, Upn, DisplayName, " " + longComment + " "); Assert.Equal(SubmitOutcome.Ok, result.Outcome); - Assert.Equal(500, h.Repo.PersistedOffboardingFeedback!.Comment!.Length); + Assert.Equal(4096, h.Repo.PersistedOffboardingFeedback!.Comment!.Length); Assert.DoesNotContain(" ", h.Repo.PersistedOffboardingFeedback.Comment!.Substring(0, 1)); } diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/SubmitOffboardingFeedbackFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/SubmitOffboardingFeedbackFunction.cs index 54b664ab..24a6f91e 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/SubmitOffboardingFeedbackFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/SubmitOffboardingFeedbackFunction.cs @@ -32,7 +32,11 @@ namespace AutopilotMonitor.Functions.Functions.Admin; /// public class SubmitOffboardingFeedbackFunction { - private const int MaxCommentLength = 500; + // Departing tenants often have more context to share than the in-app 500-char limit + // allows. Azure Table Storage caps a single string property at 64 KB, so 4096 chars + // (max ~16 KB UTF-8) sits well within both the table and the 16 KB request-body + // guard above. + private const int MaxCommentLength = 4096; private readonly ILogger _logger; private readonly IOffboardingAuditRepository _offboardingRepo; @@ -72,10 +76,12 @@ public class SubmitOffboardingFeedbackFunction } // Reject oversized bodies before parsing — defense against accidental clipboard-paste - // of huge logs into the textarea. + // of huge logs into the textarea. 64 KB is the Azure Table Storage single-property + // cap, which gives 4096 chars (max ~16 KB UTF-8) plus JSON envelope plenty of room + // while still bouncing pathological multi-MB pastes before we read them. if (req.Headers.TryGetValues("Content-Length", out var clValues) && long.TryParse(clValues.FirstOrDefault(), out var contentLength) - && contentLength > 16_384) + && contentLength > 65_536) { return await BadRequest(req, "Request body too large"); } diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Feedback/FeedbackFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Feedback/FeedbackFunction.cs index 74f08adc..467a51bf 100644 --- a/src/Backend/AutopilotMonitor.Functions/Functions/Feedback/FeedbackFunction.cs +++ b/src/Backend/AutopilotMonitor.Functions/Functions/Feedback/FeedbackFunction.cs @@ -159,10 +159,12 @@ namespace AutopilotMonitor.Functions.Functions.Feedback return badRequest; } - // Trim comment + // Trim comment. Aligned with the offboarding-feedback endpoint at 4096 chars + // so users have room for a substantive comment without hitting a tight limit; + // Azure Tables caps a single property at 64 KB so this stays well within. var comment = body.Comment?.Trim(); - if (comment?.Length > 500) - comment = comment.Substring(0, 500); + if (comment?.Length > 4096) + comment = comment.Substring(0, 4096); // Upsert feedback record await _feedbackRepo.SaveInAppFeedbackAsync(new FeedbackEntry diff --git a/src/Web/autopilot-monitor-web/app/settings/components/OffboardingSection.tsx b/src/Web/autopilot-monitor-web/app/settings/components/OffboardingSection.tsx index 635a00fd..f278aa3e 100644 --- a/src/Web/autopilot-monitor-web/app/settings/components/OffboardingSection.tsx +++ b/src/Web/autopilot-monitor-web/app/settings/components/OffboardingSection.tsx @@ -39,7 +39,7 @@ interface OffboardingSectionProps { * (re-onboard → offboard) gets a fresh prompt each time. */ const FEEDBACK_SUBMITTED_KEY_PREFIX = "offboard-feedback-submitted:"; -const FEEDBACK_MAX_CHARS = 500; +const FEEDBACK_MAX_CHARS = 4096; function formatRemaining(totalSeconds: number): string { if (totalSeconds <= 0) return "0 min"; diff --git a/src/Web/autopilot-monitor-web/components/FeedbackBubble.tsx b/src/Web/autopilot-monitor-web/components/FeedbackBubble.tsx index c15363a5..b911f5ea 100644 --- a/src/Web/autopilot-monitor-web/components/FeedbackBubble.tsx +++ b/src/Web/autopilot-monitor-web/components/FeedbackBubble.tsx @@ -7,6 +7,10 @@ import { authenticatedFetch } from "@/lib/authenticatedFetch"; type Phase = "loading" | "bubble" | "form" | "thankyou" | "hidden"; +// Aligned with the offboarding-feedback textarea — gives users room for substantive +// comments without hitting an artificial 500-char limit. Backend cap matches. +const FEEDBACK_MAX_CHARS = 4096; + export default function FeedbackBubble() { const { isAuthenticated, user, getAccessToken } = useAuth(); const [phase, setPhase] = useState("loading"); @@ -163,13 +167,13 @@ export default function FeedbackBubble() {