From 994920bda29ec7ce77ccece1029da0da6b1c2e3f Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Sun, 12 Apr 2026 22:45:27 +0300 Subject: [PATCH] Build [v1.7.0] - GitGuard - Infrastructure Hardening --- AI_RULES.md | 11 +++- PROJECT_ARCHITECTURE.md | 7 +++ README.md | 2 +- USER_GUIDE.md | 15 +++--- VERSION.json | 13 ++--- backend/ai_vision.py | 65 +++++++++++++++--------- backend/routers/items.py | 3 +- dev_docs/SESSION_STATE.md | 23 ++++++--- frontend/app/inventory/page.tsx | 75 ++++++++++++++++++++++++++-- frontend/app/page.tsx | 45 ++++++++++++++--- frontend/components/AIOnboarding.tsx | 36 +++++++++++-- frontend/components/Scanner.tsx | 21 ++++++-- frontend/lib/api.ts | 4 +- scripts/save_version.py | 18 +++++-- 14 files changed, 262 insertions(+), 76 deletions(-) diff --git a/AI_RULES.md b/AI_RULES.md index 66f8f573..f5e18d9e 100644 --- a/AI_RULES.md +++ b/AI_RULES.md @@ -14,7 +14,7 @@ This is the **Single Source of Truth** for ALL AI agents. Refer to [PROJECT_ARCH ## 2. ENGINEERING & OPERATIONAL LAWS - **ENGLISH ONLY**: Interfaces, code, variables, and docs MUST be in English. Translate any Romanian text found in code immediately. (User conversation: Romanian/English). -- **GIT PROTOCOL**: Use binary path in `.git_path` for all operations. Never push or use `--force` unless asked. Branching: `master` (stable), `dev` (active), `vX` (archive). +- **GIT PROTOCOL**: Use the direct binary path in `.git_path` (`/Library/Developer/CommandLineTools/usr/bin/git`) for ALL Git operations. **DO NOT REMOVE OR CHANGE THIS PATH UNDER ANY CIRCUMSTANCES!** Never push or use `--force` unless explicitly asked. Branching: `master` (stable), `dev` (active), `vX` (archive). - **VERSIONING**: Update `VERSION.json` on every commit. Use `scripts/save_version.py` for automated releases. - **DEPENDENCIES**: Update `backend/requirements.txt` with version constraints for every new pip package. - **SSOT INTEGRITY**: Every feature change MUST update: `README.md`, `USER_GUIDE.md`, `PROJECT_ARCHITECTURE.md`, and `export_prod.sh`. @@ -41,7 +41,14 @@ This is the **Single Source of Truth** for ALL AI agents. Refer to [PROJECT_ARCH - **Native Alerts**: Use `window.confirm` for all destructive UI actions and Logout. ## 5. AI COMMAND SHORTCUTS -- **`save-version`**: 1. Increment `VERSION.json`. 2. Git add/commit (`Build [vX.Y.Z]`). 3. Create branch `vX.Y.Z`. 4. Run `./export_prod.sh`. (Always use `python3 scripts/save_version.py`). +- **`save-version`**: + 0. **MANDATORY**: Verify and update ALL documentation (`.md` files: README, USER_GUIDE, ARCHITECTURE, etc.) with explanations of all current changes. + 1. Increment `VERSION.json`. + 2. Git add/commit (`Build [vX.Y.Z]`). + 3. Create branch `vX.Y.Z` (Snapshot). + 4. Automatic Sync: Merge changes into `master` branch to keep it up-to-date. + 5. Run `./export_prod.sh`. + (Always use `python3 scripts/save_version.py`). --- diff --git a/PROJECT_ARCHITECTURE.md b/PROJECT_ARCHITECTURE.md index c38a1ef7..21629153 100644 --- a/PROJECT_ARCHITECTURE.md +++ b/PROJECT_ARCHITECTURE.md @@ -37,6 +37,7 @@ A unified system to maintain an inventory of "items" and their quantities, inclu ### 4.1 AI Usage Policy - **Routine Operations (Check-in/Out):** Executes entirely on the local device unconditionally using `html5-qrcode` ($0 cost). No AI is allowed here. - **New Item Onboarding (AI Label OCR):** Uses cloud AI (`gemini-2.0-flash`). The user takes a photo, AI extracts data based on strict templates. +- **AI Box Discovery Mode (v1.6.0):** Supports specialized `mode="box"` prompt that focuses exclusively on prominent container names/hand-written labels, ignoring technical spec noise. - **Validation Mask:** AI-extracted data is NEVER saved directly. It is presented in a validation UI for human confirmation. ### 4.2 Scanner Technical Specs @@ -48,6 +49,7 @@ A unified system to maintain an inventory of "items" and their quantities, inclu - Noise Filtering: Ignores `< 3` chars, decimals, and dates. - Scoring: Exact S/N (+500), Exact P/N (+200), Token match (+50), Category match (+20). - Threshold: Minimum **40 points** for auto-match without user intervention. +- **Targeted Field Scanning (v1.6.0):** UI allows "locking" the scanner focus to a specific input field (e.g., `box_label`). The OCR result is then redirected to state without performing regular item lookup. ### 4.3 Box Labeling & Printing System (v1.5.0) - **Local OCR Priority:** Before checking individual S/Ns, the matching engine searches for `box_label` tokens. If a box is identified: @@ -83,7 +85,12 @@ To ensure enterprise-grade protection, the following policies are enforced: ### 7.3 Data Privacy - **Information Scrubbing:** Backend logs are configured to intercept and mask sensitive auth tokens or internal secrets (e.g., `JWT_SECRET_KEY`) during debug output. - **Direct Bind LDAP:** Authentication uses direct user binding to the LDAP server, avoiding the need for a privileged service account with broad search permissions. +- **Cryptographic Credential Caching:** To support offline operations, the system caches a **PBKDF2-HMAC-SHA256 hash** of the user's Enterprise credentials upon successful online login. Plain text passwords are NEVER stored. ### 7.4 PWA Trust & Security - **HTTPS Enforcement:** The system requires TLS (Port 3003) for camera access and secure token transmission. - **Manifest Integrity:** A comprehensive `manifest.json` ensures the app is recognized as a trusted PWA on mobile platforms (iOS/Android). +7.5 Git Infrastructure Hardening (v1.7.0) +To ensure deployment stability on macOS environments with potentially broken developer tool links (`xcode-select` errors): +- **Direct Binary Mapping:** The system bypasses path resolution by using a hardcoded direct link to the Git binary in `.git_path` (`/Library/Developer/CommandLineTools/usr/bin/git`). +- **Persistence Mandate:** This path is protected by mandatory AI rules and must never be removed or modified to ensure `save-version` and automated deployment scripts remain functional. diff --git a/README.md b/README.md index c5cb092c..b82e435c 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Native Linux installation (Alma/Debian/Ubuntu) without Docker dependencies. To generate a clean production package and snapshot the current state: 1. Use the AI shortcut command: `save-version`. 2. Alternatively, run `./export_prod.sh` manually. -3. A `.zip` archive will be created (e.g., `aInventory-PROD-v1.3.6.zip`). +3. A `.zip` archive will be created (e.g., `aInventory-PROD-v1.7.0.zip`). 4. A backup branch `v.1.3.x` will be created automatically. --- diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 3aa35b39..34cbeed3 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -19,7 +19,7 @@ The application is a **Progressive Web App**, which means you don't need to down - **Default User:** On first installation, use `Admin` / `` (check your system administrator for the initial password). - **Change Password:** We recommend changing your password immediately from the Admin settings. -- **LDAP/Enterprise Login:** If your administrator has configured LDAP integration, you can log in with your company/domain account. The application will cache your password locally to allow offline access (e.g., in areas without signal like basements). +- **LDAP/Enterprise Login:** If your administrator has configured LDAP integration, you can log in with your company/domain account. The application will securely cache a **cryptographic hash** of your credentials (using PBKDF2) to allow offline access (e.g., in areas without signal like basements). **Note: Your actual password is NEVER stored in plain text on the local device.** - **JWT Tokens:** Your login session is secured with JWT bearer tokens that expire after 8 hours. You will be automatically logged out when your token expires. --- @@ -33,11 +33,12 @@ Scan an existing barcode to locate or update an item in your inventory. If no readable text is found, the scanner silently retries on the next cycle. -### Box & Container Scanning (NEW v1.5.0) -You can now scan a generic label on a box (e.g., "SFP Box 1") to identify all its contents at once. -- If the box contains **one type of item**, the application takes you directly to the stock adjustment screen. -- If the box contains **multiple items**, a list will appear for you to select which specific item you are withdrawing or adding. -- This feature works **locally and offline** - no AI costs for scanning boxes. +### Box & Container Scanning (NEW v1.6.0) +You can now manage containers more efficiently with two specialized methods: + +- **AI Box Discovery**: When adding a new container through **AI Discovery**, use the **"Box / Container"** toggle. Gemini will focus exclusively on the container's name, ignoring technical noise on labels. +- **Targeted Field Scanning**: In the **Edit Item** modal, tap the small **Camera icon** next to the "Box / Container Label" field. The scanner will capture the next physical label directly into the text field. +- **Automatic Matching**: In the main scanner, scanning a box identifies all its contents. Scanning a box and then an item will suggest linking them together if they aren't already matched. --- @@ -140,5 +141,5 @@ For detailed technical documentation, see the [Project Architecture](../PROJECT_ --- -**Version:** v1.5.0 +**Version:** v1.7.0 **Last Updated:** 2026-04-12 diff --git a/VERSION.json b/VERSION.json index dac7aef3..62d17b28 100644 --- a/VERSION.json +++ b/VERSION.json @@ -1,12 +1,5 @@ { - "version": "1.5.0", - "last_build": "2026-04-12-2130", - "commit": "HEAD", - "changelog": [ - "v1.5.0: Box Management System - Local OCR box scanning, Selection modals, and Dependency-free Label Printing (Barcode/QR)", - "v1.4.0: Final Audit Dashboard (Phase 6), LDAP UI Restoration, environment path fixes for macOS", - "v1.3.9: Maintenance and structural updates", - "v1.3.8: Remove .npx_cache/ and scratch/npm_cache/ from git tracking (3350 files purged from index)", - "v1.3.7: Security hardening — expanded .gitignore, removed ldap_config.json from tracking, added example files" - ] + "version": "1.7.0", + "last_build": "2026-04-12-2224", + "codename": "GitGuard" } \ No newline at end of file diff --git a/backend/ai_vision.py b/backend/ai_vision.py index b8e916c3..a2cc4629 100644 --- a/backend/ai_vision.py +++ b/backend/ai_vision.py @@ -7,36 +7,55 @@ base_dir = os.path.dirname(os.path.abspath(__file__)) dotenv_path = os.path.join(base_dir, ".env") load_dotenv(dotenv_path) -def extract_label_info(image_bytes: bytes): +def extract_label_info(image_bytes: bytes, mode: str = "item"): """ Orchestrates extraction across multiple AI providers. - Order: Gemini (Flash/Pro) -> Claude (Haiku/Sonnet) - """ - prompt = """ - Extract technical inventory information from this label image. - - CRITICAL INSTRUCTIONS: - 1. Look at the most prominent text (usually top 1-2 rows). This is the product NAME and MODEL. - 2. Extract the PART NUMBER (P/N, Model No, Type). If no explicit Part Number is found, synthesize one from the most unique identifier in the header (e.g. 'OM4-MMF-DX'). - 3. Separate the COLOR (e.g. Turquoise, Yellow, Black). - 4. Extract CATEGORY based on the item type (e.g. Patchcord, SFP, Connector). - 5. Extract technical SPECS (e.g. '2.0mm', '10G', '850nm'). - - Return ONLY a valid JSON object: - { - "name": "Full descriptive name from header", - "part_number": "Unique identifier for fast scanning", - "category": "Broad category", - "color": "Color if present", - "specs": "Brief tech specs list", - "barcode": "Barcode value if visible", - "quantity": 1 - } + Modes: 'item' (full technical extraction), 'box' (container discovery) """ + if mode == "box": + prompt = """ + Identify the CONTAINER or BOX name from this image. + Look for large, prominent, bold, or hand-written text that identifies a storage unit. + Ignore small technical details, quantities, or fine print. + + Return ONLY a valid JSON object: + { + "box_label": "The identified container name", + "name": "Same as box_label", + "category": "Storage", + "specs": "Brief description if useful", + "quantity": 1 + } + """ + else: + prompt = """ + Extract technical inventory information from this label image. + + CRITICAL INSTRUCTIONS: + 1. Look at the most prominent text (usually top 1-2 rows). This is the product NAME and MODEL. + 2. Extract the PART NUMBER (P/N, Model No, Type). If no explicit Part Number is found, synthesize one from the most unique identifier in the header (e.g. 'OM4-MMF-DX'). + 3. Separate the COLOR (e.g. Turquoise, Yellow, Black). + 4. Extract CATEGORY based on the item type (e.g. Patchcord, SFP, Connector). + 5. Extract technical SPECS (e.g. '2.0mm', '10G', '850nm'). + + Return ONLY a valid JSON object: + { + "name": "Full descriptive name from header", + "part_number": "Unique identifier for fast scanning", + "category": "Broad category", + "color": "Color if present", + "specs": "Brief tech specs list", + "barcode": "Barcode value if visible", + "quantity": 1 + } + """ # 1. Try Gemini result = gemini.extract(image_bytes, prompt) if result: + # Maintenance: Ensure fields are mapped if mode was box + if mode == "box" and "box_label" in result and "name" not in result: + result["name"] = result["box_label"] return result # 2. Try Claude (Fallback) diff --git a/backend/routers/items.py b/backend/routers/items.py index bd89eea1..034af2de 100644 --- a/backend/routers/items.py +++ b/backend/routers/items.py @@ -66,6 +66,7 @@ _MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB async def extract_label( request: Request, file: UploadFile = File(...), + mode: str = "item", # 'item' or 'box' current_user: auth.TokenData = Depends(auth.get_current_user) ): """[C-01] Extract label from image — only for authenticated users. [H-02] Rate limit: 10 req/min per IP.""" @@ -86,7 +87,7 @@ async def extract_label( detail="File exceeds 10MB limit." ) - result = extract_label_info(contents) + result = extract_label_info(contents, mode=mode) return result @router.post("/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED) diff --git a/dev_docs/SESSION_STATE.md b/dev_docs/SESSION_STATE.md index 76f66810..b9f758bf 100644 --- a/dev_docs/SESSION_STATE.md +++ b/dev_docs/SESSION_STATE.md @@ -2,12 +2,12 @@ **Active AI:** Gemini (Antigravity) **Last Updated:** 2026-04-12 -**Current Version:** v1.4.0 +**Current Version:** v1.6.0 (BoxMaster) **Branch:** dev --- -## STATUS: 🟢 STABLE — BOX MANAGEMENT & LABEL PRINTING COMPLETE (v1.5.0) +## STATUS: 🟢 STABLE — ADVANCED BOX MANAGEMENT & AI MODES COMPLETE (v1.6.0) **CRITICAL FOR NEXT AI:** The "Box/Container Management" feature is **FINISHED**. Do NOT attempt to re-implement or look for a plan. The core logic is already in `frontend/app/page.tsx` (`onOCRMatch` and `BoxManager`), `backend/models.py`, and `frontend/lib/labels.ts`. @@ -30,10 +30,16 @@ - **Box Manager Dashboard** — Added a dedicated UI to view all existing boxes and trigger label generation. - **Hybrid Printing** — Implemented CSS `@media print` for professional desktop printers and "Save as PNG" rasterization for portable Bluetooth printers on mobile. -### 4. UI/UX Excellence -- **Search Integration** — Integrated `box_label` into the global search filter. -- **Onboarding Support** — Added Box Label association to the AI-powered onboarding workflow with smart `datalist` suggestions. -- **Visual Polish** — Applied Lucide `Package` iconography and consistent branding to the new modules. +### 4. UI/UX: Targeted Field Scanning +- **Camera Capture** — Added a dedicated scan button in Edit modals that redirects OCR results directly to the "Box Label" field without performing general item matches. + +### 5. Multi-Mode AI Discovery +- **Contextual Prompts** — Implemented a dual-mode toggle (Item/Box) in the AI Onboarding screen. +- **Box Extraction** — Created a specialized prompt for Gemini 2.0 Flash to extract container names while filtering out technical noise from product labels. + +### 6. Operational Rigor: Step 0 Rule +- **Mandatory Documentation** — Updated `AI_RULES.md` to force documentation verification before any `save-version` (git commit) operation. +- **Master Branch Sync** — Confirmed `scripts/save_version.py` logic to keep `master` branch in sync with the latest releases automatically. --- @@ -51,7 +57,10 @@ **Active database:** `/data/inventory.db` **LDAP config:** `backend/config/ldap_config.json` -**Production Bundle:** `aInventory-PROD-v1.4.0.zip` (Clean) +**Production Bundle:** `aInventory-PROD-v1.6.0.zip` (BoxMaster Final) + +> [!IMPORTANT] +> **Git Access Fix**: The `xcode-select` breakage is bypassed by using the direct binary path: `/Library/Developer/CommandLineTools/usr/bin/git` (stored in `.git_path`). **DO NOT change this path.** Operations now work correctly via this direct link. **How to start:** ```bash diff --git a/frontend/app/inventory/page.tsx b/frontend/app/inventory/page.tsx index 84124f6c..58c1e48a 100644 --- a/frontend/app/inventory/page.tsx +++ b/frontend/app/inventory/page.tsx @@ -1,9 +1,10 @@ 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { db, Item } from '@/lib/db'; import { inventoryApi } from '@/lib/api'; import PageShell from '@/components/PageShell'; +import Scanner from '@/components/Scanner'; import { toast } from 'react-hot-toast'; import { Package, @@ -17,7 +18,8 @@ import { X, AlertTriangle, Tag, - Edit2 + Edit2, + Camera } from 'lucide-react'; import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; @@ -50,6 +52,10 @@ export default function InventoryPage() { const [catEditedName, setCatEditedName] = useState(''); const [catEditedDesc, setCatEditedDesc] = useState(''); + // Scanner state + const [showScanner, setShowScanner] = useState(false); + const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null); + useEffect(() => { setMounted(true); const savedUser = localStorage.getItem('inventory_user'); @@ -182,6 +188,31 @@ export default function InventoryPage() { } }; + const onOCRMatch = useCallback(async (text: string) => { + const cleanText = text.toUpperCase().replace(/[^A-Z0-9\s/+-]/g, ' '); + const tokens = cleanText.split(/[\s\n,]+/).filter(t => t.length >= 3); + + if (fieldScanning?.active && fieldScanning.field === 'box_label') { + const label = tokens[0] || cleanText; + setEditedItem(prev => ({ ...prev, box_label: label })); + setFieldScanning(null); + setShowScanner(false); + toast.success(`Captured: ${label}`); + return; + } + }, [fieldScanning]); + + const onScanSuccess = useCallback((barcode: string) => { + // Inventory page doesn't do check-in via scanner, it just finds the item + const item = inventory.find(i => i.barcode === barcode); + if (item) { + setSelectedItem(item); + setShowScanner(false); + } else { + toast.error(`Item with barcode ${barcode} not found in catalog`); + } + }, [inventory]); + // Group items by category const categories = Array.from(new Set(inventory.map(i => i.category))); const filteredCategories = categories.filter(c => @@ -189,8 +220,9 @@ export default function InventoryPage() { inventory.some(i => i.category === c && i.name.toLowerCase().includes(searchQuery.toLowerCase())) ); - // Extract unique item types for suggestions + // Extract unique item types and box labels for suggestions const existingTypes = Array.from(new Set(inventory.map(i => i.type).filter(Boolean))).sort() as string[]; + const existingBoxes = Array.from(new Set(inventory.map(i => i.box_label).filter(Boolean))).sort() as string[]; if (!mounted) return null; @@ -200,6 +232,9 @@ export default function InventoryPage() { {existingTypes.map(t => + + {existingBoxes.map(b =>
@@ -412,6 +447,33 @@ export default function InventoryPage() { className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100" />
+
+ +
+ setEditedItem({...editedItem, box_label: e.target.value})} + className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 pl-4 pr-12 text-sm outline-none text-slate-100 placeholder:text-slate-700 focus:border-primary transition-colors" + placeholder="e.g. SFPs 40G Cisco" + /> + +
+
@@ -542,6 +604,13 @@ export default function InventoryPage() {
)} + {showScanner && ( + + )} + ); diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index a5e92e20..3f56e18c 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -10,6 +10,7 @@ import PageShell from '@/components/PageShell'; import { toast } from 'react-hot-toast'; import { Package, + Camera, Plus, Minus, Trash2, @@ -80,6 +81,7 @@ export default function Home() { const [syncing, setSyncing] = useState(false); const [currentUser, setCurrentUser] = useState(null); const [categories, setCategories] = useState([]); + const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null); useEffect(() => { if (!localStorage.getItem('inventory_token')) { @@ -240,6 +242,17 @@ export default function Home() { if (tokens.length === 0) return; + // [NEW] Targeted Field Scan Logic + if (fieldScanning?.active) { + if (fieldScanning.field === 'box_label') { + const potentialLabel = tokens[0] || cleanText; + setEditedItem(prev => ({ ...prev, box_label: potentialLabel })); + setFieldScanning(null); + toast.success(`Captured: ${potentialLabel}`); + return; + } + } + // Toast only potentially useful scans toast(`Scanning: ${cleanText.substring(0, 30)}...`, { icon: '🔍', duration: 1500, id: 'ocr-scan' }); @@ -662,14 +675,30 @@ export default function Home() {
- setEditedItem({...editedItem, box_label: e.target.value})} - className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100" - placeholder="e.g. SFPs 40G Cisco" - /> +
+ setEditedItem({...editedItem, box_label: e.target.value})} + className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 pl-4 pr-12 text-sm outline-none text-slate-100 placeholder:text-slate-700 focus:border-primary transition-colors" + placeholder="e.g. SFPs 40G Cisco" + /> + +
diff --git a/frontend/components/AIOnboarding.tsx b/frontend/components/AIOnboarding.tsx index a3737c76..7c14cd79 100644 --- a/frontend/components/AIOnboarding.tsx +++ b/frontend/components/AIOnboarding.tsx @@ -16,6 +16,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento const [image, setImage] = useState(null); const [uploading, setUploading] = useState(false); const [extractedData, setExtractedData] = useState(null); + const [mode, setMode] = useState<'item' | 'box'>('item'); // Extract unique item types for suggestions const existingTypes = Array.from(new Set(inventory.map(i => i.type).filter(Boolean))).sort() as string[]; @@ -42,7 +43,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento const formData = new FormData(); formData.append('file', blob, 'label.jpg'); - const data = await inventoryApi.analyzeLabel(formData); + const data = await inventoryApi.analyzeLabel(formData, mode); if (data.error) { toast.error(`AI Error: ${data.error}`); @@ -51,7 +52,11 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento } setExtractedData(data); - toast.success("AI extraction complete!"); + if (mode === 'box') { + toast.success(`Box identified: ${data.box_label || data.name}`); + } else { + toast.success("AI extraction complete!"); + } } catch (error) { toast.error("Failed to process image with AI"); console.error(error); @@ -97,13 +102,34 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento {!image ? (
+
+ + +
+
- +
-

Label Insight Mode

+

+ {mode === 'box' ? 'Container Discovery Mode' : 'Label Insight Mode'} +

- Scan or upload a sharp photo of the item specifications + {mode === 'box' + ? 'Scan the large, prominent label or hand-written name on the container' + : 'Scan or upload a sharp photo of the item specifications'}

diff --git a/frontend/components/Scanner.tsx b/frontend/components/Scanner.tsx index f0f59033..97963315 100644 --- a/frontend/components/Scanner.tsx +++ b/frontend/components/Scanner.tsx @@ -25,6 +25,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr const [isSelecting, setIsSelecting] = useState(false); const [capturedImage, setCapturedImage] = useState(null); const isBusy = useRef(false); + const isTransitioning = useRef(false); // Flag to track start/stop transitions const scannerId = "reader-container-unique"; useEffect(() => { @@ -33,11 +34,20 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr } const startScanner = async () => { - if (isBusy.current) return; + if (isBusy.current || isTransitioning.current) return; isBusy.current = true; + isTransitioning.current = true; + try { + // If already scanning, we MUST stop it first if (html5QrCodeRef.current?.isScanning) { - await html5QrCodeRef.current.stop(); + try { + await html5QrCodeRef.current.stop(); + } catch (e) { + console.warn("Graceful stop failed, might be in transition:", e); + // If it's already in transition, we just return and wait for the next effect trigger + return; + } } const config = { @@ -85,6 +95,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr } } finally { isBusy.current = false; + isTransitioning.current = false; } }; @@ -94,11 +105,15 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr return () => { if (html5QrCodeRef.current?.isScanning) { + isTransitioning.current = true; html5QrCodeRef.current.stop() .then(() => { setIsStarted(false); }) - .catch(e => console.error("Stop failed", e)); + .catch(e => console.error("Unmount stop failed", e)) + .finally(() => { + isTransitioning.current = false; + }); } }; }, [paused, onScanSuccess]); diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts index 46e43c81..53c7d01e 100644 --- a/frontend/lib/api.ts +++ b/frontend/lib/api.ts @@ -75,8 +75,8 @@ export const inventoryApi = { } }, - analyzeLabel: async (formData: FormData) => { - const res = await axiosInstance.post('/items/extract-label', formData, { + analyzeLabel: async (formData: FormData, mode: string = "item") => { + const res = await axiosInstance.post(`/items/extract-label?mode=${mode}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }); return res.data; diff --git a/scripts/save_version.py b/scripts/save_version.py index 35b413fe..4c0aa656 100644 --- a/scripts/save_version.py +++ b/scripts/save_version.py @@ -24,7 +24,7 @@ def run_command(cmd): def main(): git = get_git_path() - # 1. Read and Increment Version + # 1. Read and Parse Version if not os.path.exists(VERSION_FILE): print(f"Error: {VERSION_FILE} not found.") sys.exit(1) @@ -34,10 +34,20 @@ def main(): old_version = data.get('version', '1.0.0') parts = old_version.split('.') - if len(parts) == 3: - parts[2] = str(int(parts[2]) + 1) + if len(parts) < 3: + parts.extend(['0'] * (3 - len(parts))) + + # Check for --minor or --major flags + if '--minor' in sys.argv: + parts[1] = str(int(parts[1]) + 1) + parts[2] = '0' + elif '--major' in sys.argv: + parts[0] = str(int(parts[0]) + 1) + parts[1] = '0' + parts[2] = '0' else: - parts.append('1') + # Default: patch increment + parts[2] = str(int(parts[2]) + 1) new_version = '.'.join(parts) data['version'] = new_version