milestone 16: behavioral fixes for silent failures, locks, counters and the query log
CI / test (push) Failing after 11s
CI / cross (push) Failing after 25s
CI / docker (push) Failing after 24s
CI / test-aarch64 (push) Failing after 2m22s
CI / frontend (push) Successful in 43s

This commit is contained in:
2026-08-07 01:54:40 +02:00
parent 5802148887
commit 25455e5ae2
31 changed files with 2054 additions and 297 deletions
+24 -48
View File
@@ -1,9 +1,8 @@
import { useRef, useState, type FormEvent } from "react";
import { keepPreviousData, useQuery } from "@tanstack/react-query";
import { useState, type FormEvent } from "react";
import { useInfiniteQuery } from "@tanstack/react-query";
import * as api from "@/lib/api";
import { formatMicros, formatTime } from "@/lib/format";
import { queriesQuery } from "@/lib/queries";
import { handleUnauthorized } from "@/lib/queryClient";
import { queriesInfiniteQuery } from "@/lib/queries";
import type { QueriesFilter, QueryRow } from "@/lib/types";
import { qtypeName } from "./qtype";
@@ -12,6 +11,10 @@ const inputClass =
const buttonClass =
"rounded border border-zinc-300 px-3 py-1.5 text-sm font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700";
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function datetimeLocalToUnix(value: string): number | undefined {
if (value === "") return undefined;
const ms = new Date(value).getTime();
@@ -77,25 +80,20 @@ export default function QueryLogPage() {
const [until, setUntil] = useState("");
const [applied, setApplied] = useState<QueriesFilter>({});
const [extra, setExtra] = useState<QueryRow[]>([]);
const [cursorOverride, setCursorOverride] = useState<number | null | undefined>(undefined);
const [loadingMore, setLoadingMore] = useState(false);
const [moreError, setMoreError] = useState<string | null>(null);
const generation = useRef(0);
const base = useQuery({ ...queriesQuery(applied), placeholderData: keepPreviousData });
const base = useInfiniteQuery(queriesInfiniteQuery(applied));
const rows: QueryRow[] = [...(base.data?.queries ?? []), ...extra];
const nextBefore = cursorOverride !== undefined ? cursorOverride : (base.data?.next_before ?? null);
const pages = base.data?.pages ?? [];
const rows: QueryRow[] = pages.flatMap((page) => page.queries);
const filterActive = Object.keys(applied).length > 0;
function resetAccumulation() {
generation.current += 1;
setExtra([]);
setCursorOverride(undefined);
setLoadingMore(false);
setMoreError(null);
}
// `base.hasNextPage` reads the query state, which is empty while placeholder
// data stands in for a filter change; derive the cursor from what is on
// screen so the button keeps its place instead of flashing "end of log".
const lastPage = pages[pages.length - 1];
const hasMore = lastPage !== undefined && lastPage.next_before !== null;
// A 401 is already redirecting via the cache-level handleUnauthorized.
const isUnauthorized = base.error instanceof api.ApiError && base.error.status === 401;
const moreError = base.isFetchNextPageError && !isUnauthorized ? errorMessage(base.error) : null;
function applyFilters(event: FormEvent) {
event.preventDefault();
@@ -109,7 +107,6 @@ export default function QueryLogPage() {
const untilTs = datetimeLocalToUnix(until);
if (untilTs !== undefined) filter.until = untilTs;
setApplied(filter);
resetAccumulation();
}
function clearFilters() {
@@ -119,32 +116,11 @@ export default function QueryLogPage() {
setSince("");
setUntil("");
setApplied({});
resetAccumulation();
}
function loadMore() {
if (nextBefore === null || loadingMore || base.isPlaceholderData) return;
const startedGeneration = generation.current;
setLoadingMore(true);
setMoreError(null);
api.getQueries({ ...applied, before: nextBefore })
.then((page) => {
if (generation.current !== startedGeneration) return;
setExtra((prev) => [...prev, ...page.queries]);
setCursorOverride(page.next_before);
})
.catch((error: unknown) => {
if (error instanceof api.ApiError && error.status === 401) {
handleUnauthorized(error);
return;
}
if (generation.current !== startedGeneration) return;
setMoreError(error instanceof Error ? error.message : String(error));
})
.finally(() => {
if (generation.current !== startedGeneration) return;
setLoadingMore(false);
});
if (!hasMore || base.isFetchingNextPage || base.isPlaceholderData) return;
void base.fetchNextPage();
}
return (
@@ -236,16 +212,16 @@ export default function QueryLogPage() {
<div className="mt-3 flex items-center gap-3">
<p className="text-sm text-zinc-500">
Showing {rows.length} {rows.length === 1 ? "query" : "queries"}
{nextBefore === null ? " — end of log" : ""}
{hasMore ? "" : " — end of log"}
</p>
{nextBefore !== null && (
{hasMore && (
<button
type="button"
onClick={loadMore}
disabled={loadingMore || base.isPlaceholderData}
disabled={base.isFetchingNextPage || base.isPlaceholderData}
className={buttonClass}
>
{loadingMore ? "Loading…" : "Load more"}
{base.isFetchingNextPage ? "Loading…" : "Load more"}
</button>
)}
</div>