force db sync button (#1167)

This commit is contained in:
BilalG1 2026-02-09 10:53:55 -08:00 committed by GitHub
parent b182c1b03d
commit 2072dd4b3d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 113 additions and 53 deletions

View File

@ -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);

View File

@ -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");
}

View File

@ -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<AbortController | null>(null);
const inFlightRef = useRef(false);
const summarySamplesRef = useRef<Array<{
timestampMillis: number,
@ -341,6 +343,41 @@ export default function PageClient() {
runAsynchronouslyWithAlert(loadStatus);
}, [loadStatus]);
const forceTriggerSync = useCallback(async () => {
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() {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Fusebox</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{!fusebox ? (
<div className="space-y-3">
<Skeleton className="h-6 w-40" />
<Skeleton className="h-6 w-40" />
<Skeleton className="h-6 w-48" />
<Skeleton className="h-9 w-24" />
</div>
) : (
<>
<div className="flex items-center justify-between gap-6">
<div>
<Typography type="p" className="text-sm font-medium">Sequencer</Typography>
<Typography type="p" className="text-xs text-muted-foreground">Assigns sequence IDs and queues sync work.</Typography>
</div>
<Switch
checked={fusebox.sequencerEnabled}
onCheckedChange={(checked) => setFusebox((current) => current ? { ...current, sequencerEnabled: checked } : current)}
/>
<div className="grid gap-4 lg:grid-cols-3">
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>Fusebox</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{!fusebox ? (
<div className="space-y-3">
<Skeleton className="h-6 w-40" />
<Skeleton className="h-6 w-40" />
<Skeleton className="h-6 w-48" />
<Skeleton className="h-9 w-24" />
</div>
<div className="flex items-center justify-between gap-6">
<div>
<Typography type="p" className="text-sm font-medium">Poller</Typography>
<Typography type="p" className="text-xs text-muted-foreground">Dispatches queued sync jobs to QStash.</Typography>
) : (
<>
<div className="flex items-center justify-between gap-6">
<div>
<Typography type="p" className="text-sm font-medium">Sequencer</Typography>
<Typography type="p" className="text-xs text-muted-foreground">Assigns sequence IDs and queues sync work.</Typography>
</div>
<Switch
checked={fusebox.sequencerEnabled}
onCheckedChange={(checked) => setFusebox((current) => current ? { ...current, sequencerEnabled: checked } : current)}
/>
</div>
<div className="flex items-center justify-between gap-6">
<div>
<Typography type="p" className="text-sm font-medium">Poller</Typography>
<Typography type="p" className="text-xs text-muted-foreground">Dispatches queued sync jobs to QStash.</Typography>
</div>
<Switch
checked={fusebox.pollerEnabled}
onCheckedChange={(checked) => setFusebox((current) => current ? { ...current, pollerEnabled: checked } : current)}
/>
</div>
<Switch
checked={fusebox.pollerEnabled}
onCheckedChange={(checked) => setFusebox((current) => current ? { ...current, pollerEnabled: checked } : current)}
/>
</div>
<div className="flex justify-end">
<Button onClick={saveFusebox} disabled={!fuseboxDirty || savingFusebox} loading={savingFusebox}>
Save
<div className="flex justify-end">
<Button onClick={saveFusebox} disabled={!fuseboxDirty || savingFusebox} loading={savingFusebox}>
Save
</Button>
</div>
</>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Force Sync</CardTitle>
<CardDescription>Manually trigger sequencer and poller.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center gap-4">
<div className="flex items-center gap-2">
<div className={`h-2.5 w-2.5 rounded-full ${forceSyncRunning ? "bg-yellow-500 animate-pulse" : "bg-green-500"}`} />
<Typography type="p" className="text-sm">{forceSyncRunning ? "Running" : "Idle"}</Typography>
</div>
<div className="flex items-center gap-2">
<Button onClick={() => runAsynchronouslyWithAlert(forceTriggerSync)} disabled={forceSyncRunning} loading={forceSyncRunning}>
Run Now
</Button>
{forceSyncRunning && (
<Button onClick={cancelForceSync} variant="destructive" size="sm">
Cancel
</Button>
</div>
</>
)}
</CardContent>
</Card>
)}
</div>
</CardContent>
</Card>
</div>
</PageLayout>
);