From a97cbaee4c8ed428a77cafa51679ae90b655b584 Mon Sep 17 00:00:00 2001 From: Bilal Godil Date: Mon, 9 Feb 2026 09:52:39 -0800 Subject: [PATCH] force db sync button --- .../internal/external-db-sync/poller/route.ts | 15 +- .../external-db-sync/sequencer/route.ts | 9 +- .../external-db-sync/page-client.tsx | 142 +++++++++++++----- 3 files changed, 113 insertions(+), 53 deletions(-) diff --git a/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts b/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts index 6e82f8bb48..a12edab49f 100644 --- a/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts +++ b/apps/backend/src/app/api/latest/internal/external-db-sync/poller/route.ts @@ -53,11 +53,6 @@ function getPollerClaimLimit(): number { return parsed; } -function getLocalApiBaseUrl(): string { - const prefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81"); - return `http://localhost:${prefix}02`; -} - export const GET = createSmartRouteHandler({ metadata: { summary: "Poll outgoing requests and push to QStash", @@ -70,7 +65,7 @@ export const GET = createSmartRouteHandler({ auth: yupObject({}).nullable().optional(), method: yupString().oneOf(["GET"]).defined(), headers: yupObject({ - authorization: yupTuple([yupString().defined()]).defined(), + authorization: yupTuple([yupString().defined()]).optional(), }).defined(), query: yupObject({ maxDurationMs: yupString().optional(), @@ -85,12 +80,14 @@ export const GET = createSmartRouteHandler({ requests_processed: yupNumber().defined(), }).defined(), }), - handler: async ({ headers, query }) => { - const authHeader = headers.authorization[0]; - if (authHeader !== `Bearer ${getEnvVariable("CRON_SECRET")}`) { + handler: async ({ headers, query, auth }) => { + const isAdmin = auth?.type === "admin" && auth.project.id === "internal"; + const authHeader = headers.authorization?.[0]; + if (!isAdmin && authHeader !== `Bearer ${getEnvVariable("CRON_SECRET")}`) { throw new StatusError(401, "Unauthorized"); } + return await traceSpan("external-db-sync.poller", async (span) => { const startTime = performance.now(); const maxDurationMs = parseMaxDurationMs(query.maxDurationMs); diff --git a/apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts b/apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts index 2a40b3e0ef..7a51da1f27 100644 --- a/apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts +++ b/apps/backend/src/app/api/latest/internal/external-db-sync/sequencer/route.ts @@ -165,7 +165,7 @@ export const GET = createSmartRouteHandler({ auth: yupObject({}).nullable().optional(), method: yupString().oneOf(["GET"]).defined(), headers: yupObject({ - authorization: yupTuple([yupString().defined()]).defined(), + authorization: yupTuple([yupString().defined()]).optional(), }).defined(), query: yupObject({ maxDurationMs: yupString().optional(), @@ -180,9 +180,10 @@ export const GET = createSmartRouteHandler({ iterations: yupNumber().defined(), }).defined(), }), - handler: async ({ headers, query }) => { - const authHeader = headers.authorization[0]; - if (authHeader !== `Bearer ${getEnvVariable("CRON_SECRET")}`) { + handler: async ({ headers, query, auth }) => { + const isAdmin = auth?.type === "admin" && auth.project.id === "internal"; + const authHeader = headers.authorization?.[0]; + if (!isAdmin && authHeader !== `Bearer ${getEnvVariable("CRON_SECRET")}`) { throw new StatusError(401, "Unauthorized"); } diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/external-db-sync/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/external-db-sync/page-client.tsx index 3a68d8d9d6..c1e7d57951 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/external-db-sync/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/external-db-sync/page-client.tsx @@ -226,6 +226,8 @@ export default function PageClient() { const [loading, setLoading] = useState(false); const [autoRefresh, setAutoRefresh] = useState(true); const [savingFusebox, setSavingFusebox] = useState(false); + const [forceSyncRunning, setForceSyncRunning] = useState(false); + const forceSyncAbortRef = useRef(null); const inFlightRef = useRef(false); const summarySamplesRef = useRef { + const abortController = new AbortController(); + forceSyncAbortRef.current = abortController; + setForceSyncRunning(true); + try { + const endpoints = [ + "/internal/external-db-sync/sequencer", + "/internal/external-db-sync/poller", + ]; + await Promise.all(endpoints.map(async (endpoint) => { + const response = await adminApp[stackAppInternalsSymbol].sendRequest( + endpoint, + { method: "GET", signal: abortController.signal }, + "admin", + ); + if (!response.ok) { + const body = await response.json().catch(() => null); + const message = typeof body?.error === "string" ? body.error : `Failed to trigger ${endpoint}: ${response.status}`; + throw new Error(message); + } + })); + await loadStatus(); + } catch (err) { + if (err instanceof DOMException && err.name === "AbortError") return; + throw err; + } finally { + forceSyncAbortRef.current = null; + setForceSyncRunning(false); + } + }, [adminApp, loadStatus]); + + const cancelForceSync = useCallback(() => { + forceSyncAbortRef.current?.abort(); + }, []); + useEffect(() => { runAsynchronously(loadStatus); }, [loadStatus]); @@ -682,50 +719,75 @@ export default function PageClient() { - - - Fusebox - - - {!fusebox ? ( -
- - - - -
- ) : ( - <> -
-
- Sequencer - Assigns sequence IDs and queues sync work. -
- setFusebox((current) => current ? { ...current, sequencerEnabled: checked } : current)} - /> +
+ + + Fusebox + + + {!fusebox ? ( +
+ + + +
-
-
- Poller - Dispatches queued sync jobs to QStash. + ) : ( + <> +
+
+ Sequencer + Assigns sequence IDs and queues sync work. +
+ setFusebox((current) => current ? { ...current, sequencerEnabled: checked } : current)} + />
- setFusebox((current) => current ? { ...current, pollerEnabled: checked } : current)} - /> -
+
+
+ Poller + Dispatches queued sync jobs to QStash. +
+ setFusebox((current) => current ? { ...current, pollerEnabled: checked } : current)} + /> +
+ +
+ +
+ + )} + + -
- + {forceSyncRunning && ( + -
- - )} - - + )} +
+
+
+
);