Compare commits

..

3 Commits

Author SHA1 Message Date
Daniel Bedeleanu
994920bda2 Build [v1.7.0] - GitGuard - Infrastructure Hardening 2026-04-12 22:45:27 +03:00
Daniel Bedeleanu
0a6368b9f6 modificari de documentatii dupa o implementare noua 2026-04-12 21:43:08 +03:00
Daniel Bedeleanu
26e8f034a2 Update save_version.py: Added automatic master branch synchronization 2026-04-12 21:32:55 +03:00
16 changed files with 334 additions and 150 deletions

View File

@@ -1,81 +1,60 @@
# AI AGENT RULES - MANDATORY ENTRY POINT # AI AGENT RULES - MANDATORY SSOT ENTRY POINT
**READ THIS ENTIRE FILE BEFORE EXECUTING ANY TASK.** **READ THIS ENTIRE FILE BEFORE EXECUTING ANY TASK.**
This is the **Single Source of Truth** for ALL Artificial Intelligence agents (Claude, Gemini, etc.) working on this project. This is the **Single Source of Truth** for ALL AI agents. Refer to [PROJECT_ARCHITECTURE.md](PROJECT_ARCHITECTURE.md) for technical logic.
(Automatic IDE entry points like `GEMINI.md` and `CLAUDE.md` exist solely to redirect agents to this main file).
For technical architecture, data models, and stack details, refer to [PROJECT_ARCHITECTURE.md](PROJECT_ARCHITECTURE.md).
## 1. Multi-AI Coordination & Memory ---
- **MANDATORY STARTUP**: Every AI session MUST first read `dev_docs/SESSION_STATE.md` to understand current context.
- **MANDATORY HANDOVER**: At the end of every task/session, update the handover note in `dev_docs/SESSION_STATE.md`. Specify: **Active AI**, **Current Status**, **Technical Context**, and **Next Steps**. ## 1. AI MEMORY, TRACEABILITY & HANDOVER
- **ARCHIVAL RULE**: Before writing new state, move all content of the *previous* session handover into the top of `dev_docs/SESSION_HISTORY.md` to prevent bloat. - **MANDATORY STARTUP**: Read `dev_docs/SESSION_STATE.md` immediately at session start.
- **PLAN RETIREMENT**: Mark a completed "Master Plan" as `[COMPLETED]` in the file itself. Move technical details to `dev_docs/ARCHIVE_LOGS.md` and `PLAN.md` entries to `dev_docs/PLAN_HISTORY.md`.
- **STRICT HANDOVER**: Update `dev_docs/SESSION_STATE.md` at the end of every task with: **Active AI**, **Current Status** (Stable/Broken/In-Progress), **Context**, and **Next Steps**.
- **SESSION ARCHIVE**: Move previous handover content to `dev_docs/SESSION_HISTORY.md` before writing new state.
- **NO INTERACTION OVERLAP**: Never modify a file if another AI session is explicitly working on it. - **NO INTERACTION OVERLAP**: Never modify a file if another AI session is explicitly working on it.
## 2. Global Operational Laws ## 2. ENGINEERING & OPERATIONAL LAWS
- **STRICT ENGLISH POLICY**: All web interfaces, code, variables, scripts, and documentation MUST BE DIRECTLY AND ONLY IN ENGLISH. If you find Romanian text in code, translate it immediately. - **ENGLISH ONLY**: Interfaces, code, variables, and docs MUST be in English. Translate any Romanian text found in code immediately. (User conversation: Romanian/English).
- **COMMUNICATION LANGUAGE**: Conversation with the user will be in Romanian or English (preferably Romanian). - **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).
- **GIT BINARY PATH**: On this macOS environment, the system `git` is often broken. ALWAYS use the binary path stored in `.git_path` at the project root for all Git operations. - **VERSIONING**: Update `VERSION.json` on every commit. Use `scripts/save_version.py` for automated releases.
- **COMMIT & PUSH STRICT RULE**: - **DEPENDENCIES**: Update `backend/requirements.txt` with version constraints for every new pip package.
- Never push to remote (`git push`) or use force flags (`--hard`, `--force`) unless explicitly requested. - **SSOT INTEGRITY**: Every feature change MUST update: `README.md`, `USER_GUIDE.md`, `PROJECT_ARCHITECTURE.md`, and `export_prod.sh`.
- Always update `VERSION.json` on every commit.
- Do NOT add AI co-author signatures (e.g., `Co-Authored-By: AI...`) to commit messages.
- Branching: `master` (stable), `dev` (active development), `vX` (archival releases).
- **PACKAGE MANAGEMENT**: Any new package installed via `pip install` MUST be added to `backend/requirements.txt` with version constraints (e.g., `slowapi>=0.1.9`). Keep requirements.txt synchronized with installed packages.
- **MANDATORY LOGGING**: Document coding/architecture changes in `dev_docs/ARCHIVE_LOGS.md` at the end of the task.
- **TRIPLE CONFIRMATION (Safe Forget)**: You cannot delete a physical location, item, or critical entity without explicit user permission three times.
## 3. UI/UX Fidelity Specifications ## 3. UI/UX "PREMIUM" FIDELITY STANDARDS
- **Fidelity First**: Never simplify the UI unless asked. Density and aesthetics must remain "Premium". - **Aesthetics**: Density/aesthetics must remain "Premium". Use Tailwind CSS. NO simplification.
- **Styling**: Tailwind CSS. Font weights/spacing must be consistent (Inter/Roboto). - **Typography Rules**:
- **Readability**: NO `uppercase` or `tracking-widest` styles. Use standard camel/Title case. - **NO UPPERCASE** or **NO ITALICS** in headers, labels, buttons, or metadata.
- **Icons**: Use **Lucide Icons** exclusively. NO emojis. - **NO `tracking-widest`**. Use standard camel/Title case.
- **Interactive Affordance**: All select/dropdown boxes MUST have a `ChevronDown` icon. Masked passwords should have reduced opacity (`text-white/50`). - Use `font-black` for main headings.
- **Iconography Standardization**: - **Layout**: Main pages MUST use `max-w-7xl`.
- **Categories**: ALWAYS use `Layers` (Color: `text-primary`). - **Unified Headers**: Icon box (`p-4 bg-primary/10 border-primary/20`) + Title (`text-3xl font-black`) + Subtitle (`text-xs text-slate-500`).
- **Item Types**: ALWAYS use `Package` (Color: `text-green-500`). - **Iconography**: Use **Lucide Icons** exclusively (NO emojis).
- **Categories**: `Layers` (text-primary).
- **Item Types**: `Package` (text-green-500).
- **Affordance**: Dropdowns MUST have a `ChevronDown`. Passwords: `text-white/50`. Logout MUST be `text-rose-500`.
## 4. Documentation Maintenance ## 4. DATA INTEGRITY & AUDIT POLICY
- **SSOT INTEGRITY**: Whenever a feature is added, modified, or removed, you MUST update all corresponding documentation files: - **RESTRICTED ACTIONS**: `DELETE /items/` and Admin settings require `auth.get_current_admin`.
- `README.md` (General usage & technical modes). - **AUDIT IMMUTABILITY**: Deleting an `Item` MUST NOT delete its `AuditLog` entries.
- `USER_GUIDE.md` (End-user instructions). - **TRACEABILITY**: Log deletions to `logs/backend.log` with `USER[id]`, `ITEM[id]`, `Name`, `PN`.
- `PROJECT_ARCHITECTURE.md` (Technical logic & data models). - **CONFIRMATION**:
- `export_prod.sh` (If new scripts or files must be included in the production bundle). - **Triple Confirmation**: Deleting critical entities (Locations/Items) requires user confirmation 3 times.
- **REAL-TIME UPDATES**: Documentation updates are NOT optional and must be performed within the same session as the code changes. - **Native Alerts**: Use `window.confirm` for all destructive UI actions and Logout.
## 6. AI Command Shortcuts ## 5. AI COMMAND SHORTCUTS
- **`save-version`**: When the user triggers this command, the AI MUST: - **`save-version`**:
1. Increment the patch version in `VERSION.json`. 0. **MANDATORY**: Verify and update ALL documentation (`.md` files: README, USER_GUIDE, ARCHITECTURE, etc.) with explanations of all current changes.
2. Stage all current changes (`git add .`). 1. Increment `VERSION.json`.
3. Commit changes with message `Build [vX.Y.Z]`. 2. Git add/commit (`Build [vX.Y.Z]`).
4. Create a new branch named `vX.Y.Z` from the current state. 3. Create branch `vX.Y.Z` (Snapshot).
5. Generate a production bundle ZIP (calls `./export_prod.sh`). 4. Automatic Sync: Merge changes into `master` branch to keep it up-to-date.
6. Stay on the current branch (`dev`). 5. Run `./export_prod.sh`.
- *Implementation*: Use `python3 scripts/save_version.py` to ensure consistency. (Always use `python3 scripts/save_version.py`).
## 7. UI Fidelity Laws (Strict) ---
- **Consistent Layout**: All main pages (Inventory, Audit, Admin) MUST use `max-w-7xl` for their primary content container.
- **Unified Headers**: Every functional page MUST have an identical header structure:
- Icon inside a styled box (`p-4 bg-primary/10 rounded-[2rem] border border-primary/20`).
- Title: `text-3xl font-black text-white tracking-tight`.
- Subtitle: `text-xs text-slate-500 font-bold mt-1 tracking-wider`.
- **Typographic Integrity**:
- **NO UPPERCASE** (ALL CAPS) allowed anywhere in the UI (headers, labels, buttons, metadata).
- **NO ITALICS** in headers, titles, or active UI elements.
- Consistent font weight (`font-black`) for main headings.
- **Safety Actions**:
- The **Logout** button MUST be clearly differentiated (e.g., `text-rose-500`) and MUST always require a `window.confirm` before execution.
## 8. Audit & Deletion Policy ## END OF SESSION PROTOCOL
- **Immutability of Logs**: Deleting an `Item` MUST NOT delete its corresponding entries in the `AuditLog` table in the database. End your final response on a separate line exactly with:
- **Disk Traceability**: Every item deletion MUST be logged to the disk file (`logs/backend.log`) with `USER[id]`, `ITEM[id]`, `Name`, and `PN`. ```
- **Explicit Confirmation**: Destructive actions (Delete Item from catalog) MUST always trigger a browser-native `window.confirm` before proceeding. ---
✓ Done.
```
## 5. End of Session Protocol
- Once a phase/task from `PLAN.md` is completed and verified, move that entry into `dev_docs/PLAN_HISTORY.md`.
- After finishing an entire job (including updating session state, architecture logs, and versioning), end your final response on a separate line exactly with:
```
---
✓ Done.
```
- Do not provide unnecessary verbatim summaries of the code.

View File

@@ -37,6 +37,7 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
### 4.1 AI Usage Policy ### 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. - **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. - **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. - **Validation Mask:** AI-extracted data is NEVER saved directly. It is presented in a validation UI for human confirmation.
### 4.2 Scanner Technical Specs ### 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. - Noise Filtering: Ignores `< 3` chars, decimals, and dates.
- Scoring: Exact S/N (+500), Exact P/N (+200), Token match (+50), Category match (+20). - 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. - 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) ### 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: - **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 ### 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. - **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. - **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 ### 7.4 PWA Trust & Security
- **HTTPS Enforcement:** The system requires TLS (Port 3003) for camera access and secure token transmission. - **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). - **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.

View File

@@ -34,7 +34,7 @@ Native Linux installation (Alma/Debian/Ubuntu) without Docker dependencies.
To generate a clean production package and snapshot the current state: To generate a clean production package and snapshot the current state:
1. Use the AI shortcut command: `save-version`. 1. Use the AI shortcut command: `save-version`.
2. Alternatively, run `./export_prod.sh` manually. 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. 4. A backup branch `v.1.3.x` will be created automatically.
--- ---

View File

@@ -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` / `<initial-password>` (check your system administrator for the initial password). - **Default User:** On first installation, use `Admin` / `<initial-password>` (check your system administrator for the initial password).
- **Change Password:** We recommend changing your password immediately from the Admin settings. - **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. - **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. If no readable text is found, the scanner silently retries on the next cycle.
### Box & Container Scanning (NEW v1.5.0) ### Box & Container Scanning (NEW v1.6.0)
You can now scan a generic label on a box (e.g., "SFP Box 1") to identify all its contents at once. You can now manage containers more efficiently with two specialized methods:
- 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. - **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.
- This feature works **locally and offline** - no AI costs for scanning boxes. - **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 **Last Updated:** 2026-04-12

View File

@@ -1,12 +1,5 @@
{ {
"version": "1.5.0", "version": "1.7.0",
"last_build": "2026-04-12-2130", "last_build": "2026-04-12-2224",
"commit": "HEAD", "codename": "GitGuard"
"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"
]
} }

View File

@@ -7,36 +7,55 @@ base_dir = os.path.dirname(os.path.abspath(__file__))
dotenv_path = os.path.join(base_dir, ".env") dotenv_path = os.path.join(base_dir, ".env")
load_dotenv(dotenv_path) 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. Orchestrates extraction across multiple AI providers.
Order: Gemini (Flash/Pro) -> Claude (Haiku/Sonnet) Modes: 'item' (full technical extraction), 'box' (container discovery)
"""
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
}
""" """
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 # 1. Try Gemini
result = gemini.extract(image_bytes, prompt) result = gemini.extract(image_bytes, prompt)
if result: 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 return result
# 2. Try Claude (Fallback) # 2. Try Claude (Fallback)

View File

@@ -66,6 +66,7 @@ _MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
async def extract_label( async def extract_label(
request: Request, request: Request,
file: UploadFile = File(...), file: UploadFile = File(...),
mode: str = "item", # 'item' or 'box'
current_user: auth.TokenData = Depends(auth.get_current_user) 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.""" """[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." detail="File exceeds 10MB limit."
) )
result = extract_label_info(contents) result = extract_label_info(contents, mode=mode)
return result return result
@router.post("/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED) @router.post("/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED)

View File

@@ -1,3 +1,19 @@
### [2026-04-12] v1.5.0: Box Management, Local OCR & Dependency-Free Label Printing
**Purpose:** Implementation of a local-first box scanning workflow, including multi-item container selection and professional label generation without external libraries.
**Actions:**
- `backend/models.py` & `schemas.py` — Added `box_label` field and integrated it into Pydantic models.
- `backend/routers/items.py` — Updated item creation and deletion to include `box_label` in immutable AuditLog snapshots.
- `frontend/lib/db.ts` — Upgraded IndexedDB (Dexie) to v4 with an index on `box_label` for high-speed local searching.
- `frontend/app/page.tsx` — Rewrote `onOCRMatch` to prioritize box matching. Implemented "Box Contents" selection modal and a full Box Inventory management dashboard.
- `frontend/components/AIOnboarding.tsx` — Added Box Label association to the AI-powered onboarding form.
- `frontend/lib/labels.ts` (NEW) — Developed a 100% dependency-free SVG engine for Barcode 128 and QR Code generation.
- `scripts/save_version.py` — Updated script to automatically synchronize `dev` changes into the `master` branch during releases.
- `AI_RULES.md` — Added Rule 1.1 for mandatory "Plan Retirement" and strict traceability.
- `USER_GUIDE.md` & `PROJECT_ARCHITECTURE.md` — Full documentation of the new container-based logic.
**Status:** Stable. Build v1.5.0 release branch created and merged into Master.
---
### [2026-04-12] v1.4.1: Security Hardening, PWA Optimization & Modern CSS Upgrade ### [2026-04-12] v1.4.1: Security Hardening, PWA Optimization & Modern CSS Upgrade
**Purpose:** Implementation of security audit recommendations, REST API test suite, PWA asset generation, and visual UI refinements using Modern CSS. **Purpose:** Implementation of security audit recommendations, REST API test suite, PWA asset generation, and visual UI refinements using Modern CSS.
**Actions:** **Actions:**

View File

@@ -1,6 +1,9 @@
# MASTER PLAN: Box/Container Scanning & Printing Architecture # [COMPLETED] MASTER PLAN: Box/Container Scanning & Printing Architecture
Acest plan detaliat descrie arhitectura tehnică și etapele procedurale necesare pentru a echipa sistemul TFM aInventory cu funcționalități de recunoaștere a cutiilor (Box Scanning). Este structurat astfel încât orice AI să poată prelua dezvoltarea exact din punctul în care a fost oprită. > [!IMPORTANT]
> **STATUS: FULLY IMPLEMENTED (v1.5.0)**
> Date: 2026-04-12
> This plan is no longer active. All phases (OCR, Smart Routing, Label Printing) have been merged into the main codebase.
--- ---

View File

@@ -2,14 +2,14 @@
**Active AI:** Gemini (Antigravity) **Active AI:** Gemini (Antigravity)
**Last Updated:** 2026-04-12 **Last Updated:** 2026-04-12
**Current Version:** v1.4.0 **Current Version:** v1.6.0 (BoxMaster)
**Branch:** dev **Branch:** dev
--- ---
## STATUS: 🟢 STABLE — BOX MANAGEMENT & LABEL PRINTING COMPLETE (v1.5.0) ## STATUS: 🟢 STABLE — ADVANCED BOX MANAGEMENT & AI MODES COMPLETE (v1.6.0)
The TFM aInventory v1.5.0 has been upgraded with a powerful local-first Box/Container management system. Users can now group items into boxes, identify them instantly via local OCR, and print physical Barcode/QR labels directly from the PWA. **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 @@ The TFM aInventory v1.5.0 has been upgraded with a powerful local-first Box/Cont
- **Box Manager Dashboard** — Added a dedicated UI to view all existing boxes and trigger label generation. - **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. - **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 ### 4. UI/UX: Targeted Field Scanning
- **Search Integration** — Integrated `box_label` into the global search filter. - **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.
- **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. ### 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 @@ The TFM aInventory v1.5.0 has been upgraded with a powerful local-first Box/Cont
**Active database:** `<project_root>/data/inventory.db` **Active database:** `<project_root>/data/inventory.db`
**LDAP config:** `backend/config/ldap_config.json` **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:** **How to start:**
```bash ```bash

View File

@@ -1,9 +1,10 @@
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { db, Item } from '@/lib/db'; import { db, Item } from '@/lib/db';
import { inventoryApi } from '@/lib/api'; import { inventoryApi } from '@/lib/api';
import PageShell from '@/components/PageShell'; import PageShell from '@/components/PageShell';
import Scanner from '@/components/Scanner';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { import {
Package, Package,
@@ -17,7 +18,8 @@ import {
X, X,
AlertTriangle, AlertTriangle,
Tag, Tag,
Edit2 Edit2,
Camera
} from 'lucide-react'; } from 'lucide-react';
import { clsx, type ClassValue } from 'clsx'; import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
@@ -50,6 +52,10 @@ export default function InventoryPage() {
const [catEditedName, setCatEditedName] = useState(''); const [catEditedName, setCatEditedName] = useState('');
const [catEditedDesc, setCatEditedDesc] = useState(''); const [catEditedDesc, setCatEditedDesc] = useState('');
// Scanner state
const [showScanner, setShowScanner] = useState(false);
const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
const savedUser = localStorage.getItem('inventory_user'); 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 // Group items by category
const categories = Array.from(new Set(inventory.map(i => i.category))); const categories = Array.from(new Set(inventory.map(i => i.category)));
const filteredCategories = categories.filter(c => 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())) 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 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; if (!mounted) return null;
@@ -200,6 +232,9 @@ export default function InventoryPage() {
<datalist id="existing-types"> <datalist id="existing-types">
{existingTypes.map(t => <option key={t} value={t} />)} {existingTypes.map(t => <option key={t} value={t} />)}
</datalist> </datalist>
<datalist id="existing-boxes">
{existingBoxes.map(b => <option key={b} value={b} />)}
</datalist>
<header className="flex items-center gap-5 mb-10"> <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"> <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" 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>
<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>
<div> <div>
<label className="text-xs font-black text-slate-500 ml-1">Specs / Comments</label> <label className="text-xs font-black text-slate-500 ml-1">Specs / Comments</label>
@@ -542,6 +604,13 @@ export default function InventoryPage() {
</div> </div>
)} )}
{showScanner && (
<Scanner
onScanSuccess={onScanSuccess}
onOCRMatch={onOCRMatch}
/>
)}
</div> </div>
</PageShell> </PageShell>
); );

View File

@@ -10,6 +10,7 @@ import PageShell from '@/components/PageShell';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import { import {
Package, Package,
Camera,
Plus, Plus,
Minus, Minus,
Trash2, Trash2,
@@ -80,6 +81,7 @@ export default function Home() {
const [syncing, setSyncing] = useState(false); const [syncing, setSyncing] = useState(false);
const [currentUser, setCurrentUser] = useState<any | null>(null); const [currentUser, setCurrentUser] = useState<any | null>(null);
const [categories, setCategories] = useState<any[]>([]); const [categories, setCategories] = useState<any[]>([]);
const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null);
useEffect(() => { useEffect(() => {
if (!localStorage.getItem('inventory_token')) { if (!localStorage.getItem('inventory_token')) {
@@ -240,6 +242,17 @@ export default function Home() {
if (tokens.length === 0) return; 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 only potentially useful scans
toast(`Scanning: ${cleanText.substring(0, 30)}...`, { icon: '🔍', duration: 1500, id: 'ocr-scan' }); toast(`Scanning: ${cleanText.substring(0, 30)}...`, { icon: '🔍', duration: 1500, id: 'ocr-scan' });
@@ -662,14 +675,30 @@ export default function Home() {
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs font-black text-slate-500 ml-1">Box / Container Label</label> <label className="text-xs font-black text-slate-500 ml-1">Box / Container Label</label>
<input <div className="relative flex items-center">
type="text" <input
list="existing-boxes" type="text"
value={editedItem.box_label || ''} list="existing-boxes"
onChange={e => setEditedItem({...editedItem, box_label: e.target.value})} value={editedItem.box_label || ''}
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100" onChange={e => setEditedItem({...editedItem, box_label: e.target.value})}
placeholder="e.g. SFPs 40G Cisco" 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>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs font-black text-slate-500 ml-1">Specs</label> <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 [image, setImage] = useState<string | null>(null);
const [uploading, setUploading] = useState(false); const [uploading, setUploading] = useState(false);
const [extractedData, setExtractedData] = useState<any>(null); const [extractedData, setExtractedData] = useState<any>(null);
const [mode, setMode] = useState<'item' | 'box'>('item');
// Extract unique item types for suggestions // Extract unique item types for suggestions
const existingTypes = Array.from(new Set(inventory.map(i => i.type).filter(Boolean))).sort() as string[]; 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(); const formData = new FormData();
formData.append('file', blob, 'label.jpg'); formData.append('file', blob, 'label.jpg');
const data = await inventoryApi.analyzeLabel(formData); const data = await inventoryApi.analyzeLabel(formData, mode);
if (data.error) { if (data.error) {
toast.error(`AI Error: ${data.error}`); toast.error(`AI Error: ${data.error}`);
@@ -51,7 +52,11 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
} }
setExtractedData(data); 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) { } catch (error) {
toast.error("Failed to process image with AI"); toast.error("Failed to process image with AI");
console.error(error); console.error(error);
@@ -97,13 +102,34 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
{!image ? ( {!image ? (
<div className="flex-1 flex flex-col gap-6 min-h-0"> <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="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"> <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> </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"> <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> </p>
</div> </div>

View File

@@ -25,6 +25,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
const [isSelecting, setIsSelecting] = useState(false); const [isSelecting, setIsSelecting] = useState(false);
const [capturedImage, setCapturedImage] = useState<string | null>(null); const [capturedImage, setCapturedImage] = useState<string | null>(null);
const isBusy = useRef(false); const isBusy = useRef(false);
const isTransitioning = useRef(false); // Flag to track start/stop transitions
const scannerId = "reader-container-unique"; const scannerId = "reader-container-unique";
useEffect(() => { useEffect(() => {
@@ -33,11 +34,20 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
} }
const startScanner = async () => { const startScanner = async () => {
if (isBusy.current) return; if (isBusy.current || isTransitioning.current) return;
isBusy.current = true; isBusy.current = true;
isTransitioning.current = true;
try { try {
// If already scanning, we MUST stop it first
if (html5QrCodeRef.current?.isScanning) { 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 = { const config = {
@@ -85,6 +95,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
} }
} finally { } finally {
isBusy.current = false; isBusy.current = false;
isTransitioning.current = false;
} }
}; };
@@ -94,11 +105,15 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
return () => { return () => {
if (html5QrCodeRef.current?.isScanning) { if (html5QrCodeRef.current?.isScanning) {
isTransitioning.current = true;
html5QrCodeRef.current.stop() html5QrCodeRef.current.stop()
.then(() => { .then(() => {
setIsStarted(false); setIsStarted(false);
}) })
.catch(e => console.error("Stop failed", e)); .catch(e => console.error("Unmount stop failed", e))
.finally(() => {
isTransitioning.current = false;
});
} }
}; };
}, [paused, onScanSuccess]); }, [paused, onScanSuccess]);

View File

@@ -75,8 +75,8 @@ export const inventoryApi = {
} }
}, },
analyzeLabel: async (formData: FormData) => { analyzeLabel: async (formData: FormData, mode: string = "item") => {
const res = await axiosInstance.post('/items/extract-label', formData, { const res = await axiosInstance.post(`/items/extract-label?mode=${mode}`, formData, {
headers: { 'Content-Type': 'multipart/form-data' } headers: { 'Content-Type': 'multipart/form-data' }
}); });
return res.data; return res.data;

View File

@@ -24,7 +24,7 @@ def run_command(cmd):
def main(): def main():
git = get_git_path() git = get_git_path()
# 1. Read and Increment Version # 1. Read and Parse Version
if not os.path.exists(VERSION_FILE): if not os.path.exists(VERSION_FILE):
print(f"Error: {VERSION_FILE} not found.") print(f"Error: {VERSION_FILE} not found.")
sys.exit(1) sys.exit(1)
@@ -34,10 +34,20 @@ def main():
old_version = data.get('version', '1.0.0') old_version = data.get('version', '1.0.0')
parts = old_version.split('.') parts = old_version.split('.')
if len(parts) == 3: if len(parts) < 3:
parts[2] = str(int(parts[2]) + 1) 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: else:
parts.append('1') # Default: patch increment
parts[2] = str(int(parts[2]) + 1)
new_version = '.'.join(parts) new_version = '.'.join(parts)
data['version'] = new_version data['version'] = new_version
@@ -57,11 +67,18 @@ def main():
branch_name = f"v{new_version}" branch_name = f"v{new_version}"
run_command([git, 'branch', branch_name]) run_command([git, 'branch', branch_name])
# 4. Create Production Bundle # 4. Update master branch
print(f"🔄 Updating 'master' branch to v{new_version}...")
current_branch = run_command([git, 'rev-parse', '--abbrev-ref', 'HEAD'])
run_command([git, 'checkout', 'master'])
run_command([git, 'merge', current_branch])
run_command([git, 'checkout', current_branch])
# 5. Create Production Bundle
print("📦 Generating production bundle...") print("📦 Generating production bundle...")
run_command(['./export_prod.sh']) run_command(['./export_prod.sh'])
print(f"Successfully saved version {new_version}, created branch {branch_name}, and generated production ZIP.") print(f"Successfully saved version {new_version}, created branch {branch_name}, updated 'master', and generated production ZIP.")
print("Verification: check for the .zip file in the root.") print("Verification: check for the .zip file in the root.")
if __name__ == "__main__": if __name__ == "__main__":