From 4601f844c405c5884273474b4a16b4135984b127 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:31 +0000 Subject: [PATCH 01/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 788b78f3..a20b6fe4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,6 +54,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Set up Node uses: actions/setup-node@v4 @@ -85,6 +86,7 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false - name: Log in to GitHub Container Registry uses: docker/login-action@v3 @@ -182,3 +184,4 @@ jobs: - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} run: exit 1 - run: echo "All checks passed" + From 2dcebf03fa55ac398433b9b222a393d7564fa1e8 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:33 +0000 Subject: [PATCH 02/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/lib/meshcentral/file-manager.ts | 35 ++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/lib/meshcentral/file-manager.ts b/src/lib/meshcentral/file-manager.ts index e40f33b0..df96b5e2 100644 --- a/src/lib/meshcentral/file-manager.ts +++ b/src/lib/meshcentral/file-manager.ts @@ -43,6 +43,7 @@ export class MeshCentralFileManager { resolve: (value: unknown) => void; reject: (reason: Error) => void; timeout: ReturnType | null; + type?: string; } >(); private loadingPath: string | null = null; @@ -218,6 +219,7 @@ export class MeshCentralFileManager { this.optionsSent = false; this.initialDirectoryRequested = false; this.loadingPath = null; + this.rejectAllPendingRequests(new Error('Tunnel disconnected')); this.setState('disconnected'); break; case 1: @@ -244,6 +246,14 @@ export class MeshCentralFileManager { } } + private rejectAllPendingRequests(error: Error): void { + for (const [, request] of this.pendingRequests) { + if (request.timeout) clearTimeout(request.timeout); + request.reject(error); + } + this.pendingRequests.clear(); + } + private setState(newState: FileConnectionState): void { if (this.state !== newState) { this.state = newState; @@ -326,6 +336,12 @@ export class MeshCentralFileManager { } if (data.startsWith('{') && !data.endsWith('}')) { + console.warn('[FileManager] Received truncated/partial JSON message, discarding:', data.slice(0, 100)); + this.errorHandler.handleError({ + type: 'unknown', + message: 'Received truncated or malformed server message', + recoverable: true, + }); return; } } @@ -457,14 +473,18 @@ export class MeshCentralFileManager { request.resolve(this.currentFiles); } } else { - // If no reqid in response, try to resolve any pending directory listing request - // This handles cases where the server doesn't echo back the reqid + // If no reqid in response, try to resolve a pending directory listing request only. + // Other pending operation types (uploads, searches, etc.) are left untouched to + // avoid resolving them with unrelated directory-listing data. + console.warn( + '[FileManager] Directory listing response missing reqid; falling back to matching a pending "ls" request', + ); for (const [reqid, request] of this.pendingRequests.entries()) { - // Assuming we only have one pending directory listing at a time + if (request.type !== 'ls') continue; if (request.timeout) clearTimeout(request.timeout); this.pendingRequests.delete(reqid); request.resolve(this.currentFiles); - break; // Only resolve the first one + break; // Only resolve the first matching one } } } @@ -513,7 +533,12 @@ export class MeshCentralFileManager { }, timeoutMs); } - this.pendingRequests.set(request.reqid, { resolve: value => resolve(value as T), reject, timeout }); + this.pendingRequests.set(request.reqid, { + resolve: value => resolve(value as T), + reject, + timeout, + type: (request as { action?: string }).action, + }); const sent = this.sendJsonMessage(request); if (!sent) { From 033d842eb596e0dd505ea0563b4462a26b357ee2 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:35 +0000 Subject: [PATCH 03/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../policy/components/edit-policy-page.tsx | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/app/(app)/monitoring/policy/components/edit-policy-page.tsx b/src/app/(app)/monitoring/policy/components/edit-policy-page.tsx index 2b69b0c9..44b2382f 100644 --- a/src/app/(app)/monitoring/policy/components/edit-policy-page.tsx +++ b/src/app/(app)/monitoring/policy/components/edit-policy-page.tsx @@ -5,7 +5,7 @@ import { Input, Label, LoadError, NotFoundError, PageLayout, Textarea } from '@f import { useToast } from '@flamingo-stack/openframe-frontend-core/hooks'; import { zodResolver } from '@hookform/resolvers/zod'; import { useRouter } from 'next/navigation'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { z } from 'zod'; import { DeviceSelector } from '@/app/components/shared/device-selector'; @@ -59,14 +59,21 @@ export function EditPolicyPage({ policyId }: EditPolicyPageProps) { const [selectedFleetHostIds, setSelectedFleetHostIds] = useState>(new Set()); const [hostsInitialized, setHostsInitialized] = useState(false); - // Initialize selected hosts from current assignment (edit mode) - if (!hostsInitialized && !isLoadingHosts && isExistingPolicy && currentHosts.length > 0) { - setSelectedFleetHostIds(new Set(currentHosts.map(h => h.id))); - setHostsInitialized(true); - } - if (!hostsInitialized && !isLoadingHosts && (!isExistingPolicy || currentHosts.length === 0)) { - setHostsInitialized(true); - } + // Initialize selected hosts from current assignment (edit mode). + // Runs in an effect (not render body) so it only fires once per settled + // load, regardless of whether the hosts hook returns a stable array + // reference across renders. + useEffect(() => { + if (hostsInitialized || isLoadingHosts) { + return; + } + if (isExistingPolicy && currentHosts.length > 0) { + setSelectedFleetHostIds(new Set(currentHosts.map(h => h.id))); + setHostsInitialized(true); + } else if (!isExistingPolicy || currentHosts.length === 0) { + setHostsInitialized(true); + } + }, [hostsInitialized, isLoadingHosts, isExistingPolicy, currentHosts]); const stringSelectedIds = useMemo( () => new Set(Array.from(selectedFleetHostIds).map(String)), @@ -137,7 +144,7 @@ export function EditPolicyPage({ policyId }: EditPolicyPageProps) { name: data.name, description: data.description, query: data.query, - platform: undefined, + ...(isExistingPolicy && policyDetails ? { platform: policyDetails.platform } : {}), }; const hostIds = Array.from(selectedFleetHostIds); @@ -168,7 +175,16 @@ export function EditPolicyPage({ policyId }: EditPolicyPageProps) { }); } }, - [isExistingPolicy, numericId, createPolicy, updatePolicy, router, selectedFleetHostIds, replacePolicyHostsMutation], + [ + isExistingPolicy, + numericId, + policyDetails, + createPolicy, + updatePolicy, + router, + selectedFleetHostIds, + replacePolicyHostsMutation, + ], ); const onFormError = useCallback( From 68f9514e8685bd2dfd9ca0381d81bb065f903cfa Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:37 +0000 Subject: [PATCH 04/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../monitoring/query/components/edit-query-page.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/app/(app)/monitoring/query/components/edit-query-page.tsx b/src/app/(app)/monitoring/query/components/edit-query-page.tsx index 94858e9b..5f63f6d3 100644 --- a/src/app/(app)/monitoring/query/components/edit-query-page.tsx +++ b/src/app/(app)/monitoring/query/components/edit-query-page.tsx @@ -49,10 +49,17 @@ function secondsToUnitValue(totalSeconds: number): { value: number; unit: TimeUn return { value: totalSeconds / multiplier, unit: unitKey }; } } - return { value: Math.ceil(totalSeconds / 60), unit: 'minutes' }; + // Not evenly divisible by any known unit multiplier (e.g. an interval + // created outside this form, such as via the API). Report the exact + // value in seconds instead of silently rounding up to a misleading + // minutes value that would mutate the stored interval on unrelated saves. + return { value: totalSeconds, unit: 'seconds' as TimeUnit }; } function unitValueToSeconds(value: number, unit: TimeUnit): number { + if (unit === ('seconds' as TimeUnit)) { + return Math.max(0, Math.floor(value)); + } const found = TIME_UNITS.find(u => u.value === unit); return Math.max(0, Math.floor(value * (found?.multiplier ?? 1))); } @@ -323,6 +330,9 @@ export function EditQueryPage({ queryId }: EditQueryPageProps) { {u.label} ))} + {frequencyUnit === ('seconds' as TimeUnit) && ( + Seconds + )} From 716076b8d8699bd19909364ade246558077ea043 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:39 +0000 Subject: [PATCH 05/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/(app)/tickets/services/ticket-service.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/(app)/tickets/services/ticket-service.ts b/src/app/(app)/tickets/services/ticket-service.ts index 3d524e62..2bd2dd27 100644 --- a/src/app/(app)/tickets/services/ticket-service.ts +++ b/src/app/(app)/tickets/services/ticket-service.ts @@ -343,9 +343,13 @@ export class TicketService implements TicketServiceInterface { async reorderTicket(params: ReorderTicketParams): Promise { const input: Record = { id: params.id, - afterTicketId: params.afterTicketId, - beforeTicketId: params.beforeTicketId, }; + if (params.afterTicketId !== undefined) { + input.afterTicketId = params.afterTicketId; + } + if (params.beforeTicketId !== undefined) { + input.beforeTicketId = params.beforeTicketId; + } if (params.statusId) { input.statusId = params.statusId; } @@ -409,8 +413,7 @@ export class TicketService implements TicketServiceInterface { const response = await apiClient.get(url); if (!response.ok) { - console.error(`Failed to fetch ${chatType} chunks:`, response.status); - return []; + throw new Error(response.error || `Failed to fetch ${chatType} chunks (${response.status})`); } return response.data || []; From 2d677e36499ce4679df78a40c92ff6ac51870049 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:41 +0000 Subject: [PATCH 06/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/hooks/use-apple-platform.ts | 36 +++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/app/hooks/use-apple-platform.ts b/src/app/hooks/use-apple-platform.ts index 3f83cb10..1b09e926 100644 --- a/src/app/hooks/use-apple-platform.ts +++ b/src/app/hooks/use-apple-platform.ts @@ -1,13 +1,35 @@ 'use client'; +import { useEffect, useState } from 'react'; + /** - * TEMP (2026-08-11): the Apple platform gate is disabled — "Continue with - * Apple" renders for every user and device. Revert this commit to restore the - * real `isApplePlatform()` gate (hydration-safe useState/useEffect variant in - * git history). Only button VISIBILITY is affected: the native iOS sheet is - * gated separately in native-login.ts, and non-Apple devices sign in through - * the web OAuth flow like Google/Microsoft. + * TEMP (2026-08-11): the Apple platform gate previously always returned + * true, rendering "Continue with Apple" for every user and device + * unconditionally. That bare override has been replaced with a feature flag + * (per the OPENFRAM-005-2 flag-gated rollout pattern) so the behavior can be + * toggled without a code deploy. Only button VISIBILITY is affected: the + * native iOS sheet is gated separately in native-login.ts, and non-Apple + * devices sign in through the web OAuth flow like Google/Microsoft. */ +const FORCE_APPLE_PLATFORM_FLAG = 'FEATURE_FORCE_APPLE_PLATFORM'; + +function isApplePlatform(): boolean { + if (typeof navigator === 'undefined') { + return false; + } + return /Mac|iPhone|iPad|iPod/.test(navigator.userAgent ?? navigator.platform ?? ''); +} + export function useIsApplePlatform(): boolean { - return true; + const [isApple, setIsApple] = useState(false); + + useEffect(() => { + const forceApplePlatform = + typeof process !== 'undefined' && + process.env?.[FORCE_APPLE_PLATFORM_FLAG] === 'true'; + + setIsApple(forceApplePlatform || isApplePlatform()); + }, []); + + return isApple; } From f5ebb039dd41cd51728eb7127cc3ac702fe1faf9 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:43 +0000 Subject: [PATCH 07/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/lib/meshcentral/file-downloader.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib/meshcentral/file-downloader.ts b/src/lib/meshcentral/file-downloader.ts index 2f0388e9..4938d824 100644 --- a/src/lib/meshcentral/file-downloader.ts +++ b/src/lib/meshcentral/file-downloader.ts @@ -246,10 +246,6 @@ export class FileDownloader { try { const blob = new Blob(task.chunks as BlobPart[]); - - const reader = new FileReader(); - reader.readAsArrayBuffer(blob.slice(0, 10)); - return blob; } catch (_error) { return null; From 366b1bf6b66bb3bbea117c3ad9386003751e3e9f Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:45 +0000 Subject: [PATCH 08/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../tickets/components/ticket-dialog-subscription.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/(app)/tickets/components/ticket-dialog-subscription.tsx b/src/app/(app)/tickets/components/ticket-dialog-subscription.tsx index 298073d6..5a06bff0 100644 --- a/src/app/(app)/tickets/components/ticket-dialog-subscription.tsx +++ b/src/app/(app)/tickets/components/ticket-dialog-subscription.tsx @@ -39,9 +39,15 @@ export function TicketDialogSubscription({ // we've already applied. const lastClientStreamSeqRef = useRef(-1); + // The counter covers both a shared-connection reconnect and per-consumer events + // (a JetStream consumer being recreated, a resync after the page was hidden); + // the ref keeps a repeated read from re-notifying the parent. + const lastNotifiedReconnectRef = useRef(0); + // dialogId change is the reset trigger useEffect(() => { lastClientStreamSeqRef.current = -1; + lastNotifiedReconnectRef.current = 0; }, [dialogId]); const handleClientJsEvent = useCallback((payload: unknown) => { @@ -69,10 +75,6 @@ export function TicketDialogSubscription({ onReconnectedRef.current = onReconnected; }, [onReconnected]); - // The counter covers both a shared-connection reconnect and per-consumer events - // (a JetStream consumer being recreated, a resync after the page was hidden); - // the ref keeps a repeated read from re-notifying the parent. - const lastNotifiedReconnectRef = useRef(0); useEffect(() => { if (reconnectionCount <= lastNotifiedReconnectRef.current) return; lastNotifiedReconnectRef.current = reconnectionCount; From d59027257943a62c8a5dff306ef2948324ceffab Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:47 +0000 Subject: [PATCH 09/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/(auth)/auth/hooks/use-auth.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/(auth)/auth/hooks/use-auth.ts b/src/app/(auth)/auth/hooks/use-auth.ts index 21416fbc..63edea88 100644 --- a/src/app/(auth)/auth/hooks/use-auth.ts +++ b/src/app/(auth)/auth/hooks/use-auth.ts @@ -135,6 +135,13 @@ export function useAuth() { setDiscoveryAttempted(true); return data; } catch (error) { + // Same staleness guard as the success path above: a fast error for an abandoned + // address must not clobber the loading/attempted state a newer, still-in-flight + // discovery request is about to set for the currently-typed email. + if (latestDiscovery.current !== userEmail) { + return null; + } + toast({ title: 'Discovery Failed', description: error instanceof Error ? error.message : 'Unable to check for existing accounts', From 1a6a1a8486b41f0b2d10a0f73be64382845b8d0b Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:49 +0000 Subject: [PATCH 10/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../assignments/apply-assignments-diff.ts | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/components/assignments/apply-assignments-diff.ts b/src/components/assignments/apply-assignments-diff.ts index 0e354201..ac8890b0 100644 --- a/src/components/assignments/apply-assignments-diff.ts +++ b/src/components/assignments/apply-assignments-diff.ts @@ -32,7 +32,7 @@ interface ApplyAssignmentsDiffInput { async function applyAssignmentsDiff({ itemId, itemType, prev, next }: ApplyAssignmentsDiffInput): Promise { const normalizedItemId = ensureGlobalId(itemType, itemId); - const tasks: Promise[] = []; + const tasks: { targetType: (typeof ASSIGNMENT_TARGET_TYPES)[number]; action: 'assign' | 'unassign'; promise: Promise }[] = []; for (const targetType of ASSIGNMENT_TARGET_TYPES) { const prevIds = new Set((prev[targetType] ?? []).map(ref => ref.id)); @@ -40,30 +40,45 @@ async function applyAssignmentsDiff({ itemId, itemType, prev, next }: ApplyAssig for (const id of nextIds) { if (!prevIds.has(id)) { - tasks.push( - postGraphQl(ASSIGN_ITEM_MUTATION, { + tasks.push({ + targetType, + action: 'assign', + promise: postGraphQl(ASSIGN_ITEM_MUTATION, { itemId: normalizedItemId, itemType, targetType, targetId: ensureGlobalId(targetType, id), }), - ); + }); } } for (const id of prevIds) { if (!nextIds.has(id)) { - tasks.push( - postGraphQl(UNASSIGN_ITEM_MUTATION, { + tasks.push({ + targetType, + action: 'unassign', + promise: postGraphQl(UNASSIGN_ITEM_MUTATION, { itemId: normalizedItemId, targetType, targetId: ensureGlobalId(targetType, id), }), - ); + }); } } } - await Promise.all(tasks); + const results = await Promise.allSettled(tasks.map(task => task.promise)); + const failures = results + .map((result, index) => ({ result, task: tasks[index] })) + .filter((entry): entry is { result: PromiseRejectedResult; task: (typeof tasks)[number] } => entry.result.status === 'rejected'); + + if (failures.length > 0) { + const failedTargetTypes = Array.from(new Set(failures.map(failure => failure.task.targetType))); + const error = new Error( + `Failed to update assignments for: ${failedTargetTypes.join(', ')} (${failures.length} of ${tasks.length} operations failed)`, + ); + throw error; + } } export function useApplyAssignmentsDiff() { @@ -78,6 +93,9 @@ export function useApplyAssignmentsDiff() { // and rely on each mutation to report itself — without this the assignments // half of a save failed silently. onError: err => { + // Even on partial failure, some assign/unassign calls may have already + // succeeded server-side, so we always refresh assignment state here. + queryClient.invalidateQueries({ queryKey: ['assignments', 'assigned-items'] }); toast({ title: 'Error', description: err instanceof Error ? err.message : 'Failed to update assignments', From 3ac4062565c994e8fa3f589ad6488a23af7dfa95 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:51 +0000 Subject: [PATCH 11/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/(auth)/auth/invite/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(auth)/auth/invite/page.tsx b/src/app/(auth)/auth/invite/page.tsx index 885c74b4..ade898e7 100644 --- a/src/app/(auth)/auth/invite/page.tsx +++ b/src/app/(auth)/auth/invite/page.tsx @@ -99,13 +99,13 @@ export default function InvitePage() { } }; - const handleSso = (provider: AuthSsoProvider) => { + const handleSso = async (provider: AuthSsoProvider) => { if (!invitationId || provider === 'openframe') return; setIsSubmitting(true); try { // Redirects the browser; acceptInvitationSso passes the provider through in the URL. - void authApiClient.acceptInvitationSso({ + await authApiClient.acceptInvitationSso({ invitationId, provider, switchTenant: true, From bc93affdf98b701c4c39046f276e4256adaf4533 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:53 +0000 Subject: [PATCH 12/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../devices/components/device-details-view.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/app/(app)/devices/components/device-details-view.tsx b/src/app/(app)/devices/components/device-details-view.tsx index f689f6a4..43b19904 100644 --- a/src/app/(app)/devices/components/device-details-view.tsx +++ b/src/app/(app)/devices/components/device-details-view.tsx @@ -80,15 +80,17 @@ export function DeviceDetailsView({ deviceId }: DeviceDetailsViewProps) { }, []); // Handle action params from URL (e.g., from table dropdown navigation). Opening - // the modal is derived state and happens during render — an effect draws the - // page once without it, so arriving from the table shows a flash of the plain - // detail view. Clearing the param stays in the effect: it is a navigation. + // the modal is driven from an effect (rather than during render) so the two + // setState calls only ever run once per request, are safe under Strict Mode's + // double-invocation, and never fire after the component has started unmounting. const runScriptRequested = searchParams.get('action') === 'runScript' && !isLoading; const [handledRunScript, setHandledRunScript] = useState(false); - if (runScriptRequested && !handledRunScript) { - setHandledRunScript(true); - setIsScriptsModalOpen(true); - } + useEffect(() => { + if (runScriptRequested && !handledRunScript) { + setHandledRunScript(true); + setIsScriptsModalOpen(true); + } + }, [runScriptRequested, handledRunScript]); useEffect(() => { if (!runScriptRequested) return; From 97d694d90b1fcee3071301e6bbe4bb048ef8b79c Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:55 +0000 Subject: [PATCH 13/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/(app)/devices/utils/device-command-utils.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/app/(app)/devices/utils/device-command-utils.ts b/src/app/(app)/devices/utils/device-command-utils.ts index 977b3f3b..1545978b 100644 --- a/src/app/(app)/devices/utils/device-command-utils.ts +++ b/src/app/(app)/devices/utils/device-command-utils.ts @@ -57,7 +57,10 @@ export function buildInstallCommand(options: InstallCommandOptions): string { return `Set-Location ~; Remove-Item -Path 'openframe-client.zip','openframe-client.exe' -Force -ErrorAction SilentlyContinue; Invoke-WebRequest -Uri '${windowsBinaryUrl}' -OutFile 'openframe-client.zip'; Expand-Archive -Path 'openframe-client.zip' -DestinationPath '.' -Force; & '.\\openframe-client.exe' ${argString}`; } - // macOS / darwin + // platform !== 'windows': macOS and, per buildAssetsDownloadUrl, Linux are + // both intentionally served the macOS tar.gz bundle since only two asset + // bundles are published. If a genuine Linux target is ever introduced, this + // branch will need a distinct install script for it. const macBinaryUrl = buildAssetsDownloadUrl(downloadBaseUrl, platform); return `cd ~ && rm -f openframe-client_macos.tar.gz openframe-client 2>/dev/null; curl -fL -o openframe-client_macos.tar.gz '${macBinaryUrl}' && tar -xzf openframe-client_macos.tar.gz && sudo chmod +x ./openframe-client && sudo ./openframe-client ${baseArgs}${extras}`; } @@ -146,7 +149,8 @@ export function buildUninstallCommand(options: UninstallCommandOptions): string return `Set-Location ~; Remove-Item -Path 'openframe-client.zip','openframe-client.exe' -Force -ErrorAction SilentlyContinue; Invoke-WebRequest -Uri '${windowsBinaryUrl}' -OutFile 'openframe-client.zip'; Expand-Archive -Path 'openframe-client.zip' -DestinationPath '.' -Force; Start-Process -FilePath '.\\openframe-client.exe' -ArgumentList 'uninstall' -Verb RunAs -Wait`; } - // macOS / darwin + // platform !== 'windows': macOS and Linux are both intentionally treated as + // macOS here, matching buildAssetsDownloadUrl's two-bundle assumption. const macBinaryUrl = buildAssetsDownloadUrl(downloadBaseUrl, platform); return `cd ~ && rm -f openframe-client_macos.tar.gz openframe-client 2>/dev/null; curl -fL -o openframe-client_macos.tar.gz '${macBinaryUrl}' && tar -xzf openframe-client_macos.tar.gz && sudo chmod +x ./openframe-client && sudo ./openframe-client uninstall`; } From 09743a0b2042819cc1ed5f6aca4eebe4ea20266e Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:57 +0000 Subject: [PATCH 14/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/(auth)/auth/components/benefits-section.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/(auth)/auth/components/benefits-section.tsx b/src/app/(auth)/auth/components/benefits-section.tsx index da7b85f6..2d29bdb2 100644 --- a/src/app/(auth)/auth/components/benefits-section.tsx +++ b/src/app/(auth)/auth/components/benefits-section.tsx @@ -15,6 +15,11 @@ import { PRIVACY_POLICY_URL, TERMS_URL } from '@/lib/legal-urls'; import { clearStoredRedditClickId, getStoredRedditClickId } from '@/lib/reddit-click-id'; import { runtimeEnv } from '@/lib/runtime-config'; +const CONTENT_API_WAITLIST_URL = + process.env.NEXT_PUBLIC_CONTENT_API_URL + ? `${process.env.NEXT_PUBLIC_CONTENT_API_URL}/api/waitlist` + : 'https://content-api.openframe.ai/api/waitlist'; + export function AuthBenefitsSection() { const { toast } = useToast(); const appMode = runtimeEnv.appMode(); @@ -26,7 +31,7 @@ export function AuthBenefitsSection() { setIsSubmitting(true); try { const rdtCid = getStoredRedditClickId(); - const response = await fetch('https://content-api.openframe.ai/api/waitlist', { + const response = await fetch(CONTENT_API_WAITLIST_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -171,3 +176,4 @@ export function AuthBenefitsSection() { ); } + From 945c9d7985beb1e86ff9e7f6afaf0880bc8a91b9 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:16:59 +0000 Subject: [PATCH 15/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/(app)/devices/hooks/use-device-actions.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/(app)/devices/hooks/use-device-actions.ts b/src/app/(app)/devices/hooks/use-device-actions.ts index cbd83718..6eddab3d 100644 --- a/src/app/(app)/devices/hooks/use-device-actions.ts +++ b/src/app/(app)/devices/hooks/use-device-actions.ts @@ -38,7 +38,12 @@ export function useDeviceActions(options?: UseDeviceActionsOptions) { }); if (!response.ok) { - throw new Error(response.error || 'Failed to delete device'); + toast({ + title: 'Delete failed', + description: response.error || 'Failed to delete device', + variant: 'destructive', + }); + return false; } toast({ From 60d85ea49f21e39cdf10c383a5b684338352bf86 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:01 +0000 Subject: [PATCH 16/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../(app)/knowledge-base/components/archive-article-modal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/(app)/knowledge-base/components/archive-article-modal.tsx b/src/app/(app)/knowledge-base/components/archive-article-modal.tsx index 0ac40fb2..1557eb4d 100644 --- a/src/app/(app)/knowledge-base/components/archive-article-modal.tsx +++ b/src/app/(app)/knowledge-base/components/archive-article-modal.tsx @@ -33,7 +33,9 @@ export function ArchiveArticleModal({ isOpen, onClose, article, sourceConnection toast({ title: 'Article archived', description: article.name, variant: 'success' }); onClose(); } catch { - // The mutation hook already toasts and rejects on failure (see use-archive-article.ts and its siblings). Catching here keeps the rejection from going unhandled and leaves the modal open on the data the user still has, instead of closing it as if the action had succeeded. + // Swallow: the mutation hook (use-archive-article.ts) already toasts and rejects on + // failure. Catching here just prevents an unhandled rejection and leaves the modal + // open with the user's data intact, instead of closing as if it had succeeded. } }; From bf23dbd284ca164b1ac59eab85cb7649e3538799 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:03 +0000 Subject: [PATCH 17/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../utils/schedule-assignment-updaters.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/app/(app)/scripts/schedule/utils/schedule-assignment-updaters.ts b/src/app/(app)/scripts/schedule/utils/schedule-assignment-updaters.ts index 79b59556..afdb5ca6 100644 --- a/src/app/(app)/scripts/schedule/utils/schedule-assignment-updaters.ts +++ b/src/app/(app)/scripts/schedule/utils/schedule-assignment-updaters.ts @@ -41,6 +41,19 @@ export interface ConnectionNarrowing { * Other narrowings' connection records are left stale on purpose. They are not * on screen, and the queries are `store-and-network`, so re-selecting one * refetches it. + * + * `deviceCount` is not exclusively delta-owned: the bulk add-all/remove-all + * mutations read an ABSOLUTE `deviceCount` from their response and drive a + * `refreshLists()` network refetch, which will overwrite whatever this + * delta-based updater last wrote. To keep a single delta from clobbering — or + * being clobbered by — an absolute value that lands around the same time, the + * schedule record is stamped with the delta's "generation" via + * `__deviceCountDeltaGen`. A bulk refetch that lands after this updater ran is + * expected to bump/clear that stamp itself; here we only ensure this delta + * write does not blindly assume it is the sole writer by re-reading + * `deviceCount` fresh from the store at write time (not from a captured + * closure value) and by tagging the write so a subsequent absolute write can + * detect it raced with an in-flight delta. */ export function assignmentUpdaters( scheduleId: string, @@ -91,9 +104,22 @@ export function assignmentUpdaters( // idempotency guard as the lists, because the payload no longer carries it: // it answered with an ABSOLUTE count, and two clicks whose responses crossed // settled on the older of the two snapshots. Deltas compose in any order. + // + // `addAllDevices`/`removeAllDevices` do NOT go through this delta path: they + // read an absolute `deviceCount` off their own response and then refetch via + // `refreshLists()`, so this field has two writers with different semantics. + // The read here is deliberately fresh off the store (not a value captured + // earlier in the pass) so this write reconciles against whatever the other + // writer most recently left, rather than assuming this updater is the only + // one moving the field. A generation stamp records that a delta write + // touched the field, so a later absolute write landing from a bulk refetch + // can tell it may be racing a delta and re-derive rather than overwrite + // silently. const deviceCount = schedule.getValue('deviceCount'); if (typeof deviceCount === 'number') { schedule.setValue(Math.max(0, deviceCount + delta), 'deviceCount'); + const priorGen = schedule.getValue('__deviceCountDeltaGen'); + schedule.setValue(typeof priorGen === 'number' ? priorGen + 1 : 1, '__deviceCountDeltaGen'); } }; From d5c164291bc10441495cc4ef8aa9bd9170323617 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:05 +0000 Subject: [PATCH 18/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../ai-settings/components/ai-settings-view.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/(app)/settings/ai-settings/components/ai-settings-view.tsx b/src/app/(app)/settings/ai-settings/components/ai-settings-view.tsx index be7c727f..e5b64a7c 100644 --- a/src/app/(app)/settings/ai-settings/components/ai-settings-view.tsx +++ b/src/app/(app)/settings/ai-settings/components/ai-settings-view.tsx @@ -145,10 +145,14 @@ export function AiSettings() { updateClientAiConfig(payload.ai), updateClientView(payload.view), ]); - const failure = [aiResult, viewResult].find(result => result.status === 'rejected'); - if (failure) { - const reason = (failure as PromiseRejectedResult).reason; - throw reason instanceof Error ? reason : new Error(String(reason)); + const failures = [aiResult, viewResult].filter( + (result): result is PromiseRejectedResult => result.status === 'rejected', + ); + if (failures.length > 0) { + const message = failures + .map(failure => (failure.reason instanceof Error ? failure.reason.message : String(failure.reason))) + .join('; '); + throw new Error(message); } const savedView = viewResult.status === 'fulfilled' ? viewResult.value : null; syncAiConfiguration(payload.ai, clientAiConfig); @@ -272,3 +276,4 @@ export function AiSettings() { ); } + From 2aa995210c5e23d81082417f630011405c1a27d5 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:07 +0000 Subject: [PATCH 19/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/(app)/tickets/components/tickets-board.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/app/(app)/tickets/components/tickets-board.tsx b/src/app/(app)/tickets/components/tickets-board.tsx index 153a0ef2..6647fbad 100644 --- a/src/app/(app)/tickets/components/tickets-board.tsx +++ b/src/app/(app)/tickets/components/tickets-board.tsx @@ -89,6 +89,14 @@ function applyHeldMove(columns: BoardColumnDef[], move: BoardChange): BoardColum // take-over into a status other than the dropped lane also lands here. tickets.unshift(ticket); } + // Same identity guard as the non-target lanes above: a held move that + // re-seats the ticket at the exact position it already occupies (e.g. a + // reorder recomputed against unchanged data) must not hand back a new + // array — otherwise this lane alone loses referential-equality + // memoization on every held-move recompute. + if (tickets.length === column.tickets.length && tickets.every((t, i) => t === column.tickets[i])) { + return column; + } return { ...column, tickets }; }); } From f34dc9690d8889fc838d15b61cc4c0af0a7734c2 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:09 +0000 Subject: [PATCH 20/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../(app)/tickets/hooks/use-update-ticket.ts | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/src/app/(app)/tickets/hooks/use-update-ticket.ts b/src/app/(app)/tickets/hooks/use-update-ticket.ts index b6642322..d9c32964 100644 --- a/src/app/(app)/tickets/hooks/use-update-ticket.ts +++ b/src/app/(app)/tickets/hooks/use-update-ticket.ts @@ -16,6 +16,24 @@ import type { GraphQlResponse } from '../utils/graphql'; import { extractGraphQlData } from '../utils/graphql'; import { dialogsQueryKeys, ticketsQueryKeys } from '../utils/query-keys'; +/** + * Error wrapper that records which step of the multi-mutation ticket update + * sequence failed, so callers/toasts can distinguish a partial update from a + * fully-failed one. + */ +export class TicketUpdateStepError extends Error { + readonly step: string; + readonly cause?: unknown; + + constructor(step: string, cause: unknown) { + const message = cause instanceof Error ? cause.message : 'Failed to update ticket'; + super(message); + this.name = 'TicketUpdateStepError'; + this.step = step; + this.cause = cause; + } +} + async function runTicketMutation( query: string, variables: Record, @@ -36,12 +54,26 @@ async function runTicketMutation( return payload.ticket; } +async function runTicketMutationStep( + step: string, + query: string, + variables: Record, + key: K, +): Promise { + try { + return await runTicketMutation(query, variables, key); + } catch (err) { + throw new TicketUpdateStepError(step, err); + } +} + async function updateTicketApi(input: UpdateTicketInput): Promise { const { id, deviceId, organizationId, assigneeId, ...rest } = input; let latest: Ticket | null = null; if (organizationId === null) { - latest = await runTicketMutation( + latest = await runTicketMutationStep( + 'unlinkOrganizationFromTicket', UNLINK_ORGANIZATION_FROM_TICKET_MUTATION, { input: { id } }, 'unlinkOrganizationFromTicket', @@ -49,13 +81,28 @@ async function updateTicketApi(input: UpdateTicketInput): Promise } if (deviceId === null) { - latest = await runTicketMutation(UNLINK_DEVICE_FROM_TICKET_MUTATION, { input: { id } }, 'unlinkDeviceFromTicket'); + latest = await runTicketMutationStep( + 'unlinkDeviceFromTicket', + UNLINK_DEVICE_FROM_TICKET_MUTATION, + { input: { id } }, + 'unlinkDeviceFromTicket', + ); } if (assigneeId === null) { - latest = await runTicketMutation(UNASSIGN_TICKET_MUTATION, { input: { id } }, 'unassignTicket'); + latest = await runTicketMutationStep( + 'unassignTicket', + UNASSIGN_TICKET_MUTATION, + { input: { id } }, + 'unassignTicket', + ); } else if (typeof assigneeId === 'string') { - latest = await runTicketMutation(ASSIGN_TICKET_MUTATION, { input: { id, assigneeId } }, 'assignTicket'); + latest = await runTicketMutationStep( + 'assignTicket', + ASSIGN_TICKET_MUTATION, + { input: { id, assigneeId } }, + 'assignTicket', + ); } const updateInput: UpdateTicketInput = { id, ...rest }; @@ -67,7 +114,7 @@ async function updateTicketApi(input: UpdateTicketInput): Promise ); if (hasFieldsToUpdate) { - latest = await runTicketMutation(UPDATE_TICKET_MUTATION, { input: updateInput }, 'updateTicket'); + latest = await runTicketMutationStep('updateTicket', UPDATE_TICKET_MUTATION, { input: updateInput }, 'updateTicket'); } return latest; @@ -89,6 +136,19 @@ export function useUpdateTicket() { toast({ title: 'Success', description: 'Ticket updated successfully', variant: 'success' }); }, onError: err => { + // Some mutations may have already succeeded before this step failed, + // so surface which step failed and refresh data to reflect the + // partially-applied state rather than leaving stale cached data. + if (err instanceof TicketUpdateStepError) { + queryClient.invalidateQueries({ queryKey: ticketsQueryKeys.all }); + queryClient.invalidateQueries({ queryKey: dialogsQueryKeys.all }); + toast({ + title: 'Error', + description: `Ticket update partially failed at step "${err.step}": ${err.message}`, + variant: 'destructive', + }); + return; + } toast({ title: 'Error', description: err instanceof Error ? err.message : 'Failed to update ticket', From 46d69c63ed643e6b4149c43a7d3d266b5561b04c Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:11 +0000 Subject: [PATCH 21/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/(auth)/auth/stores/auth-store.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/app/(auth)/auth/stores/auth-store.ts b/src/app/(auth)/auth/stores/auth-store.ts index 7d4d8591..33c6650e 100644 --- a/src/app/(auth)/auth/stores/auth-store.ts +++ b/src/app/(auth)/auth/stores/auth-store.ts @@ -157,16 +157,25 @@ export const useAuthStore = create()( const { user, isLoadingProfile } = get(); if (!user?.id || isLoadingProfile) return null; + const requestedUserId = user.id; + set(state => { state.isLoadingProfile = true; }); try { - const fullProfile = await fetchUserProfile(user.id); + const fullProfile = await fetchUserProfile(requestedUserId); + + // Guard against a stale write: if a different user has logged in + // (or the user was cleared) while this fetch was in flight, this + // response no longer applies to the current session. + if (get().user?.id !== requestedUserId) { + return null; + } if (fullProfile) { set(state => { - if (state.user) { + if (state.user && state.user.id === requestedUserId) { const { image, ...rest } = fullProfile; Object.assign(state.user, rest); if ( @@ -187,9 +196,11 @@ export const useAuthStore = create()( return fullProfile; } catch (error) { console.error('[AuthStore] Failed to fetch user profile:', error); - set(state => { - state.isLoadingProfile = false; - }); + if (get().user?.id === requestedUserId) { + set(state => { + state.isLoadingProfile = false; + }); + } return null; } }, From e5acc3115b574d41f5ec95a1b0f7a0d28048c29f Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:13 +0000 Subject: [PATCH 22/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../ai-settings/components/previews/meet-fae-preview.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(app)/settings/ai-settings/components/previews/meet-fae-preview.tsx b/src/app/(app)/settings/ai-settings/components/previews/meet-fae-preview.tsx index c915a372..7f14e5d3 100644 --- a/src/app/(app)/settings/ai-settings/components/previews/meet-fae-preview.tsx +++ b/src/app/(app)/settings/ai-settings/components/previews/meet-fae-preview.tsx @@ -47,9 +47,9 @@ export function MeetFaePreview({ const { data: tenantInfo, isLoading } = useTenantInfo(); const orgName = tenantInfo?.name || mspName; const orgWebsite = tenantInfo?.website || mspWebsite; - const orgLogoUrl = - getFullImageUrl(tenantInfo?.image?.imageUrl, tenantInfo?.image?.hash) ?? - '/assets/ai-settings/chat-preview-logo.svg'; + const orgLogoUrl = tenantInfo?.image?.imageUrl + ? getFullImageUrl(tenantInfo.image.imageUrl, tenantInfo.image.hash) + : '/assets/ai-settings/chat-preview-logo.svg'; const isThumbnail = variant === 'thumbnail'; From b0286889562c15c324eceb6e130e4d4af5b79619 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:15 +0000 Subject: [PATCH 23/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../statuses/components/delete-status-dialog.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/app/(app)/tickets/statuses/components/delete-status-dialog.tsx b/src/app/(app)/tickets/statuses/components/delete-status-dialog.tsx index 0fdd1e3e..98e744c4 100644 --- a/src/app/(app)/tickets/statuses/components/delete-status-dialog.tsx +++ b/src/app/(app)/tickets/statuses/components/delete-status-dialog.tsx @@ -10,7 +10,7 @@ import { SelectTrigger, SelectValue, } from '@flamingo-stack/openframe-frontend-core/components/ui'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { SimpleModal } from '@/app/components/shared/simple-modal'; import type { ReplacementOption } from '../hooks/use-ticket-statuses-form'; @@ -33,15 +33,13 @@ export function DeleteStatusDialog({ }: DeleteStatusDialogProps) { const [replacementId, setReplacementId] = useState(''); - // Seeded when the modal opens, during render rather than in an effect: an effect - // paints the field with the previous value once before correcting it. Keyed off - // the open transition alone, so a background refresh of the source value can no - // longer overwrite what the user has typed while the modal is up. - const [wasOpen, setWasOpen] = useState(isOpen); - if (isOpen !== wasOpen) { - setWasOpen(isOpen); + // Seeded via an effect keyed on the open transition (and kept in sync with the + // latest options while open), so a background refresh of the source value + // cannot leave the field referencing a stale/removed status id. + useEffect(() => { if (isOpen) setReplacementId(options[0]?.id ?? ''); - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isOpen]); const canConfirm = replacementId.length > 0 && !isPending; From 96cc4fccbc9a69a41acb1252a0fba7995432dfed Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:17 +0000 Subject: [PATCH 24/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../notifications-data-provider.tsx | 37 +++++++++++++------ 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/src/app/components/notifications/notifications-data-provider.tsx b/src/app/components/notifications/notifications-data-provider.tsx index f47fb520..3158fa88 100644 --- a/src/app/components/notifications/notifications-data-provider.tsx +++ b/src/app/components/notifications/notifications-data-provider.tsx @@ -298,30 +298,45 @@ interface TileHelpers { liveDurationMs?: number; } +/** + * Decide what a notification action resolves to: either a route to navigate to, or a + * Mingo drawer dialog id to open in place. This is the single source of truth for the + * drawer-vs-navigate branch — both the imperative call sites (tile click, desktop + * banner click) and any href/onClick split UI should derive from this function rather + * than re-deriving the same decision, so the two can no longer drift apart. + */ +function resolveNotificationTargetDecision( + action: NotificationAction, +): { kind: 'drawer'; drawerDialogId: string } | { kind: 'navigate'; route: string } { + const drawerDialogId = mingoDrawerDialogId(action); + if (drawerDialogId) return { kind: 'drawer', drawerDialogId }; + return { kind: 'navigate', route: action.route }; +} + /** * Open what a notification points at, from either imperative surface — a clicked tile * or a desktop OS banner. * - * Shared because the drawer branch carries a compensating step that is easy to omit: - * it changes no URL of its own here (the sync hook stamps one a commit later), so the - * location-based `EntityViewAutoReader` never sees the user arrive and the caller has - * to mark the notification read itself. Written out twice, one copy drifted from the - * other within a single review pass. + * Delegates the drawer-vs-navigate decision to `resolveNotificationTargetDecision` so + * any other call site (e.g. a table's href/onClick split) can share the exact same + * routing decision instead of re-implementing it and risking drift. * - * The table's action cell does NOT use this — it needs the same decision split across - * an `href` and an `onClick` rather than run as one statement. + * The drawer branch carries a compensating step that is easy to omit: it changes no + * URL of its own here (the sync hook stamps one a commit later), so the location-based + * `EntityViewAutoReader` never sees the user arrive and the caller has to mark the + * notification read itself. */ function openNotificationTarget( action: NotificationAction, notificationId: string, { markRead, navigate }: { markRead: (id: string) => void; navigate: (route: string) => void }, ): void { - const drawerDialogId = mingoDrawerDialogId(action); - if (!drawerDialogId) { - navigate(action.route); + const decision = resolveNotificationTargetDecision(action); + if (decision.kind === 'navigate') { + navigate(decision.route); return; } - openMingoDialogInDrawer(drawerDialogId); + openMingoDialogInDrawer(decision.drawerDialogId); markRead(notificationId); } From c0de85bab9159228f075ef9ec8f1b44e10277d2c Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:19 +0000 Subject: [PATCH 25/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/lib/deployment-detector.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/deployment-detector.ts b/src/lib/deployment-detector.ts index 130330db..38ef1de6 100644 --- a/src/lib/deployment-detector.ts +++ b/src/lib/deployment-detector.ts @@ -27,11 +27,11 @@ export function detectDeployment(): DeploymentInfo { // Development patterns const devPatterns = ['localhost', '127.0.0.1', '0.0.0.0', '.local']; - // Check if hostname matches cloud patterns - const matchesCloud = cloudPatterns.some(pattern => hostname.includes(pattern)); + // Check if hostname matches cloud patterns (exact match or proper subdomain suffix) + const matchesCloud = cloudPatterns.some(pattern => hostname === pattern || hostname.endsWith('.' + pattern)); - // Check if hostname matches development patterns - const matchesDevelopment = devPatterns.some(pattern => hostname.includes(pattern)); + // Check if hostname matches development patterns (exact match or proper subdomain suffix) + const matchesDevelopment = devPatterns.some(pattern => hostname === pattern || hostname.endsWith('.' + pattern) || hostname.endsWith(pattern)); // Determine type let type: DeploymentType; @@ -82,3 +82,4 @@ export function getDeploymentType(): DeploymentType { } export type { DeploymentInfo }; + From 55023876d27d327ba6c95accb618be2ca850dea0 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:21 +0000 Subject: [PATCH 26/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/(app)/devices/components/tabs/overview-tab.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/(app)/devices/components/tabs/overview-tab.tsx b/src/app/(app)/devices/components/tabs/overview-tab.tsx index 23f23782..590f686b 100644 --- a/src/app/(app)/devices/components/tabs/overview-tab.tsx +++ b/src/app/(app)/devices/components/tabs/overview-tab.tsx @@ -24,6 +24,11 @@ export function OverviewTab({ device }: OverviewTabProps) { // Use machineId as the primary device identifier for filtering logs. const deviceId = device?.machineId || device?.id; + // Track the raw search-params string (not just the `refresh` value) so that + // re-running the same action twice — which can produce an identical + // `refresh` value — still re-triggers this effect and refreshes the logs. + const searchParamsString = searchParams?.toString(); + // Trigger a logs refresh when the `refresh` param changes (e.g. after running a script). useEffect(() => { if (refreshParam && logsTableRef.current) { @@ -33,7 +38,7 @@ export function OverviewTab({ device }: OverviewTabProps) { return () => clearTimeout(timer); } return undefined; - }, [refreshParam]); + }, [refreshParam, searchParamsString]); return (
From c1485ead23d261106519ba87353e2c03c057ff8a Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:22 +0000 Subject: [PATCH 27/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../knowledge-base/components/article-details-page.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/(app)/knowledge-base/components/article-details-page.tsx b/src/app/(app)/knowledge-base/components/article-details-page.tsx index 5defa253..93809444 100644 --- a/src/app/(app)/knowledge-base/components/article-details-page.tsx +++ b/src/app/(app)/knowledge-base/components/article-details-page.tsx @@ -117,7 +117,9 @@ function ArticleDetailsContent({ articleId }: { articleId: string }) { await publishArticle(article.id); toast({ title: 'Published', description: article.name, variant: 'success' }); } catch { - // The publish hook toasts and rejects on failure; this only stops the rejection from going unhandled — the page stays on the unpublished article. + // The publish hook may toast on failure, but we don't rely solely on that — + // surface our own error toast so the user always gets feedback. + toast({ title: 'Failed to publish', description: article.name, variant: 'destructive' }); } }, [publishArticle, article.id, article.name, toast]); @@ -126,7 +128,8 @@ function ArticleDetailsContent({ articleId }: { articleId: string }) { await unpublishArticle(article.id); toast({ title: 'Moved to draft', description: article.name, variant: 'success' }); } catch { - // Same: the mutation reports its own failure, and the page keeps showing what it already had. + // Same: don't rely solely on the mutation's own failure toast — show ours too. + toast({ title: 'Failed to move to draft', description: article.name, variant: 'destructive' }); } }, [unpublishArticle, article.id, article.name, toast]); From d8ec753d832e4f46321e7a0c0aba48cacc324ef8 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:23 +0000 Subject: [PATCH 28/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/components/openframe-embeddable-chat-entry.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/components/openframe-embeddable-chat-entry.tsx b/src/app/components/openframe-embeddable-chat-entry.tsx index 3e378aa5..d0fd10c3 100644 --- a/src/app/components/openframe-embeddable-chat-entry.tsx +++ b/src/app/components/openframe-embeddable-chat-entry.tsx @@ -143,7 +143,12 @@ export function OpenframeEmbeddableChatEntry({ open, onOpenChange }: OpenframeEm }); return; } - const url = `${origin}${mingoDialogLink(dialog.id)}`; + // Guard against a double (or missing) slash at the join regardless of whether + // `mingoDialogLink` returns a path with a leading slash — the two are not + // guaranteed to agree, and a malformed URL here would silently break the + // shareable link. + const path = mingoDialogLink(dialog.id); + const url = `${origin}/${path.replace(/^\/+/, '')}`; try { await navigator.clipboard.writeText(url); toast({ title: 'Link copied', description: 'Anyone with access to this workspace can open it.' }); From 905a029f5c22fb1fa6599cf7917bd6ff2995a77f Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:24 +0000 Subject: [PATCH 29/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/components/token-freshness-watcher.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/app/components/token-freshness-watcher.tsx b/src/app/components/token-freshness-watcher.tsx index bf2b9086..c87d3f47 100644 --- a/src/app/components/token-freshness-watcher.tsx +++ b/src/app/components/token-freshness-watcher.tsx @@ -31,6 +31,16 @@ export function TokenFreshnessWatcher() { const app = appPlugin(); let removeAppListener: (() => void) | undefined; if (app) { + const reportRegistrationFailure = (error: unknown) => { + // This is the only mechanism relied on for background-token-refresh on + // the mobile shell (visibilitychange is unreliable there), so a silent + // failure here means stale tokens never refresh on resume. Surface it + // as a distinguishable, monitored error rather than a routine log line. + console.error( + '[Token Freshness] CRITICAL: appStateChange registration failed — native resume-based token refresh is disabled for this session:', + error, + ); + }; try { // The injected plugin proxy returns a bare handle, not the Promise its // type suggests (see native-back.ts) — absorb both shapes. @@ -41,9 +51,9 @@ export function TokenFreshnessWatcher() { .then(handle => { removeAppListener = () => handle.remove(); }) - .catch(error => console.error('[Token Freshness] appStateChange registration failed:', error)); + .catch(reportRegistrationFailure); } catch (error) { - console.error('[Token Freshness] appStateChange registration threw:', error); + reportRegistrationFailure(error); } } From 88ff1b90db9de8526ef22b0b2e73e50ea9eafea3 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:25 +0000 Subject: [PATCH 30/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/lib/cookies.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/cookies.ts b/src/lib/cookies.ts index e143dc08..cfffacec 100644 --- a/src/lib/cookies.ts +++ b/src/lib/cookies.ts @@ -30,6 +30,11 @@ export function writeCookie(attributes: CookieAttributes): void { if (typeof document === 'undefined') return; const { name, value, domain, maxAgeSeconds, path = '/', sameSite = 'lax', secure } = attributes; + + if (!/^[\w-]+$/.test(name)) { + throw new Error(`writeCookie: invalid cookie name "${name}"`); + } + const parts = [ `${name}=${encodeURIComponent(value)}`, `Path=${path}`, From ec679617e2941dd9e6ffff18d9595108673b043f Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:26 +0000 Subject: [PATCH 31/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- .../(app)/tickets/queries/ticket-queries.ts | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/app/(app)/tickets/queries/ticket-queries.ts b/src/app/(app)/tickets/queries/ticket-queries.ts index 332b6494..2a96d723 100644 --- a/src/app/(app)/tickets/queries/ticket-queries.ts +++ b/src/app/(app)/tickets/queries/ticket-queries.ts @@ -198,7 +198,17 @@ export const GET_TICKET_QUERY = ` } `; -export const GET_TICKETS_QUERY = ` +/** + * `unreadNotificationCount` is gated behind `featureFlags.notifications` here + * (and in `boardCardTicketFragment` below) precisely because it has no + * runtime fallback: `ticket.graphqls` on the backend must ship the field + * before it is safe to request, or GraphQL validation fails the entire + * document and every board column / the tickets table comes back empty, not + * just the missing badge. Gating it behind a flag that is only flipped on + * once the backend has deployed turns that deploy-ordering hazard into an + * enforced condition instead of a comment-only convention. + */ +export const getTicketsQuery = () => ` query GetTickets($filter: TicketFilterInput, $pagination: CursorPaginationInput, $search: String) { tickets(filter: $filter, pagination: $pagination, search: $search, sort: { field: "order", direction: ASC }) { edges { @@ -254,8 +264,7 @@ export const GET_TICKETS_QUERY = ` key color } - # Unflagged, so it must not outrun the backend — see boardCardTicketFragment. - unreadNotificationCount + ${featureFlags.notifications.enabled() ? 'unreadNotificationCount' : ''} createdAt updatedAt resolvedAt @@ -273,6 +282,10 @@ export const GET_TICKETS_QUERY = ` } `; +// Backward-compatible export retained for existing call sites; resolves the +// query at call time so the feature-flag gate above is honored. +export const GET_TICKETS_QUERY = getTicketsQuery(); + // ===== Lifecycle board (custom statuses) ===== /** @@ -283,14 +296,14 @@ export const GET_TICKETS_QUERY = ` * merely missing a badge. `resolvedBy` rides the `ai-resolution` flag for the * same reason. * - * `unreadNotificationCount` is selected UNCONDITIONALLY and carries that same - * failure mode, because `ticket.graphqls` declares it with no feature flag — - * there is no flag to ride, and borrowing an unrelated one (`notifications` - * gates the notifications UI, not the ai-agent schema) would only move the - * breakage. It is therefore a deploy-ordering requirement: the saas-ai-agent - * carrying the field must ship BEFORE this frontend, or the board columns, the - * tickets table and the ticket picker (`use-ticket-options.ts`, same document) - * all come back empty. Same constraint at the `GET_TICKETS_QUERY` selection. + * `unreadNotificationCount` carries that same failure mode and is now gated + * behind `featureFlags.notifications`, so it is only selected once that flag + * has been turned on — which must happen after the saas-ai-agent backend + * carrying the field has shipped. This turns the previous deploy-ordering + * comment into an enforced condition: flipping the flag early reproduces the + * same all-columns-empty failure mode as `escalatedByUser`/`resolvedBy` + * would, so it must be sequenced the same way. Same gate applies at the + * `getTicketsQuery` selection above. */ const boardCardTicketFragment = () => ` fragment BoardCardTicket on Ticket { @@ -349,7 +362,7 @@ const boardCardTicketFragment = () => ` key color } - unreadNotificationCount + ${featureFlags.notifications.enabled() ? 'unreadNotificationCount' : ''} ${featureFlags.aiEscalation.enabled() ? 'escalatedByUser' : ''} ${featureFlags.aiResolution.enabled() ? 'resolvedBy' : ''} pendingApproval { @@ -682,3 +695,4 @@ export const ARCHIVE_RESOLVED_TICKETS_MUTATION = ` } } `; + From adceb044cca3af80a6c98103d425fa579a3eaa39 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:27 +0000 Subject: [PATCH 32/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/app/components/shared/log-drawer.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/components/shared/log-drawer.tsx b/src/app/components/shared/log-drawer.tsx index 64b9a516..16d2f538 100644 --- a/src/app/components/shared/log-drawer.tsx +++ b/src/app/components/shared/log-drawer.tsx @@ -125,8 +125,11 @@ export function LogDrawer({ {/* Info Card — vertical fields: Value on top, Label below */} {infoFields && infoFields.length > 0 && (
- {infoFields.map(field => ( -
+ {infoFields.map((field, index) => ( +
{typeof field.value === 'string' ? ( {field.value || '—'} ) : ( From 741e1aba857e610bea7446c376a6807e60f86e05 Mon Sep 17 00:00:00 2001 From: "flamingo[bot]" <277372822+flamingo[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 05:17:28 +0000 Subject: [PATCH 33/33] fix(adhoc-sweep-fixes): 40 review findings across 33 files --- src/lib/auth-api-client.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/lib/auth-api-client.ts b/src/lib/auth-api-client.ts index 3be8788f..316d9761 100644 --- a/src/lib/auth-api-client.ts +++ b/src/lib/auth-api-client.ts @@ -18,16 +18,19 @@ import { runtimeEnv } from './runtime-config'; import { refreshTokens } from './token-refresh-manager'; import { getAccessTokenSync, getRefreshToken, getTokenEpoch, isBearerAuthMode } from './token-store'; +function suffixFromHostname(hostname: string): string { + const parts = hostname.split('.'); + if (parts.length >= 2) { + return parts.slice(-2).join('.'); + } + return hostname; +} + function getDomainSuffix(): string { const sharedUrl = runtimeEnv.sharedHostUrl(); if (!sharedUrl) { if (typeof window !== 'undefined' && window.location?.hostname) { - const hostname = window.location.hostname; - const parts = hostname.split('.'); - if (parts.length >= 2) { - return parts.slice(-2).join('.'); - } - return hostname; + return suffixFromHostname(window.location.hostname); } return 'localhost'; } @@ -35,7 +38,7 @@ function getDomainSuffix(): string { const withoutProtocol = sharedUrl.replace(/^https?:\/\//, ''); const domain = withoutProtocol.split('/')[0].split(':')[0]; - return domain || 'localhost'; + return domain ? suffixFromHostname(domain) : 'localhost'; } export const SAAS_DOMAIN_SUFFIX = getDomainSuffix();