Build [v1.7.0] - GitGuard - Infrastructure Hardening

This commit is contained in:
Daniel Bedeleanu
2026-04-12 22:45:27 +03:00
parent 0a6368b9f6
commit 994920bda2
14 changed files with 262 additions and 76 deletions

View File

@@ -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() {
<datalist id="existing-types">
{existingTypes.map(t => <option key={t} value={t} />)}
</datalist>
<datalist id="existing-boxes">
{existingBoxes.map(b => <option key={b} value={b} />)}
</datalist>
<header className="flex items-center gap-5 mb-10">
<div className="p-4 bg-primary/10 rounded-[2rem] text-primary border border-primary/20 shadow-xl shadow-primary/5">
@@ -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"
/>
</div>
<div className="col-span-2">
<label className="text-xs font-black text-slate-500 ml-1">Box / Container Label</label>
<div className="relative flex items-center">
<input
type="text"
list="existing-boxes"
value={editedItem.box_label || ''}
onChange={e => 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"
/>
<button
type="button"
onClick={() => {
setFieldScanning({ active: true, field: 'box_label' });
setShowScanner(true);
toast.success("Ready to scan Box label...");
}}
className={cn(
"absolute right-2 p-2 rounded-lg transition-all",
fieldScanning?.active ? "bg-primary text-white animate-pulse" : "text-slate-500 hover:bg-slate-800"
)}
>
<Camera size={18} />
</button>
</div>
</div>
</div>
<div>
<label className="text-xs font-black text-slate-500 ml-1">Specs / Comments</label>
@@ -542,6 +604,13 @@ export default function InventoryPage() {
</div>
)}
{showScanner && (
<Scanner
onScanSuccess={onScanSuccess}
onOCRMatch={onOCRMatch}
/>
)}
</div>
</PageShell>
);

View File

@@ -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<any | null>(null);
const [categories, setCategories] = useState<any[]>([]);
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() {
</div>
<div className="col-span-2">
<label className="text-xs font-black text-slate-500 ml-1">Box / Container Label</label>
<input
type="text"
list="existing-boxes"
value={editedItem.box_label || ''}
onChange={e => 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"
/>
<div className="relative flex items-center">
<input
type="text"
list="existing-boxes"
value={editedItem.box_label || ''}
onChange={e => 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"
/>
<button
type="button"
onClick={() => {
setFieldScanning({ active: true, field: 'box_label' });
setShowScanner(true);
toast.success("Scanning for BOX label...");
}}
className={cn(
"absolute right-2 p-2 rounded-lg transition-all",
fieldScanning?.active ? "bg-primary text-white animate-pulse" : "text-slate-500 hover:bg-slate-800"
)}
>
<Camera size={18} />
</button>
</div>
</div>
<div className="col-span-2">
<label className="text-xs font-black text-slate-500 ml-1">Specs</label>

View File

@@ -16,6 +16,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
const [image, setImage] = useState<string | null>(null);
const [uploading, setUploading] = useState(false);
const [extractedData, setExtractedData] = useState<any>(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 ? (
<div className="flex-1 flex flex-col gap-6 min-h-0">
<div className="flex bg-slate-900/50 p-1.5 rounded-2xl border border-slate-800/50 shrink-0">
<button
onClick={() => setMode('item')}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-bold transition-all ${mode === 'item' ? 'bg-primary text-white shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}
>
<Package size={18} />
<span className="text-xs">Item Label</span>
</button>
<button
onClick={() => setMode('box')}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-bold transition-all ${mode === 'box' ? 'bg-primary text-white shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}
>
<Layers size={18} />
<span className="text-xs">Box / Container</span>
</button>
</div>
<div className="flex-1 flex flex-col items-center justify-center border-2 border-dashed border-slate-800 rounded-[2.5rem] bg-slate-900/30 overflow-hidden px-4">
<div className="w-20 h-20 bg-slate-900 rounded-3xl flex items-center justify-center mb-6 shadow-inner">
<Camera size={32} className="text-slate-500" />
<Camera size={32} className={mode === 'box' ? 'text-primary' : 'text-slate-500'} />
</div>
<p className="text-slate-300 mb-2 text-center font-bold">Label Insight Mode</p>
<p className="text-slate-300 mb-2 text-center font-bold">
{mode === 'box' ? 'Container Discovery Mode' : 'Label Insight Mode'}
</p>
<p className="text-xs text-slate-500 px-8 text-center leading-relaxed font-bold">
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'}
</p>
</div>

View File

@@ -25,6 +25,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
const [isSelecting, setIsSelecting] = useState(false);
const [capturedImage, setCapturedImage] = useState<string | null>(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]);

View File

@@ -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;