Render finalising status on the bulk-upload page and auto-advance to complete

The async finaliser (ADR-0005) introduced the `finalising` status, but the
server page's STATUS_CONFIG had no entry for it, so it fell through to the
`ready_for_processing` fallback ("Awaiting column mapping") and never mounted the
live poller — the page looked stuck even though the Lambda had inserted the
properties and written `complete`.

- Add the `finalising` card ("Uploading to ARA") to STATUS_CONFIG.
- Render OnboardingProgress during `finalising` so it polls live.
- Refresh the server page once when the poll first sees a terminal status
  (guarded by a new `serverStatus` prop to avoid a loop; uses react-query v4
  onSuccess, no useEffect) so it advances to the "Processing complete" card.
- Add a `finalising` → "Uploading to ARA" badge on the uploads list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-04 18:21:35 +00:00
parent fc2664aeef
commit 5fe86f01d8
4 changed files with 37 additions and 3 deletions

View file

@ -28,7 +28,7 @@ import {
RoofTypeValues,
} from "@/app/db/schema/landlord_overrides";
import { CLASSIFIER_FIELDS } from "@/lib/bulkUpload/columnFields";
import { statusLabel } from "@/lib/bulkUpload/types";
import { statusLabel, isTerminalStatus } from "@/lib/bulkUpload/types";
// Valid enum options per classifier category, for the editable dropdowns (#299).
const CATEGORY_VALUES: Record<string, readonly string[]> = {
@ -50,6 +50,10 @@ interface Props {
portfolioSlug: string;
portfolioId: string;
uploadId: string;
// The status at the last server render. Used to refresh the server page exactly
// once when polling first observes a terminal status (async finalise, ADR-0005),
// so the page advances from "Uploading to ARA" to the "Processing complete" card.
serverStatus: string;
isDomnaUser: boolean;
}
@ -60,10 +64,22 @@ export default function OnboardingProgress({
portfolioSlug,
portfolioId,
uploadId,
serverStatus,
isDomnaUser,
}: Props) {
const router = useRouter();
const progress = useBulkUploadProgress(portfolioId, uploadId);
const progress = useBulkUploadProgress(portfolioId, uploadId, {
// When the async finaliser finishes, the poll flips the status to a terminal
// value while the server page is still on `finalising`. Refresh once so the
// server re-renders the "Processing complete" / "failed" card. Guarding on the
// non-terminal serverStatus prevents a refresh loop: after the refresh the
// prop is terminal, so this no-ops.
onSuccess: (data) => {
if (!isTerminalStatus(serverStatus) && isTerminalStatus(data.upload.status)) {
router.refresh();
}
},
});
const combine = useRequestCombine(portfolioId, uploadId);
const finalize = useFinalize(portfolioId, uploadId);

View file

@ -67,6 +67,14 @@ const STATUS_CONFIG = {
body: "Matches ready, writing into your portfolio.",
cta: false,
},
finalising: {
icon: ArrowPathIcon,
iconBg: "bg-blue-50",
iconColor: "text-blue-500",
title: "Uploading to ARA",
body: "Creating your properties from the matched addresses. This can take a little while for large files.",
cta: false,
},
complete: {
icon: CheckCircleIcon,
iconBg: "bg-green-50",
@ -167,6 +175,7 @@ export default async function BulkUploadDetailPage(props: {
{(statusKey === "processing" ||
statusKey === "combining" ||
statusKey === "awaiting_review" ||
statusKey === "finalising" ||
statusKey === "complete" ||
statusKey === "failed") &&
upload.taskId && (
@ -174,6 +183,7 @@ export default async function BulkUploadDetailPage(props: {
portfolioSlug={slug}
portfolioId={upload.portfolioId}
uploadId={uploadId}
serverStatus={upload.status}
isDomnaUser={isDomnaUser}
/>
)}

View file

@ -14,6 +14,7 @@ import {
const STATUS_LABELS: Record<string, { label: string; classes: string }> = {
ready_for_processing: { label: "Ready", classes: "bg-amber-100 text-amber-700" },
processing: { label: "Processing", classes: "bg-blue-100 text-blue-700" },
finalising: { label: "Uploading to ARA", classes: "bg-blue-100 text-blue-700" },
complete: { label: "Complete", classes: "bg-green-100 text-green-700" },
failed: { label: "Failed", classes: "bg-red-100 text-red-700" },
};

View file

@ -197,7 +197,11 @@ export function useStartAddressMatching(portfolioId: string, uploadId: string) {
});
}
export function useBulkUploadProgress(portfolioId: string, uploadId: string) {
export function useBulkUploadProgress(
portfolioId: string,
uploadId: string,
options?: { onSuccess?: (data: ProgressView) => void },
) {
return useQuery<ProgressView, Error>({
queryKey: bulkUploadKeys.progress(uploadId),
queryFn: async () => {
@ -211,6 +215,9 @@ export function useBulkUploadProgress(portfolioId: string, uploadId: string) {
const status = data?.upload.status;
return status && isTerminalStatus(status) ? false : 3000;
},
// v4 onSuccess fires after each successful poll; callers use it to react to a
// status transition (e.g. refresh the server page once it goes terminal).
onSuccess: options?.onSuccess,
});
}