From 643b25b11561fed4e33f55243b97fb1f0a744415 Mon Sep 17 00:00:00 2001 From: Andrew Anderson Date: Wed, 1 Apr 2026 09:04:43 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20screenshot=20upload=20fail?= =?UTF-8?q?ure=20and=20overlapping=20info=20tooltips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix #4118: Screenshot uploads caused form submission to fail silently due to: - Fiber default body limit (4 MB) too small for base64-encoded images - GitHub API upload timeout (10s) too short for large screenshots - Frontend API timeout (15s) insufficient for server-side GitHub uploads - Base64 padding validation failure when browsers omit trailing '=' Fix #4120: Info icon showed both a native browser tooltip ("Card information") and a custom portal tooltip simultaneously, causing overlapping text. Removed the native title attribute since the custom tooltip already provides the card description. Closes #4118 Closes #4120 Signed-off-by: Andrew Anderson --- pkg/api/handlers/feedback.go | 16 ++++++++++++++-- pkg/api/server.go | 3 +++ web/src/components/cards/CardWrapper.tsx | 1 - .../components/feedback/FeatureRequestModal.tsx | 6 ++++-- web/src/components/feedback/FeedbackModal.tsx | 6 ++++-- web/src/hooks/useFeatureRequests.ts | 4 ++-- web/src/lib/constants/network.ts | 5 +++++ 7 files changed, 32 insertions(+), 9 deletions(-) diff --git a/pkg/api/handlers/feedback.go b/pkg/api/handlers/feedback.go index 1b29f5597..b5270d3bc 100644 --- a/pkg/api/handlers/feedback.go +++ b/pkg/api/handlers/feedback.go @@ -30,6 +30,10 @@ import ( // githubAPITimeout is the timeout for HTTP requests to the GitHub API. const githubAPITimeout = 10 * time.Second +// screenshotUploadTimeout is a longer timeout for uploading base64 screenshots +// to GitHub via the Contents API, which can be slow for large images. +const screenshotUploadTimeout = 60 * time.Second + // prCacheTTL is how long cached PR data is considered fresh. const prCacheTTL = 5 * time.Minute @@ -1659,9 +1663,15 @@ func (h *FeedbackHandler) uploadScreenshotToGitHub(repoOwner, repoName, requestI ext = "webp" } - // The base64 content (GitHub Contents API expects raw base64, no wrapping) + // The base64 content (GitHub Contents API expects raw base64, no wrapping). + // Browsers may omit trailing '=' padding, so we normalize first. b64Content := parts[1] + // Add padding if missing — base64 requires length to be a multiple of 4 + if remainder := len(b64Content) % 4; remainder != 0 { + b64Content += strings.Repeat("=", 4-remainder) + } + // Validate that the base64 content is actually valid if _, err := base64.StdEncoding.DecodeString(b64Content); err != nil { return "", fmt.Errorf("invalid base64 content: %w", err) @@ -1688,7 +1698,9 @@ func (h *FeedbackHandler) uploadScreenshotToGitHub(repoOwner, repoName, requestI req.Header.Set("Accept", "application/vnd.github.v3+json") req.Header.Set("Content-Type", "application/json") - resp, err := h.httpClient.Do(req) + // Use a longer timeout for screenshot uploads (large base64 payloads) + uploadClient := &http.Client{Timeout: screenshotUploadTimeout} + resp, err := uploadClient.Do(req) if err != nil { return "", err } diff --git a/pkg/api/server.go b/pkg/api/server.go index afe3f9b0a..6eb337c2e 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -194,10 +194,13 @@ func NewServer(cfg Config) (*Server, error) { middleware.InitTokenRevocation(db) // Create Fiber app + // feedbackBodyLimit allows base64-encoded screenshot uploads (up to ~20 MB) + const feedbackBodyLimit = 20 * 1024 * 1024 app := fiber.New(fiber.Config{ ErrorHandler: customErrorHandler, ReadBufferSize: 16384, WriteBufferSize: 16384, + BodyLimit: feedbackBodyLimit, ReadTimeout: 30 * time.Second, WriteTimeout: 5 * time.Minute, // large static assets on slow networks IdleTimeout: 2 * time.Minute, diff --git a/web/src/components/cards/CardWrapper.tsx b/web/src/components/cards/CardWrapper.tsx index 9687a90ad..132240a08 100644 --- a/web/src/components/cards/CardWrapper.tsx +++ b/web/src/components/cards/CardWrapper.tsx @@ -303,7 +303,6 @@ function InfoTooltip({ text }: { text: string }) { onMouseLeave={() => setIsVisible(false)} className="p-0.5 rounded text-muted-foreground/50 hover:text-muted-foreground transition-colors" aria-label={t('cardWrapper.cardInfo')} - title={t('cardWrapper.cardInfo')} > diff --git a/web/src/components/feedback/FeatureRequestModal.tsx b/web/src/components/feedback/FeatureRequestModal.tsx index 61ec216d9..d34fb2578 100644 --- a/web/src/components/feedback/FeatureRequestModal.tsx +++ b/web/src/components/feedback/FeatureRequestModal.tsx @@ -16,6 +16,7 @@ import { import { useAuth } from '../../lib/auth' import { useRewards } from '../../hooks/useRewards' import { BACKEND_DEFAULT_URL, STORAGE_KEY_TOKEN, DEMO_TOKEN_VALUE, FETCH_DEFAULT_TIMEOUT_MS, COPY_FEEDBACK_TIMEOUT_MS } from '../../lib/constants' +import { FEEDBACK_UPLOAD_TIMEOUT_MS } from '../../lib/constants/network' import { emitLinkedInShare } from '../../lib/analytics' import { isDemoModeForced } from '../../lib/demoMode' import { useToast } from '../ui/Toast' @@ -338,13 +339,14 @@ export function FeatureRequestModal({ isOpen, onClose, initialTab, initialReques const screenshotDataURIs = screenshots.map(s => s.preview) try { + const hasScreenshots = screenshotDataURIs.length > 0 const result = await createRequest({ title: extractedTitle, description: extractedDesc, request_type: requestType, target_repo: targetRepo, - ...(screenshotDataURIs.length > 0 && { screenshots: screenshotDataURIs }), - }) + ...(hasScreenshots && { screenshots: screenshotDataURIs }), + }, hasScreenshots ? { timeout: FEEDBACK_UPLOAD_TIMEOUT_MS } : undefined) setSuccess({ issueUrl: result.github_issue_url }) // Keep the success state visible for 5s so users can read the confirmation and open the issue before switching to the Updates tab setTimeout(() => { diff --git a/web/src/components/feedback/FeedbackModal.tsx b/web/src/components/feedback/FeedbackModal.tsx index 8676f4854..efb8fbc9e 100644 --- a/web/src/components/feedback/FeedbackModal.tsx +++ b/web/src/components/feedback/FeedbackModal.tsx @@ -20,6 +20,7 @@ import { useToast } from '../ui/Toast' import { emitFeedbackSubmitted, emitLinkedInShare } from '../../lib/analytics' import { useBranding } from '../../hooks/useBranding' import { FETCH_DEFAULT_TIMEOUT_MS, COPY_FEEDBACK_TIMEOUT_MS } from '../../lib/constants' +import { FEEDBACK_UPLOAD_TIMEOUT_MS } from '../../lib/constants/network' import { useFeatureRequests } from '../../hooks/useFeatureRequests' import { useAuth } from '../../lib/auth' @@ -158,13 +159,14 @@ export function FeedbackModal({ isOpen, onClose, initialType = 'feature' }: Feed // Submit via backend API — creates GitHub issue directly using the // server-side token. No GitHub login required from the user. // Screenshots are uploaded server-side and embedded as images. + const hasScreenshots = screenshotDataURIs.length > 0 const result = await createRequest({ title: title.trim(), description: description.trim(), request_type: type, target_repo: 'console', - ...(screenshotDataURIs.length > 0 && { screenshots: screenshotDataURIs }), - }) + ...(hasScreenshots && { screenshots: screenshotDataURIs }), + }, hasScreenshots ? { timeout: FEEDBACK_UPLOAD_TIMEOUT_MS } : undefined) emitFeedbackSubmitted(type) diff --git a/web/src/hooks/useFeatureRequests.ts b/web/src/hooks/useFeatureRequests.ts index da29d55b7..be563d1c0 100644 --- a/web/src/hooks/useFeatureRequests.ts +++ b/web/src/hooks/useFeatureRequests.ts @@ -300,10 +300,10 @@ export function useFeatureRequests(currentUserId?: string) { setIsRefreshing(false) }, [loadRequests]) - const createRequest = useCallback(async (input: CreateFeatureRequestInput) => { + const createRequest = useCallback(async (input: CreateFeatureRequestInput, options?: { timeout?: number }) => { try { setIsSubmitting(true) - const { data } = await api.post('/api/feedback/requests', input) + const { data } = await api.post('/api/feedback/requests', input, options) setRequests(prev => [data, ...prev]) return data } finally { diff --git a/web/src/lib/constants/network.ts b/web/src/lib/constants/network.ts index 56989ed6d..5a76585db 100644 --- a/web/src/lib/constants/network.ts +++ b/web/src/lib/constants/network.ts @@ -88,6 +88,11 @@ export const FETCH_DEFAULT_TIMEOUT_MS = 10_000 /** Timeout for fetch() calls to external services (GitHub API, registries, etc.) */ export const FETCH_EXTERNAL_TIMEOUT_MS = 15_000 +/** Extended timeout for feedback submissions with screenshot uploads (90 seconds). + * Screenshots are uploaded server-side to GitHub via the Contents API, which can + * be slow for multiple large images. */ +export const FEEDBACK_UPLOAD_TIMEOUT_MS = 90_000 + // ============================================================================ // UI Feedback Timeouts // ============================================================================