diff --git a/web/curate/speciesList/validateSpeciesList-body.jsp b/web/curate/speciesList/validateSpeciesList-body.jsp
new file mode 100644
index 00000000..f5e0f4da
--- /dev/null
+++ b/web/curate/speciesList/validateSpeciesList-body.jsp
@@ -0,0 +1,178 @@
+<%@ page language="java" %>
+<%@ page errorPage = "/error.jsp" %>
+<%@ page import="java.util.*" %>
+<%@ page import="org.calacademy.antweb.*" %>
+<%@ page import="org.calacademy.antweb.util.*" %>
+<%@ page import="org.calacademy.antweb.curate.speciesList.*" %>
+
+<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
+<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
+<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
+
+
+
+
+<%
+ String domainApp = AntwebProps.getDomainApp();
+ String message = (String) request.getAttribute("message");
+ ValidateSpeciesReport report = (ValidateSpeciesReport) request.getAttribute("validationReport");
+%>
+
+
+
+
Validate Species List
+
+ Read-Only Validator. Safe to use for reconciliation workflows. No database modifications will occur.
+
+
+ <% if (message != null && !message.isEmpty()) { %>
+
+ <%= message %>
+
+ <% } %>
+
+
+
+
Validation Instructions
+
Upload a tab-delimited .txt or .tsv file. Maximum file size: 5MB (approx. 50,000 rows). The first row must contain column headers.
+
+
Two formats are supported:
+
+
+
Format 1 (Separate Columns): Required headers:
genus,
species. Optional:
subfamily,
subspecies.
+
+ subfamily genus species subspecies
+ Myrmicinae Acromyrmex balzani multituber
+ Dorylinae Aenictus clavatus atripennis
+
+
+
Format 2 (Combined Taxon): Required header:
taxon_name (case-insensitive).
+
+ subfamily taxon_name
+ Myrmicinae Acromyrmex balzani multituber
+ Dorylinae Aenictus clavatus atripennis
+
+
+
+
+
Download Template File
+
+
+
+
+
+
Upload File for Validation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% if (report != null) { %>
+
+
Validation Report Summary
+
+ | Total Rows Processed | <%= report.getTotalInputRows() %> |
+ | Exact Matches | <%= report.getExactMatchCount() %> |
+ | Not Found / Ambiguous | <%= report.getProblemCount() %> |
+ | Format Errors | <%= report.getFormatErrorCount() %> |
+ <% if (validateSpeciesListForm.isShowUnmatched()) { %>
+ | Unmatched AntWeb Taxa | <%= report.getUnmatchedCount() %> |
+ <% } %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <% if (report.getProblemCount() > 0 || report.getFormatErrorCount() > 0) { %>
+
Problems & Format Errors Log
+
+
+ | Row |
+ Input Raw |
+ Normalized Taxon Name |
+ Status |
+ Message |
+ Suggestion |
+
+
+ <%
+ List issues = new ArrayList<>();
+ issues.addAll(report.getFormatErrors());
+ issues.addAll(report.getProblems());
+ issues.sort((a, b) -> Integer.compare(a.getRowNum(), b.getRowNum()));
+
+ for (ValidateSpeciesResultItem item : issues) {
+ String bg = item.getStatus() == ValidateSpeciesResultItem.Status.FORMAT_ERROR ? "#ffe6e6" : "#fff3cd";
+ %>
+
+ | <%= item.getRowNum() %> |
+ <%= item.getInputRaw() != null ? item.getInputRaw().replace("<", "<").replace(">", ">") : "" %> |
+ <%= item.getNormalizedName() != null ? item.getNormalizedName() : "" %> |
+ <%= item.getStatus().name() %> |
+ <%= item.getMessage() %> |
+ <% if (item.getSuggestion() != null && !item.getSuggestion().isEmpty()) { %>
+ <%= item.getSuggestion() %>
+ <% if (item.getStatus() == ValidateSpeciesResultItem.Status.NOT_FOUND) { %>
+ ↑ Try replacing with this valid name.
+ <% } %>
+ |
+ <% } else { %>
+ |
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+ <% if (report.getFormatErrorCount() == 0 && report.getProblemCount() == 0) { %>
+
+ ✓ All rows matched valid extant taxa exactly.
+
+ <% } %>
+
+ <% if (validateSpeciesListForm.isShowUnmatched() && !report.getUnmatchedValidTaxa().isEmpty()) { %>
+
Unmatched Valid AntWeb Taxa (Preview)
+
+ <%
+ int maxPreview = Math.min(100, report.getUnmatchedValidTaxa().size());
+ for (int i=0; i");
+ }
+ if (report.getUnmatchedValidTaxa().size() > 100) {
+ out.println("
... and " + (report.getUnmatchedValidTaxa().size() - 100) + " more (Download TSV for full list)");
+ }
+ %>
+
+ <% } %>
+
+
+ <% } %>
+
+
diff --git a/web/curate/speciesList/validateSpeciesList.jsp b/web/curate/speciesList/validateSpeciesList.jsp
new file mode 100644
index 00000000..7ca6035a
--- /dev/null
+++ b/web/curate/speciesList/validateSpeciesList.jsp
@@ -0,0 +1,14 @@
+<%@ page language="java" %>
+<%@ page errorPage = "error.jsp" %>
+<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
+<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
+<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
+<%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %>
+
+<%@include file="/curate/curatorCheck.jsp" %>
+<%@include file="/common/antweb_admin-defs.jsp" %>
+
+
+
+
+
diff --git a/web/data/validateSpeciesList_template.txt b/web/data/validateSpeciesList_template.txt
new file mode 100644
index 00000000..c95bc851
--- /dev/null
+++ b/web/data/validateSpeciesList_template.txt
@@ -0,0 +1,4 @@
+subfamily genus species subspecies
+Myrmicinae Acromyrmex balzani multituber
+Dorylinae Aenictus clavatus atripennis
+Myrmicinae Atta cephalotes lutea