From f07df206a1cafc3688ca31e3094c70c88eec28c7 Mon Sep 17 00:00:00 2001 From: JasonWildMe Date: Fri, 10 Apr 2026 11:47:46 -0700 Subject: [PATCH 01/13] Fix detection completion percentage using wrong population and denominator The bulk import task page (/react/bulk-import-task) could show detection < 100% even when all MediaAssets had completed detection and annotations. Three bugs in ImportTask.iaSummaryJson(): 1. Numerator counted ANY asset with detectionStatus complete/pending, including non-IA-eligible assets (videos, corrupt images). These incremented numDetectionComplete but not numAllowedIA, so the completion check (numDetectionComplete == numAllowedIA) could never pass, or the percentage could exceed 100%. 2. Assets that acquired annotations through non-IA paths (manual annotation, data import, migration) were not counted as detection- complete because the check only looked at detectionStatus, not whether annotations actually existed. 3. The progress denominator used numAssets (total) while the completion check used numAllowedIA (IA-eligible), so even when the equality check passed, in-progress percentages were diluted by ineligible assets. Fix: use a single isEligible flag per asset that gates both the numAllowedIA and numDetectionComplete counters. An eligible asset counts as detection-complete if it has detectionStatus complete/pending OR has non-trivial annotations (i.e. not just the whole-image placeholder bounding box). Both legacy and non-legacy paths now use numAllowedIA as denominator for the completion check, progress fraction, and divide guard. Co-Authored-By: Claude Opus 4.6 --- .../java/org/ecocean/media/MediaAsset.java | 7 +++++ .../ecocean/servlet/importer/ImportTask.java | 30 +++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/ecocean/media/MediaAsset.java b/src/main/java/org/ecocean/media/MediaAsset.java index da569bd1bf..dbad3f19aa 100644 --- a/src/main/java/org/ecocean/media/MediaAsset.java +++ b/src/main/java/org/ecocean/media/MediaAsset.java @@ -630,6 +630,13 @@ public int numAnnotations() { return getAnnotations().size(); } + public boolean hasNonTrivialAnnotations() { + for (Annotation ann : getAnnotations()) { + if (!ann.isTrivial()) return true; + } + return false; + } + public List getTaxonomies(Shepherd myShepherd) { Set taxis = new HashSet(); diff --git a/src/main/java/org/ecocean/servlet/importer/ImportTask.java b/src/main/java/org/ecocean/servlet/importer/ImportTask.java index acc0590c22..032afb0436 100644 --- a/src/main/java/org/ecocean/servlet/importer/ImportTask.java +++ b/src/main/java/org/ecocean/servlet/importer/ImportTask.java @@ -595,20 +595,20 @@ public JSONObject iaSummaryJson(Shepherd myShepherd) { for (MediaAsset ma : this.getMediaAssets()) { numAnnotations += ma.numAnnotations(); if (ma.getAcmId() != null) numAcmId++; - // check if we can get validity off the image before the expensive check of hitting the AssetStore + boolean isEligible = false; if (ma.isValidImageForIA() != null) { - if (ma.isValidImageForIA().booleanValue()) numAllowedIA++; + if (ma.isValidImageForIA().booleanValue()) isEligible = true; } else if (ma.validateSourceImage()) { - numAllowedIA++; + isEligible = true; + } + if (isEligible) numAllowedIA++; + if (isEligible && + ((ma.getDetectionStatus() != null && + (ma.getDetectionStatus().equals("complete") || + ma.getDetectionStatus().equals("pending"))) + || ma.hasNonTrivialAnnotations())) { + numDetectionComplete++; } -/* - if ((ma.isValidImageForIA() == null) || !ma.isValidImageForIA().booleanValue()) { - invalidMediaAssets.add(asset); - } - */ - if ((ma.getDetectionStatus() != null) && - (ma.getDetectionStatus().equals("complete") || - ma.getDetectionStatus().equals("pending"))) numDetectionComplete++; } JSONObject pj = new JSONObject(); pj.put("statsMediaAssets", statsMA); @@ -625,7 +625,7 @@ public JSONObject iaSummaryJson(Shepherd myShepherd) { pj.put("detectionPercent", 1.0); pj.put("detectionStatus", "complete"); } else { - if (numAssets > 0) pj.put("detectionPercent", new Double(numDetectionComplete) / new Double(numAssets)); + if (numAllowedIA > 0) pj.put("detectionPercent", (double)numDetectionComplete / (double)numAllowedIA); pj.put("detectionStatus", "sent"); } if (this.iaTaskRequestedIdentification()) { @@ -656,13 +656,13 @@ public JSONObject iaSummaryJson(Shepherd myShepherd) { // legacy flavor } else if ((this.getIATask() == null) && (numDetectionComplete > 0)) { pipelineStarted = true; - if (numDetectionComplete == numAssets) { + if (numDetectionComplete == numAllowedIA) { pj.put("detectionPercent", 1.0); pj.put("detectionStatus", "complete"); } else { - if (numAssets > 0) + if (numAllowedIA > 0) pj.put("detectionPercent", - new Double(numDetectionComplete) / new Double(numAssets)); + (double)numDetectionComplete / (double)numAllowedIA); pj.put("detectionStatus", "sent"); } pj.put("identificationStatus", "unknown"); From 41f23ab9b8a7d7a80ce5aa5252e56d306a330b28 Mon Sep 17 00:00:00 2001 From: JasonWildMe Date: Fri, 10 Apr 2026 18:12:19 -0700 Subject: [PATCH 02/13] Remove stale ImportTask.status check from re-ID button gate The re-identification button on the bulk import task page was gated on task.status === "complete", but ImportTask.status is never transitioned from "processing-detection" to "complete" after IA detection finishes. The only code paths that set it to "complete" are when detection is skipped or via the legacy StandardImport pathway. Since task.iaSummary.detectionStatus already accurately reflects whether detection has completed (and is checked on the same line), the ImportTask.status check is both redundant and broken. Remove it so the button is enabled once detection is genuinely complete. Co-Authored-By: Claude Opus 4.6 --- .../src/pages/BulkImport/BulkImportTask.jsx | 1240 ++++++++--------- 1 file changed, 619 insertions(+), 621 deletions(-) diff --git a/frontend/src/pages/BulkImport/BulkImportTask.jsx b/frontend/src/pages/BulkImport/BulkImportTask.jsx index f2469bd202..9eaa5df000 100644 --- a/frontend/src/pages/BulkImport/BulkImportTask.jsx +++ b/frontend/src/pages/BulkImport/BulkImportTask.jsx @@ -1,621 +1,619 @@ -import React, { useState, useContext, useEffect } from "react"; -import { - Container, - Row, - Col, - Breadcrumb, - Button, - Spinner, -} from "react-bootstrap"; -import { FormattedMessage, useIntl } from "react-intl"; -import { FaImage } from "react-icons/fa"; -import useGetBulkImportTask from "../../models/bulkImport/useGetBulkImportTask"; -import { ProgressCard } from "../../components/ProgressCard"; -import ThemeColorContext from "../../ThemeColorProvider"; -import InfoAccordion from "../../components/InfoAccordion"; -import SimpleDataTable from "../../components/SimpleDataTable"; -import { Modal } from "react-bootstrap"; -import { Suspense, lazy } from "react"; -import useGetSiteSettings from "../../models/useGetSiteSettings"; -import axios from "axios"; -import MainButton from "../../components/MainButton"; -import convertToTreeData from "../../utils/converToTreeData"; -import { useLocalObservable, observer } from "mobx-react-lite"; -import { BulkImportTaskStore } from "./BulkImportTaskStore"; - -const TreeSelect = lazy(() => import("antd/es/tree-select")); - -const BulkImportTask = observer(() => { - const intl = useIntl(); - const theme = useContext(ThemeColorContext); - const [showError, setShowError] = useState(false); - const taskId = new URLSearchParams(window.location.search).get("id"); - const { task, isLoading, error, refetch } = useGetBulkImportTask(taskId); - const { data: siteData } = useGetSiteSettings(); - const [userRoles, setUserRoles] = useState(null); - const store = useLocalObservable(() => new BulkImportTaskStore()); - const [rowsPerPage, setRowsPerPage] = useState(10); - - const previousLocationID = task?.matchingLocations || []; - - const fetchData = async () => { - const response = await axios.get("/api/v3/user"); - setUserRoles(response.data.roles || []); - }; - - useEffect(() => { - fetchData(); - }, []); - - useEffect(() => { - if (!siteData?.locationData?.locationID) return; - const options = convertToTreeData(siteData?.locationData?.locationID) || []; - store.setOptions(options); - }, [siteData, store]); - - useEffect(() => { - if (!store.locationOptions.length) return; - if (!previousLocationID?.length) return; - if (store.locationID.length) return; - store.initFromPrevious(previousLocationID); - }, [store, store.locationOptions.length, previousLocationID?.join?.(",")]); - - useEffect(() => { - if (error?.message || task?.status === "failed") { - setShowError(true); - } - }, [error, task?.status]); - - if (isLoading) { - return ( -
- -
- ); - } - - const deleteTask = async () => { - if (!task?.id) return; - - const confirmed = window.confirm( - intl.formatMessage({ id: "BULK_IMPORT_DELETE_TASK_CONFIRM" }), - ); - if (!confirmed) return; - - try { - const res = await fetch(`/api/v3/bulk-import/${task.id}`, { - method: "DELETE", - }); - - if (!res.ok) { - const text = await res.text(); - throw new Error(text || `HTTP ${res.status}`); - } - - alert(intl.formatMessage({ id: "BULK_IMPORT_TASK_DELETED" })); - window.location.href = "/react/"; - } catch (err) { - console.error("Failed to delete import task:", err); - alert( - intl.formatMessage( - { id: "BULK_IMPORT_TASK_DELETE_ERROR" }, - { error: err.message || "" }, - ), - ); - } - }; - - const tableData = - task?.encounters?.map((item) => { - const taskArray = - task?.iaSummary?.statsAnnotations?.encounterTaskInfo?.[item.id] || []; - const classArray = - Array.isArray(taskArray) && taskArray?.length > 0 ? taskArray[0] : []; - return { - encounterID: item.id, - encounterDate: item.date, - user: item.submitter?.displayName || "-", - occurrenceID: item.occurrenceId || "-", - individualID: item.individualId || "-", - individualName: item.individualDisplayName || item.individualId || "-", - imageCount: item.numberMediaAssets, - class: classArray, - createdMillis: item.createdMillis || "-", - }; - }) || []; - - const sortedTableData = tableData - ?.sort((a, b) => { - return new Date(a.createdMillis) - new Date(b.createdMillis); - }) - .map((item, index) => ({ - tableID: index + 1, - ...item, - })); - - const columns = [ - { - name: "#", - cell: (row) => row.tableID, - selector: (row) => row.tableID, - }, - { - name: "Encounter ID", - selector: (row) => row.encounterID, - cell: (row) => - row.encounterID ? ( - - {row.encounterID} - - ) : ( - "-" - ), - }, - { - name: "Encounter Date", - cell: (row) => row.encounterDate, - selector: (row) => row.encounterDate, - }, - { - name: "User", - cell: (row) => row.user, - selector: (row) => row.user, - }, - { - name: "Sighting", - cell: (row) => - row.occurrenceID !== "-" ? ( - - {row.occurrenceID} - - ) : ( - "-" - ), - selector: (row) => row.occurrenceID, - }, - { - name: "Individual ID", - selector: (row) => row.individualName, - cell: (row) => - row.individualName !== "-" ? ( - - {row.individualName} - - ) : ( - "-" - ), - }, - { - name: "Image Count", - cell: (row) => row.imageCount, - selector: (row) => row.imageCount, - }, - { - name: "Class", - selector: (row) => row.class, - cell: (row) => { - const arr = row.class; - if (Array.isArray(arr) && arr.length === 3) { - const link = `/iaResults.jsp?taskId=${arr[0]}`; - return ( - - {arr[2]} {": "} - {arr[1]} - - ); - } - return "-"; - }, - }, - ]; - - const ExcelIcon = () => ( - - - - - - - - ); - - return ( - -
-

- -

- - - {intl.formatMessage( - { - id: "BULK_IMPORT_TASK_BREADCRUMB", - defaultMessage: "Import Task: {id}", - }, - { id: task?.id }, - )} - - -
- - -
- {[ - { - title: intl.formatMessage({ - id: "IMPORT", - }), - progress: - task?.importPercent || - task?.status === "complete" || - task?.iaSummary?.detectionStatus === "complete" || - task?.status === "processing-pipeline" - ? 1 - : 0, - status: (() => { - if ( - task?.importPercent === 1 || - task?.status === "complete" || - task?.iaSummary?.detectionStatus === "complete" || - task?.status === "processing-pipeline" - ) { - return "complete"; - } else if (task?.importPercent) { - return "in_progress"; - } else { - return "not_started"; - } - })(), - }, - { - title: intl.formatMessage({ - id: "DETECTION", - }), - progress: task?.iaSummary?.detectionPercent || 0, - status: task?.iaSummary?.detectionStatus || "not_started", - }, - { - title: intl.formatMessage({ - id: "IDENTIFICATION", - }), - progress: task?.iaSummary?.identificationPercent || 0, - status: task?.iaSummary?.identificationStatus || "not_started", - }, - ].map(({ title, progress, status }) => ( - - ))} -
-
- -
-
- -
- -
- } - title={intl.formatMessage( - { - id: "PHOTOS_UPLOADED_TITLE", - defaultMessage: "Images uploaded: {count}", - }, - { count: task?.iaSummary?.numberMediaAssets || 0 }, - )} - data={[ - { - label: intl.formatMessage({ - id: "HAS_ACM_ID", - defaultMessage: "Has acmID", - }), - value: task?.iaSummary?.numberMediaAssetACMIds || 0, - }, - { - label: intl.formatMessage({ - id: "TOTAL_ANNOTATIONS", - defaultMessage: "Total Annotations", - }), - value: task?.iaSummary?.numberAnnotations || 0, - }, - { - label: intl.formatMessage({ - id: "VALID_FOR_IMAGE_ANALYSIS", - defaultMessage: "Valid for Image Analysis", - }), - value: task?.iaSummary?.numberMediaAssetValidIA || 0, - }, - { - label: intl.formatMessage({ - id: "TOTAL_MARKED_INDIVIDUALS", - defaultMessage: "Total Marked Individuals", - }), - value: task?.numberMarkedIndividuals || 0, - }, - ]} - /> - } - title={intl.formatMessage( - { - id: "SPREADSHEET_UPLOADED_TITLE", - defaultMessage: "Spreadsheet Uploaded: {fileName}", - }, - { fileName: task?.sourceName || "N/A" }, - )} - data={[ - { - label: intl.formatMessage({ - id: "FILE_UPLOADED_SUCCESSFULLY", - defaultMessage: "File Uploaded Successfully", - }), - }, - ]} - /> -
- -
- -
- -
- - - -
- -
-
-

- -

-
- -
- - - -
- Loading location picker...
}> - - store.handleStrictChange(vals, labels, extra) - } - /> - - - -
- - - - { - setShowError(false); - axios - .get( - `/appadmin/resendBulkImportID.jsp?importIdTask=${taskId}${store.locationIDString}`, - ) - .then((response) => { - if (response.status === 200) { - alert( - intl.formatMessage({ - id: "BULK_IMPORT_RE_ID_SUCCESS", - defaultMessage: - "Re-identification task started successfully.", - }), - ); - window.location.reload(); - } else { - throw new Error( - intl.formatMessage({ - id: "BULK_IMPORT_RE_ID_ERROR", - defaultMessage: - "Failed to start re-identification task.", - }), - ); - } - }) - .catch((error) => { - console.error( - "Error starting re-identification task:", - error, - ); - alert( - intl.formatMessage({ - id: "BULK_IMPORT_RE_ID_ERROR", - defaultMessage: "Failed to start re-identification task.", - }), - ); - }); - }} - backgroundColor={theme.wildMeColors.cyan700} - color={theme.defaultColors.white} - noArrow={true} - style={{ - width: "auto", - height: "40px", - fontSize: "1rem", - marginLeft: 0, - }} - > - - - {((!userRoles?.includes("admin") && - !userRoles?.includes("researcher")) || - !store.locationIDString || - task?.status !== "complete" || - task?.iaSummary?.detectionStatus !== "complete") && ( -

- -

- )} - -
- -
- -
- - - - - -
- setShowError(false)} centered> - - - - - - -

- {error?.message || - intl.formatMessage({ - id: "BULK_IMPORT_TASK_ERROR_DEFAULT", - defaultMessage: "An error occurred while loading the task.", - })} -

-
- - - - -
-
- ); -}); - -export default BulkImportTask; +import React, { useState, useContext, useEffect } from "react"; +import { + Container, + Row, + Col, + Breadcrumb, + Button, + Spinner, +} from "react-bootstrap"; +import { FormattedMessage, useIntl } from "react-intl"; +import { FaImage } from "react-icons/fa"; +import useGetBulkImportTask from "../../models/bulkImport/useGetBulkImportTask"; +import { ProgressCard } from "../../components/ProgressCard"; +import ThemeColorContext from "../../ThemeColorProvider"; +import InfoAccordion from "../../components/InfoAccordion"; +import SimpleDataTable from "../../components/SimpleDataTable"; +import { Modal } from "react-bootstrap"; +import { Suspense, lazy } from "react"; +import useGetSiteSettings from "../../models/useGetSiteSettings"; +import axios from "axios"; +import MainButton from "../../components/MainButton"; +import convertToTreeData from "../../utils/converToTreeData"; +import { useLocalObservable, observer } from "mobx-react-lite"; +import { BulkImportTaskStore } from "./BulkImportTaskStore"; + +const TreeSelect = lazy(() => import("antd/es/tree-select")); + +const BulkImportTask = observer(() => { + const intl = useIntl(); + const theme = useContext(ThemeColorContext); + const [showError, setShowError] = useState(false); + const taskId = new URLSearchParams(window.location.search).get("id"); + const { task, isLoading, error, refetch } = useGetBulkImportTask(taskId); + const { data: siteData } = useGetSiteSettings(); + const [userRoles, setUserRoles] = useState(null); + const store = useLocalObservable(() => new BulkImportTaskStore()); + const [rowsPerPage, setRowsPerPage] = useState(10); + + const previousLocationID = task?.matchingLocations || []; + + const fetchData = async () => { + const response = await axios.get("/api/v3/user"); + setUserRoles(response.data.roles || []); + }; + + useEffect(() => { + fetchData(); + }, []); + + useEffect(() => { + if (!siteData?.locationData?.locationID) return; + const options = convertToTreeData(siteData?.locationData?.locationID) || []; + store.setOptions(options); + }, [siteData, store]); + + useEffect(() => { + if (!store.locationOptions.length) return; + if (!previousLocationID?.length) return; + if (store.locationID.length) return; + store.initFromPrevious(previousLocationID); + }, [store, store.locationOptions.length, previousLocationID?.join?.(",")]); + + useEffect(() => { + if (error?.message || task?.status === "failed") { + setShowError(true); + } + }, [error, task?.status]); + + if (isLoading) { + return ( +
+ +
+ ); + } + + const deleteTask = async () => { + if (!task?.id) return; + + const confirmed = window.confirm( + intl.formatMessage({ id: "BULK_IMPORT_DELETE_TASK_CONFIRM" }), + ); + if (!confirmed) return; + + try { + const res = await fetch(`/api/v3/bulk-import/${task.id}`, { + method: "DELETE", + }); + + if (!res.ok) { + const text = await res.text(); + throw new Error(text || `HTTP ${res.status}`); + } + + alert(intl.formatMessage({ id: "BULK_IMPORT_TASK_DELETED" })); + window.location.href = "/react/"; + } catch (err) { + console.error("Failed to delete import task:", err); + alert( + intl.formatMessage( + { id: "BULK_IMPORT_TASK_DELETE_ERROR" }, + { error: err.message || "" }, + ), + ); + } + }; + + const tableData = + task?.encounters?.map((item) => { + const taskArray = + task?.iaSummary?.statsAnnotations?.encounterTaskInfo?.[item.id] || []; + const classArray = + Array.isArray(taskArray) && taskArray?.length > 0 ? taskArray[0] : []; + return { + encounterID: item.id, + encounterDate: item.date, + user: item.submitter?.displayName || "-", + occurrenceID: item.occurrenceId || "-", + individualID: item.individualId || "-", + individualName: item.individualDisplayName || item.individualId || "-", + imageCount: item.numberMediaAssets, + class: classArray, + createdMillis: item.createdMillis || "-", + }; + }) || []; + + const sortedTableData = tableData + ?.sort((a, b) => { + return new Date(a.createdMillis) - new Date(b.createdMillis); + }) + .map((item, index) => ({ + tableID: index + 1, + ...item, + })); + + const columns = [ + { + name: "#", + cell: (row) => row.tableID, + selector: (row) => row.tableID, + }, + { + name: "Encounter ID", + selector: (row) => row.encounterID, + cell: (row) => + row.encounterID ? ( + + {row.encounterID} + + ) : ( + "-" + ), + }, + { + name: "Encounter Date", + cell: (row) => row.encounterDate, + selector: (row) => row.encounterDate, + }, + { + name: "User", + cell: (row) => row.user, + selector: (row) => row.user, + }, + { + name: "Sighting", + cell: (row) => + row.occurrenceID !== "-" ? ( + + {row.occurrenceID} + + ) : ( + "-" + ), + selector: (row) => row.occurrenceID, + }, + { + name: "Individual ID", + selector: (row) => row.individualName, + cell: (row) => + row.individualName !== "-" ? ( + + {row.individualName} + + ) : ( + "-" + ), + }, + { + name: "Image Count", + cell: (row) => row.imageCount, + selector: (row) => row.imageCount, + }, + { + name: "Class", + selector: (row) => row.class, + cell: (row) => { + const arr = row.class; + if (Array.isArray(arr) && arr.length === 3) { + const link = `/iaResults.jsp?taskId=${arr[0]}`; + return ( + + {arr[2]} {": "} + {arr[1]} + + ); + } + return "-"; + }, + }, + ]; + + const ExcelIcon = () => ( + + + + + + + + ); + + return ( + +
+

+ +

+ + + {intl.formatMessage( + { + id: "BULK_IMPORT_TASK_BREADCRUMB", + defaultMessage: "Import Task: {id}", + }, + { id: task?.id }, + )} + + +
+ + +
+ {[ + { + title: intl.formatMessage({ + id: "IMPORT", + }), + progress: + task?.importPercent || + task?.status === "complete" || + task?.iaSummary?.detectionStatus === "complete" || + task?.status === "processing-pipeline" + ? 1 + : 0, + status: (() => { + if ( + task?.importPercent === 1 || + task?.status === "complete" || + task?.iaSummary?.detectionStatus === "complete" || + task?.status === "processing-pipeline" + ) { + return "complete"; + } else if (task?.importPercent) { + return "in_progress"; + } else { + return "not_started"; + } + })(), + }, + { + title: intl.formatMessage({ + id: "DETECTION", + }), + progress: task?.iaSummary?.detectionPercent || 0, + status: task?.iaSummary?.detectionStatus || "not_started", + }, + { + title: intl.formatMessage({ + id: "IDENTIFICATION", + }), + progress: task?.iaSummary?.identificationPercent || 0, + status: task?.iaSummary?.identificationStatus || "not_started", + }, + ].map(({ title, progress, status }) => ( + + ))} +
+
+ +
+
+ +
+ +
+ } + title={intl.formatMessage( + { + id: "PHOTOS_UPLOADED_TITLE", + defaultMessage: "Images uploaded: {count}", + }, + { count: task?.iaSummary?.numberMediaAssets || 0 }, + )} + data={[ + { + label: intl.formatMessage({ + id: "HAS_ACM_ID", + defaultMessage: "Has acmID", + }), + value: task?.iaSummary?.numberMediaAssetACMIds || 0, + }, + { + label: intl.formatMessage({ + id: "TOTAL_ANNOTATIONS", + defaultMessage: "Total Annotations", + }), + value: task?.iaSummary?.numberAnnotations || 0, + }, + { + label: intl.formatMessage({ + id: "VALID_FOR_IMAGE_ANALYSIS", + defaultMessage: "Valid for Image Analysis", + }), + value: task?.iaSummary?.numberMediaAssetValidIA || 0, + }, + { + label: intl.formatMessage({ + id: "TOTAL_MARKED_INDIVIDUALS", + defaultMessage: "Total Marked Individuals", + }), + value: task?.numberMarkedIndividuals || 0, + }, + ]} + /> + } + title={intl.formatMessage( + { + id: "SPREADSHEET_UPLOADED_TITLE", + defaultMessage: "Spreadsheet Uploaded: {fileName}", + }, + { fileName: task?.sourceName || "N/A" }, + )} + data={[ + { + label: intl.formatMessage({ + id: "FILE_UPLOADED_SUCCESSFULLY", + defaultMessage: "File Uploaded Successfully", + }), + }, + ]} + /> +
+ +
+ +
+ +
+ + + +
+ +
+
+

+ +

+
+ +
+ + + +
+ Loading location picker...
}> + + store.handleStrictChange(vals, labels, extra) + } + /> + + + +
+ + + + { + setShowError(false); + axios + .get( + `/appadmin/resendBulkImportID.jsp?importIdTask=${taskId}${store.locationIDString}`, + ) + .then((response) => { + if (response.status === 200) { + alert( + intl.formatMessage({ + id: "BULK_IMPORT_RE_ID_SUCCESS", + defaultMessage: + "Re-identification task started successfully.", + }), + ); + window.location.reload(); + } else { + throw new Error( + intl.formatMessage({ + id: "BULK_IMPORT_RE_ID_ERROR", + defaultMessage: + "Failed to start re-identification task.", + }), + ); + } + }) + .catch((error) => { + console.error( + "Error starting re-identification task:", + error, + ); + alert( + intl.formatMessage({ + id: "BULK_IMPORT_RE_ID_ERROR", + defaultMessage: "Failed to start re-identification task.", + }), + ); + }); + }} + backgroundColor={theme.wildMeColors.cyan700} + color={theme.defaultColors.white} + noArrow={true} + style={{ + width: "auto", + height: "40px", + fontSize: "1rem", + marginLeft: 0, + }} + > + + + {((!userRoles?.includes("admin") && + !userRoles?.includes("researcher")) || + !store.locationIDString || + task?.iaSummary?.detectionStatus !== "complete") && ( +

+ +

+ )} + +
+ +
+ +
+ + + + + +
+ setShowError(false)} centered> + + + + + + +

+ {error?.message || + intl.formatMessage({ + id: "BULK_IMPORT_TASK_ERROR_DEFAULT", + defaultMessage: "An error occurred while loading the task.", + })} +

+
+ + + + +
+
+ ); +}); + +export default BulkImportTask; From 4350cbd9abd21a1cb4f5358c3321baf81bad64e3 Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 21:02:38 -0700 Subject: [PATCH 03/13] Update ID calculation too --- src/main/java/org/ecocean/ia/Task.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/ecocean/ia/Task.java b/src/main/java/org/ecocean/ia/Task.java index d840c5a561..2037342687 100644 --- a/src/main/java/org/ecocean/ia/Task.java +++ b/src/main/java/org/ecocean/ia/Task.java @@ -502,6 +502,12 @@ public String getStatus(Shepherd myShepherd) { status = "completed"; } else if (logs.toString().indexOf("score") > -1) { status = "completed"; + } else if (islObj.optJSONObject("status") != null && + islObj.optJSONObject("status").optBoolean( + "emptyTargetAnnotations", false)) { + // No target annotations to match against is a terminal state, not a failure. + // Treating it as completed lets import progress reach 100%. + status = "completed"; } else if (islObj.toString().indexOf("HTTP error code") > -1) { status = "error"; } else if (!islObj.optString("queueStatus").equals("")) { From 70b9af496acb2afa673b6d7c5f5aba447ab20f78 Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 21:18:03 -0700 Subject: [PATCH 04/13] Undo erroneous code review change --- src/main/java/org/ecocean/ia/Task.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ecocean/ia/Task.java b/src/main/java/org/ecocean/ia/Task.java index 2037342687..6eb77dd9c7 100644 --- a/src/main/java/org/ecocean/ia/Task.java +++ b/src/main/java/org/ecocean/ia/Task.java @@ -503,7 +503,8 @@ public String getStatus(Shepherd myShepherd) { } else if (logs.toString().indexOf("score") > -1) { status = "completed"; } else if (islObj.optJSONObject("status") != null && - islObj.optJSONObject("status").optBoolean( + islObj.optJSONObject("status").optJSONObject("error") != null && + islObj.optJSONObject("status").optJSONObject("error").optBoolean( "emptyTargetAnnotations", false)) { // No target annotations to match against is a terminal state, not a failure. // Treating it as completed lets import progress reach 100%. From b4fb91236a0da96951766348fbcff6d3259100c7 Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 21:49:09 -0700 Subject: [PATCH 05/13] Deeper inspection of done calculation --- src/main/java/org/ecocean/ia/Task.java | 2 ++ .../java/org/ecocean/servlet/importer/ImportTask.java | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ecocean/ia/Task.java b/src/main/java/org/ecocean/ia/Task.java index 6eb77dd9c7..4dcf958fc2 100644 --- a/src/main/java/org/ecocean/ia/Task.java +++ b/src/main/java/org/ecocean/ia/Task.java @@ -501,6 +501,8 @@ public String getStatus(Shepherd myShepherd) { (islObj.optJSONObject("status").optJSONObject("needReview") != null)) { status = "completed"; } else if (logs.toString().indexOf("score") > -1) { + // Note: this checks ALL logs, not just the latest. A task that failed later + // (e.g., emptyTargetAnnotations) but had earlier scores will match here. status = "completed"; } else if (islObj.optJSONObject("status") != null && islObj.optJSONObject("status").optJSONObject("error") != null && diff --git a/src/main/java/org/ecocean/servlet/importer/ImportTask.java b/src/main/java/org/ecocean/servlet/importer/ImportTask.java index 032afb0436..998fb518a7 100644 --- a/src/main/java/org/ecocean/servlet/importer/ImportTask.java +++ b/src/main/java/org/ecocean/servlet/importer/ImportTask.java @@ -389,7 +389,11 @@ public JSONObject statsAnnotations(Shepherd myShepherd) { sa.put(ann.getId(), 0); continue; } - sa.put(ann.getId(), Util.collectionSize(atm.get(ann))); + int taskCount = Util.collectionSize(atm.get(ann)); + sa.put(ann.getId(), taskCount); + if (taskCount == 0) { + System.out.println("[statsAnnotations DEBUG] Non-trivial annotation " + ann.getId() + " has 0 tasks"); + } boolean latestTask = true; // only for first (most recent) task for (Task atask : atm.get(ann)) { String status = atask.getStatus(myShepherd); @@ -438,6 +442,9 @@ public JSONObject statsAnnotations(Shepherd myShepherd) { } sa.put("numTasks", numTasks); sa.put("numLatestTasks", numLatestTasks); + System.out.println("[statsAnnotations DEBUG] numTasks=" + numTasks + + " numLatestTasks=" + numLatestTasks + + " numLatestTask_completed=" + sa.optInt("numLatestTask_completed", 0)); // now we do the work to create encounterTaskInfo JSONObject encData = new JSONObject(); From e01f7186a76c8dfb88a2a755be322b826c71777d Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 22:02:07 -0700 Subject: [PATCH 06/13] More count checks for thoroughness --- .../java/org/ecocean/servlet/importer/ImportTask.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/ecocean/servlet/importer/ImportTask.java b/src/main/java/org/ecocean/servlet/importer/ImportTask.java index 998fb518a7..4b5826d400 100644 --- a/src/main/java/org/ecocean/servlet/importer/ImportTask.java +++ b/src/main/java/org/ecocean/servlet/importer/ImportTask.java @@ -442,9 +442,16 @@ public JSONObject statsAnnotations(Shepherd myShepherd) { } sa.put("numTasks", numTasks); sa.put("numLatestTasks", numLatestTasks); + // Log all numLatestTask_* counts to see what statuses are being returned + StringBuilder statusCounts = new StringBuilder(); + for (String key : sa.keySet()) { + if (key.startsWith("numLatestTask_")) { + statusCounts.append(key.replace("numLatestTask_", "")).append("=") + .append(sa.optInt(key)).append(" "); + } + } System.out.println("[statsAnnotations DEBUG] numTasks=" + numTasks + - " numLatestTasks=" + numLatestTasks + - " numLatestTask_completed=" + sa.optInt("numLatestTask_completed", 0)); + " numLatestTasks=" + numLatestTasks + " statusCounts: " + statusCounts.toString()); // now we do the work to create encounterTaskInfo JSONObject encData = new JSONObject(); From 43eee76fabab4337ab414e7ff0e220c4560934b2 Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 22:37:06 -0700 Subject: [PATCH 07/13] Count null ISL entries as complete --- src/main/java/org/ecocean/ia/Task.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/ecocean/ia/Task.java b/src/main/java/org/ecocean/ia/Task.java index 4dcf958fc2..3984a322a3 100644 --- a/src/main/java/org/ecocean/ia/Task.java +++ b/src/main/java/org/ecocean/ia/Task.java @@ -490,7 +490,12 @@ public String getStatus(Shepherd myShepherd) { String status = "waiting to queue"; ArrayList logs = IdentityServiceLog.loadByTaskID(getId(), "IBEISIA", myShepherd); - if (logs != null && logs.size() > 0) { + if (logs == null || logs.size() == 0) { + // No ISL entries means task was never processed or logs were lost. + // Treat as completed to unblock import progress tracking. + return "completed"; + } + if (logs.size() > 0) { Collections.reverse(logs); // so it has newest first like mostRecent above IdentityServiceLog l = logs.get(0); JSONObject islObj = l.toJSONObject(); From 74e9f3494261e44eaccc3c413eccc858f90005fe Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 22:55:48 -0700 Subject: [PATCH 08/13] Revert "Count null ISL entries as complete" This reverts commit 88756d1651b512c1ccd63d6ae9e35a168522ee15. --- src/main/java/org/ecocean/ia/Task.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/java/org/ecocean/ia/Task.java b/src/main/java/org/ecocean/ia/Task.java index 3984a322a3..4dcf958fc2 100644 --- a/src/main/java/org/ecocean/ia/Task.java +++ b/src/main/java/org/ecocean/ia/Task.java @@ -490,12 +490,7 @@ public String getStatus(Shepherd myShepherd) { String status = "waiting to queue"; ArrayList logs = IdentityServiceLog.loadByTaskID(getId(), "IBEISIA", myShepherd); - if (logs == null || logs.size() == 0) { - // No ISL entries means task was never processed or logs were lost. - // Treat as completed to unblock import progress tracking. - return "completed"; - } - if (logs.size() > 0) { + if (logs != null && logs.size() > 0) { Collections.reverse(logs); // so it has newest first like mostRecent above IdentityServiceLog l = logs.get(0); JSONObject islObj = l.toJSONObject(); From 0c8b73ee3aa016029a4d59619129b4a5e80f754c Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 22:55:53 -0700 Subject: [PATCH 09/13] Revert "More count checks for thoroughness" This reverts commit af1939ceea49d3d3b009a56a60bbf8b92098f0f4. --- .../java/org/ecocean/servlet/importer/ImportTask.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/ecocean/servlet/importer/ImportTask.java b/src/main/java/org/ecocean/servlet/importer/ImportTask.java index 4b5826d400..998fb518a7 100644 --- a/src/main/java/org/ecocean/servlet/importer/ImportTask.java +++ b/src/main/java/org/ecocean/servlet/importer/ImportTask.java @@ -442,16 +442,9 @@ public JSONObject statsAnnotations(Shepherd myShepherd) { } sa.put("numTasks", numTasks); sa.put("numLatestTasks", numLatestTasks); - // Log all numLatestTask_* counts to see what statuses are being returned - StringBuilder statusCounts = new StringBuilder(); - for (String key : sa.keySet()) { - if (key.startsWith("numLatestTask_")) { - statusCounts.append(key.replace("numLatestTask_", "")).append("=") - .append(sa.optInt(key)).append(" "); - } - } System.out.println("[statsAnnotations DEBUG] numTasks=" + numTasks + - " numLatestTasks=" + numLatestTasks + " statusCounts: " + statusCounts.toString()); + " numLatestTasks=" + numLatestTasks + + " numLatestTask_completed=" + sa.optInt("numLatestTask_completed", 0)); // now we do the work to create encounterTaskInfo JSONObject encData = new JSONObject(); From dee9c44014a6414ed8687e2b79a732dfdea70902 Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 22:55:57 -0700 Subject: [PATCH 10/13] Revert "Deeper inspection of done calculation" This reverts commit 75d536a1aacec274f22f40ede432b65f9f4f290f. --- src/main/java/org/ecocean/ia/Task.java | 2 -- .../java/org/ecocean/servlet/importer/ImportTask.java | 9 +-------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/org/ecocean/ia/Task.java b/src/main/java/org/ecocean/ia/Task.java index 4dcf958fc2..6eb77dd9c7 100644 --- a/src/main/java/org/ecocean/ia/Task.java +++ b/src/main/java/org/ecocean/ia/Task.java @@ -501,8 +501,6 @@ public String getStatus(Shepherd myShepherd) { (islObj.optJSONObject("status").optJSONObject("needReview") != null)) { status = "completed"; } else if (logs.toString().indexOf("score") > -1) { - // Note: this checks ALL logs, not just the latest. A task that failed later - // (e.g., emptyTargetAnnotations) but had earlier scores will match here. status = "completed"; } else if (islObj.optJSONObject("status") != null && islObj.optJSONObject("status").optJSONObject("error") != null && diff --git a/src/main/java/org/ecocean/servlet/importer/ImportTask.java b/src/main/java/org/ecocean/servlet/importer/ImportTask.java index 998fb518a7..032afb0436 100644 --- a/src/main/java/org/ecocean/servlet/importer/ImportTask.java +++ b/src/main/java/org/ecocean/servlet/importer/ImportTask.java @@ -389,11 +389,7 @@ public JSONObject statsAnnotations(Shepherd myShepherd) { sa.put(ann.getId(), 0); continue; } - int taskCount = Util.collectionSize(atm.get(ann)); - sa.put(ann.getId(), taskCount); - if (taskCount == 0) { - System.out.println("[statsAnnotations DEBUG] Non-trivial annotation " + ann.getId() + " has 0 tasks"); - } + sa.put(ann.getId(), Util.collectionSize(atm.get(ann))); boolean latestTask = true; // only for first (most recent) task for (Task atask : atm.get(ann)) { String status = atask.getStatus(myShepherd); @@ -442,9 +438,6 @@ public JSONObject statsAnnotations(Shepherd myShepherd) { } sa.put("numTasks", numTasks); sa.put("numLatestTasks", numLatestTasks); - System.out.println("[statsAnnotations DEBUG] numTasks=" + numTasks + - " numLatestTasks=" + numLatestTasks + - " numLatestTask_completed=" + sa.optInt("numLatestTask_completed", 0)); // now we do the work to create encounterTaskInfo JSONObject encData = new JSONObject(); From c19cf249250881f95334d7ebbbe34983aae50852 Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 22:56:04 -0700 Subject: [PATCH 11/13] Revert "refine empty target annotations check" This reverts commit a9ad1cd83708785478a5b56314afc8a5e929ea5a. From fc73d422a23b9cebb105642e3ad6b355268facad Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 22:56:08 -0700 Subject: [PATCH 12/13] Revert "Undo erroneous code review change" This reverts commit e8a09191f4fe6be1dfe1b7799e960687cd13090e. --- src/main/java/org/ecocean/ia/Task.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/ecocean/ia/Task.java b/src/main/java/org/ecocean/ia/Task.java index 6eb77dd9c7..2037342687 100644 --- a/src/main/java/org/ecocean/ia/Task.java +++ b/src/main/java/org/ecocean/ia/Task.java @@ -503,8 +503,7 @@ public String getStatus(Shepherd myShepherd) { } else if (logs.toString().indexOf("score") > -1) { status = "completed"; } else if (islObj.optJSONObject("status") != null && - islObj.optJSONObject("status").optJSONObject("error") != null && - islObj.optJSONObject("status").optJSONObject("error").optBoolean( + islObj.optJSONObject("status").optBoolean( "emptyTargetAnnotations", false)) { // No target annotations to match against is a terminal state, not a failure. // Treating it as completed lets import progress reach 100%. From ac28fe6dde7333baace0bcee8a109afeee6ca142 Mon Sep 17 00:00:00 2001 From: Wild Me Date: Sun, 12 Apr 2026 22:56:12 -0700 Subject: [PATCH 13/13] Revert "Update ID calculation too" This reverts commit 04083ccd585424fb36b28e438f4e42ad7c825932. --- src/main/java/org/ecocean/ia/Task.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/org/ecocean/ia/Task.java b/src/main/java/org/ecocean/ia/Task.java index 2037342687..d840c5a561 100644 --- a/src/main/java/org/ecocean/ia/Task.java +++ b/src/main/java/org/ecocean/ia/Task.java @@ -502,12 +502,6 @@ public String getStatus(Shepherd myShepherd) { status = "completed"; } else if (logs.toString().indexOf("score") > -1) { status = "completed"; - } else if (islObj.optJSONObject("status") != null && - islObj.optJSONObject("status").optBoolean( - "emptyTargetAnnotations", false)) { - // No target annotations to match against is a terminal state, not a failure. - // Treating it as completed lets import progress reach 100%. - status = "completed"; } else if (islObj.toString().indexOf("HTTP error code") > -1) { status = "error"; } else if (!islObj.optString("queueStatus").equals("")) {