From c9b3f5db41c577c8ee0a3d1442c41d7131cb5c22 Mon Sep 17 00:00:00 2001
From: prsmith777
Date: Wed, 21 Oct 2026 20:10:55 -0600
Subject: [PATCH] Release 0.1.57 with plate review defaults and manual
State/Country
---
.github/ISSUE_TEMPLATE/bug_report.yml | 2 +-
CHANGELOG.md | 8 ++
app/actions.js | 35 +++++++++
app/database/page.jsx | 1 +
app/live_feed/page.jsx | 1 +
app/settings/SettingsForm.jsx | 10 +++
components/PlateStateCountryField.jsx | 92 +++++++++++++++++++++++
components/PlateTable.jsx | 33 +++++++--
components/PlateTableWrapper.jsx | 9 +++
components/plateDbTable.jsx | 11 ++-
components/ui/dropdown-menu.jsx | 4 +-
docs/COMMUNITY_PRODUCT_ROADMAP.md | 13 ++++
docs/INSTALL.md | 2 +-
docs/UPDATES.md | 6 +-
lib/db.js | 5 ++
lib/help-manual.mjs | 6 +-
lib/plate-correction-defaults.mjs | 10 +++
lib/plate-state-country.mjs | 45 ++++++++++++
lib/release-info.mjs | 7 +-
lib/settings-help-coverage.mjs | 2 +-
lib/settings.js | 2 +
migrations.sql | 9 +++
package.json | 2 +-
test/help-manual.test.mjs | 2 +-
test/plate-state-country.test.mjs | 101 ++++++++++++++++++++++++++
test/release-info.test.mjs | 9 ++-
26 files changed, 401 insertions(+), 26 deletions(-)
create mode 100644 components/PlateStateCountryField.jsx
create mode 100644 lib/plate-correction-defaults.mjs
create mode 100644 lib/plate-state-country.mjs
create mode 100644 test/plate-state-country.test.mjs
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index ee1ea25..510f23d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -16,7 +16,7 @@ body:
attributes:
label: ALPR Community version
description: Find this under Settings → Release. Include the source revision there when reporting an installation or update problem.
- placeholder: v0.1.56
+ placeholder: v0.1.57
validations:
required: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c058d64..0bb9af5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 0.1.57 — 2026-10-21
+
+- Enable batch correction and recurring OCR-misread aliases by default for administrators; retain the batch preview and unchanged-plate safeguards. Operators retain single-read correction.
+- Keep long tag menus within the available screen space and allow scrolling to every tag in tables and the image detail modal.
+- Add an optional State/Country column to Recognition Feed and Plate Database, off by default under Settings → General. Operators and administrators enter values manually or select a previous entry.
+- Share manually entered State/Country values across reads of the same effective plate, validate and audit changes, and provide indexed, case-insensitive saved-value suggestions. Existing records start without a value; OCR does not populate it.
+- Update User Guide 3.25, release notes, Settings help coverage and current installation/update examples.
+
## 0.1.56 — 2026-10-21
- Export the original Docker ALPR database from Windows Setup before creating a
diff --git a/app/actions.js b/app/actions.js
index 2218bdb..d1fe2fc 100644
--- a/app/actions.js
+++ b/app/actions.js
@@ -42,6 +42,7 @@ import {
updateTagName,
addUnseenPlate,
} from "@/lib/db";
+import { savePlateStateCountry, listPlateStateCountries } from "@/lib/plate-state-country.mjs";
import { normalizePlateMatchingSettings } from "@/lib/plate-matching.mjs";
import { normalizeVehicleImageSource, vehicleImageSourceData } from "@/lib/vehicle-image-source.mjs";
import { getPlateReviewRepository } from "@/lib/plate-review-runtime.mjs";
@@ -2738,6 +2739,7 @@ export async function getPlateViewSettings() {
const config = await getConfig();
return {
plateMatching: normalizePlateMatchingSettings(config.plateMatching),
+ showStateCountry: config.general?.showStateCountry === true,
blueiris: {
host: config.blueiris?.host || "",
},
@@ -2793,6 +2795,13 @@ export async function updateSettings(formData) {
};
}
+ if (updateIfExists("showStateCountry")) {
+ newConfig.general = {
+ ...newConfig.general,
+ showStateCountry: formData.get("showStateCountry") === "true",
+ };
+ }
+
if (
updateIfExists("dbHost") ||
updateIfExists("dbName") ||
@@ -2967,6 +2976,9 @@ export async function updateSettings(formData) {
}
revalidatePath("/settings");
+ revalidatePath("/settings/general");
+ revalidatePath("/live_feed");
+ revalidatePath("/database");
revalidatePath("/settings/integrations");
revalidatePath("/settings/integrations/pushover");
revalidatePath("/settings/integrations/email");
@@ -4906,3 +4918,26 @@ export async function createVehicleReidV2ProfileCandidateSnapshot() {
);
}
}
+
+export async function updatePlateStateCountry(plateNumber, stateCountry) {
+ const principal = await requirePermission("plate.review");
+ try {
+ const data = await savePlateStateCountry(getPool, { plateNumber, stateCountry, actor: principal });
+ revalidatePath("/live_feed");
+ revalidatePath("/database");
+ return { success: true, data };
+ } catch (error) {
+ console.error("Error saving State/Country:", error);
+ return { success: false, error: error.code === "INVALID_STATE_COUNTRY" || error.code === "PLATE_NOT_FOUND" || error.code === "INVALID_PLATE" ? error.message : "Unable to save State/Country." };
+ }
+}
+
+export async function getPlateStateCountries() {
+ await requirePermission("plate.read");
+ try {
+ return { success: true, data: await listPlateStateCountries(getPool) };
+ } catch (error) {
+ console.error("Error loading State/Country entries:", error);
+ return { success: false, data: [], error: "Unable to load previous entries." };
+ }
+}
diff --git a/app/database/page.jsx b/app/database/page.jsx
index 58f8014..b20d90c 100644
--- a/app/database/page.jsx
+++ b/app/database/page.jsx
@@ -25,6 +25,7 @@ export default async function Database() {
diff --git a/app/live_feed/page.jsx b/app/live_feed/page.jsx
index f839428..053843e 100644
--- a/app/live_feed/page.jsx
+++ b/app/live_feed/page.jsx
@@ -168,6 +168,7 @@ export default async function LivePlates(props) {
timeFormat={timeFormat}
biHost={config?.blueiris?.host}
matchingSettings={config?.plateMatching}
+ showStateCountry={config?.showStateCountry}
dashboardTimeFrame={dashboardTimeFrame}
dashboardMetric={dashboardMetric}
defaultMatchMode={defaultMatchMode}
diff --git a/app/settings/SettingsForm.jsx b/app/settings/SettingsForm.jsx
index 3322d74..ae17b01 100644
--- a/app/settings/SettingsForm.jsx
+++ b/app/settings/SettingsForm.jsx
@@ -129,6 +129,7 @@ export default function SettingsForm({
newFormData.append("retention", formData.get("retention"));
newFormData.append("ignoreNonPlate", formData.get("ignoreNonPlate"));
newFormData.append("timeFormat", Number(formData.get("timeFormat")));
+ newFormData.append("showStateCountry", formData.get("showStateCountry") === "on");
break;
case "database":
["dbHost", "dbName", "dbUser", "dbPassword"].forEach(appendIfPresent);
@@ -335,6 +336,15 @@ export default function SettingsForm({
Used to estimate retention-eligible reads. Community does not automatically delete plate reads or source images.
+
+
+
+
+
+
+ Display a State/Country column in the recognition feed and plate database. Enter values manually or choose a previous entry. Off by default.
+
+
@@ -2539,6 +2552,8 @@ export default function PlateTable({
{canReview && {
setCorrection(correctionDraft({
+ canBatchReview,
+ canManageAliases,
id: plate.id,
plateNumber: plate.plate_number,
observedPlate: plate.observed_plate || plate.plate_number,
@@ -3090,6 +3105,8 @@ export default function PlateTable({
title="Correct detected plate"
onClick={() => {
setCorrection(correctionDraft({
+ canBatchReview,
+ canManageAliases,
id: selectedImage.id,
plateNumber: selectedImage.plateNumber,
observedPlate: selectedImage.observedPlate || selectedImage.plateNumber,
@@ -3641,7 +3658,7 @@ export default function PlateTable({
onClick={() => handleCorrectSubmit()}
disabled={
!correction?.newPlateNumber ||
- (!correctionChangesPlate && !correction?.rememberAlias) ||
+ (!correctionChangesPlate && !aliasOnlyCorrection) ||
(correctionChangesPlate && correction.correctAll && !correctionPreview)
}
>
diff --git a/components/PlateTableWrapper.jsx b/components/PlateTableWrapper.jsx
index 0e40c0b..5326733 100644
--- a/components/PlateTableWrapper.jsx
+++ b/components/PlateTableWrapper.jsx
@@ -60,6 +60,7 @@ export default function PlateTableWrapper({
timeFormat,
biHost,
matchingSettings,
+ showStateCountry = false,
dashboardTimeFrame,
dashboardMetric,
defaultMatchMode = "balanced",
@@ -632,6 +633,14 @@ export default function PlateTableWrapper({
direction: displayedParams.get("sortDirection") || "desc",
}}
matchingSettings={matchingSettings}
+ showStateCountry={showStateCountry}
+ onStateCountrySaved={({ plate_number, state_country }) => {
+ setLiveData((current) => {
+ const next = current.map((plate) => plate.plate_number === plate_number ? { ...plate, state_country } : plate);
+ liveDataRef.current = next;
+ return next;
+ });
+ }}
onSort={handleSort}
onUpdateFilters={handleUpdateFilters}
onAddTag={handleAddTag}
diff --git a/components/plateDbTable.jsx b/components/plateDbTable.jsx
index 85c887b..9423300 100644
--- a/components/plateDbTable.jsx
+++ b/components/plateDbTable.jsx
@@ -99,6 +99,7 @@ import {
writeTablePageSizePreference,
} from "@/lib/table-page-size-preference.mjs";
import ResultsPagination from "@/components/ResultsPagination";
+import PlateStateCountryField from "@/components/PlateStateCountryField";
const formatDaysAgo = (days) => {
if (days === 0) return "Today";
@@ -128,7 +129,7 @@ const formatTimestamp = (timestamp, timeFormat) => {
});
};
-export default function PlateTable({ matchingSettings }) {
+export default function PlateTable({ matchingSettings, showStateCountry = false }) {
const { can } = useAccess();
const canReview = can("plate.review");
const canDelete = can("plate.delete");
@@ -492,6 +493,7 @@ export default function PlateTable({ matchingSettings }) {
{getSortIcon("occurrence_count")}
+ {showStateCountry && State/Country}