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}