Compare commits

...

15 Commits

Author SHA1 Message Date
Daniel Bedeleanu
07b15c8e01 Build [v1.9.1] (CORS Dynamic IP Resolution Fix) 2026-04-13 20:52:29 +03:00
Daniel Bedeleanu
65cc0c7b6d Official Release [v1.9.0] (Stability Milestone • Automatic Git Commit ID) 2026-04-13 20:41:10 +03:00
Daniel Bedeleanu
f137ded5aa Build [v1.8.9] (Runtime Permission Healing for Docker Volumes) 2026-04-13 20:32:40 +03:00
Daniel Bedeleanu
946f1787c7 Build [v1.8.8] (Complete Frontend Build Cleaned & Verified) 2026-04-13 20:24:21 +03:00
Daniel Bedeleanu
09be0b401d Build [v1.8.7] (Fix strict Type error in logs page) 2026-04-13 20:07:55 +03:00
Daniel Bedeleanu
3069a921e7 Build [v1.8.6] (TypeScript unknown catch type fix) 2026-04-13 19:56:27 +03:00
Daniel Bedeleanu
55e3cf5042 Build [v1.8.5] (Self-contained Frontend Build) 2026-04-13 19:50:28 +03:00
Daniel Bedeleanu
c874d27d64 Build [v1.8.4] (Satisfy frontend relative VERSION.json path) 2026-04-13 19:44:52 +03:00
Daniel Bedeleanu
939ad8648d Build [v1.8.3] (Explicit directory creation in bundle) 2026-04-13 19:39:33 +03:00
Daniel Bedeleanu
6f6caf3c5a Build [v1.8.2] (Fix missing scripts in bundle) 2026-04-13 19:38:11 +03:00
Daniel Bedeleanu
5e648002f0 Build [v1.8.1] (Fix Docker Context) 2026-04-13 19:36:06 +03:00
Daniel Bedeleanu
81b775c9ae Build [v1.8.0] 2026-04-13 19:23:48 +03:00
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
37 changed files with 631 additions and 249 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

@@ -24,8 +24,9 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
### 2.3 Operations & Tooling ### 2.3 Operations & Tooling
- **PWA Deployment:** `next-pwa` (Service Workers + Manifest.json) - **PWA Deployment:** `next-pwa` (Service Workers + Manifest.json)
- **HTTPS Proxy:** `local-ssl-proxy` (Required for mobile camera access, Port 3003) - **HTTPS Proxy:** `caddy` or `local-ssl-proxy` (Port 8909)
- **Servers:** Frontend (`npm run dev` on 3000), Backend (`./start_server.sh` on 8000) - **Servers:** Frontend (Port 8907), Backend (Port 8906)
- **Configuration:** Centrally managed via root `config/` directory (includes `network_config.env`, `ldap_config.json`, `Caddyfile`).
## 3. Data Models & Entities ## 3. Data Models & Entities
- **Item:** Name, Category Group (Structured), Item Type (Specific), Quantity, Barcode, Part Number, Box Label (Association). - **Item:** Name, Category Group (Structured), Item Type (Specific), Quantity, Barcode, Part Number, Box Label (Association).
@@ -37,6 +38,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 +50,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 +86,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 8909) 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

@@ -12,21 +12,21 @@ This project supports three distinct operational modes:
Ideal for local development on macOS/Linux. Ideal for local development on macOS/Linux.
* **Command:** `./start_server.sh` * **Command:** `./start_server.sh`
* **Details:** Runs FastAPI (backend) and Next.js (frontend) in development mode. Uses `local-ssl-proxy` for HTTPS. * **Details:** Runs FastAPI (backend) and Next.js (frontend) in development mode. Uses `local-ssl-proxy` for HTTPS.
* **Backend:** http://localhost:8000 * **Backend:** http://localhost:8906
* **Frontend:** https://localhost:3003 * **Frontend:** https://localhost:8909
### 2. 🐳 Docker Mode (Recommended for Production) ### 2. 🐳 Docker Mode (Recommended for Production)
Isolated and portable container stack. Isolated and portable container stack.
* **Command:** `docker-compose up -d --build` * **Command:** `docker-compose up -d --build`
* **Details:** Uses Caddy as a reverse proxy for HTTPS. Persistent data and logs are mapped to `./data` and `./logs`. * **Details:** Uses Caddy as a reverse proxy for HTTPS. Persistent data and logs are mapped to `./data` and `./logs`.
* **Access:** https://localhost:3003 * **Access:** https://localhost:8909
### 3. 🐧 Standalone Linux Mode (Systemd) ### 3. 🐧 Standalone Linux Mode (Systemd)
Native Linux installation (Alma/Debian/Ubuntu) without Docker dependencies. Native Linux installation (Alma/Debian/Ubuntu) without Docker dependencies.
* **Installation:** `sudo ./install_service.sh` * **Installation:** `sudo ./install_service.sh`
* **Execution:** `sudo systemctl start inventory` * **Execution:** `sudo systemctl start inventory`
* **Details:** Compiles the frontend for production and manages the entire stack as a system service. * **Details:** Compiles the frontend for production and manages the entire stack as a system service.
* **Access:** https://<SERVER-IP>:3003 * **Access:** https://<SERVER-IP>:8909
--- ---
@@ -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.
--- ---
@@ -77,6 +77,12 @@ export ALLOWED_ORIGINS="https://your-domain.com"
docker-compose up -d --build docker-compose up -d --build
``` ```
### 🌐 Network & Port Customization
The application uses a central configuration file for all network settings:
- **Location:** `config/network_config.env`
- **Purpose:** Change the `SERVER_IP` (default: `192.168.84.113`) and reserved ports (`8906-8909`).
- **Mechanism:** Startup scripts automatically sync these settings to the frontend and Docker environment.
For detailed security audit report, see [dev_docs/SECURITY_REPORT.md](dev_docs/SECURITY_REPORT.md). For detailed security audit report, see [dev_docs/SECURITY_REPORT.md](dev_docs/SECURITY_REPORT.md).
--- ---

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.
--- ---
@@ -99,6 +100,11 @@ If your organization uses LDAP/Active Directory, administrators can:
### Settings ### Settings
Access application settings from the **Admin** panel. Access application settings from the **Admin** panel.
### 🌐 Network & Configuration (NEW v1.8.0)
The application now uses a centralized configuration folder in the project root:
- **`config/`**: Contains all network settings (`network_config.env`), LDAP profiles (`ldap_config.json`), and security proxy rules (`Caddyfile`).
- **Dynamic Port Mapping**: Changes to the server IP or ports in the configuration are automatically detected by both the frontend and backend after a restart.
--- ---
## 🚨 Security Notices ## 🚨 Security Notices
@@ -140,5 +146,5 @@ For detailed technical documentation, see the [Project Architecture](../PROJECT_
--- ---
**Version:** v1.5.0 **Version:** v1.8.4
**Last Updated:** 2026-04-12 **Last Updated:** 2026-04-13

View File

@@ -1,12 +0,0 @@
{
"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"
]
}

View File

@@ -1,10 +1,11 @@
FROM python:3.12-slim FROM python:3.12-slim
# Install system dependencies required for python-ldap (needed by backend) # Install system dependencies
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
build-essential \ build-essential \
libldap2-dev \ libldap2-dev \
libsasl2-dev \ libsasl2-dev \
gosu \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
@@ -26,14 +27,13 @@ COPY scripts ./scripts
ENV DATA_DIR="/app/data" ENV DATA_DIR="/app/data"
ENV LOGS_DIR="/app/logs" ENV LOGS_DIR="/app/logs"
# Ensure the appuser can write to data and logs if we pre-create them, # Pre-create directories and ensure they are writable
# although Docker volumes will handle ownership context.
RUN mkdir -p /app/data /app/logs && chown -R appuser:appuser /app RUN mkdir -p /app/data /app/logs && chown -R appuser:appuser /app
# Make initialization scripts executable # Make initialization scripts executable
RUN chmod +x /app/scripts/init_data.sh /app/backend/entrypoint.sh RUN chmod +x /app/scripts/init_data.sh /app/backend/entrypoint.sh
USER appuser EXPOSE 8000
EXPOSE 8000 EXPOSE 8000

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

@@ -23,6 +23,10 @@ export LOGS_DIR="${LOGS_DIR:-/app/logs}"
echo "🐳 [Docker] Running data initialization..." echo "🐳 [Docker] Running data initialization..."
bash /app/scripts/init_data.sh bash /app/scripts/init_data.sh
# Hand off to the application server # Fix permissions for mounted volumes (which might be root-owned by the host)
echo "🐳 [Docker] Starting uvicorn..." echo "🐳 [Docker] Fixing volume permissions..."
exec python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000 chown -R appuser:appuser "${DATA_DIR}" "${LOGS_DIR}"
# Hand off to the application server as non-root user
echo "🐳 [Docker] Starting uvicorn as appuser..."
exec gosu appuser python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000

View File

@@ -19,14 +19,38 @@ log.info("Database tables verified.")
app = FastAPI(title="TFM aInventory API", version="1.1.0") app = FastAPI(title="TFM aInventory API", version="1.1.0")
log.info("TFM aInventory API process started.") log.info("TFM aInventory API process started.")
# [SECURITY FIX M-01] CORS: allow_origins=["*"] + allow_credentials=True is invalid per spec. # [SECURITY FIX M-01] CORS Configuration
# Allowed origins are configured via ALLOWED_ORIGINS environment variable (comma-separated). # We dynamically build allowed origins from environment variables to simplify deployment.
# Secure fallback: localhost only for development. _raw_origins = os.environ.get("ALLOWED_ORIGINS", "")
_raw_origins = os.environ.get(
"ALLOWED_ORIGINS",
"http://localhost:3000,http://localhost:3002"
)
ALLOWED_ORIGINS = [o.strip() for o in _raw_origins.split(",") if o.strip()] ALLOWED_ORIGINS = [o.strip() for o in _raw_origins.split(",") if o.strip()]
# Automatically add origins based on network_config.env variables if present
server_ip = os.environ.get("SERVER_IP")
front_port = os.environ.get("FRONTEND_PORT", "8907")
front_ssl_port = os.environ.get("FRONTEND_SSL_PORT", "8909")
back_ssl_port = os.environ.get("BACKEND_SSL_PORT", "8908")
# Always allow localhost
defaults = [
f"http://localhost:{front_port}",
f"https://localhost:{front_ssl_port}",
f"https://localhost:{back_ssl_port}",
]
for d in defaults:
if d not in ALLOWED_ORIGINS:
ALLOWED_ORIGINS.append(d)
# Add IP-based origins if SERVER_IP is set
if server_ip and server_ip != "localhost":
ip_origins = [
f"http://{server_ip}:{front_port}",
f"https://{server_ip}:{front_ssl_port}",
f"https://{server_ip}:{back_ssl_port}",
]
for ip_o in ip_origins:
if ip_o not in ALLOWED_ORIGINS:
ALLOWED_ORIGINS.append(ip_o)
log.info(f"CORS allowed origins: {ALLOWED_ORIGINS}") log.info(f"CORS allowed origins: {ALLOWED_ORIGINS}")
# Add CORS middleware FIRST (before rate limiter) # Add CORS middleware FIRST (before rate limiter)

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

@@ -20,8 +20,9 @@ limiter = Limiter(key_func=get_remote_address)
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
def get_ldap_config(): def get_ldap_config():
# Read from backend/config/ directory # Read from root /config directory
config_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config") root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
config_dir = os.path.join(root_dir, "config")
config_path = os.path.join(config_dir, "ldap_config.json") config_path = os.path.join(config_dir, "ldap_config.json")
if os.path.exists(config_path): if os.path.exists(config_path):
with open(config_path, "r") as f: with open(config_path, "r") as f:
@@ -314,7 +315,8 @@ def update_ldap_settings(
current_user: auth.TokenData = Depends(auth.get_current_admin) current_user: auth.TokenData = Depends(auth.get_current_admin)
): ):
"""[C-01] Update LDAP config — admin only.""" """[C-01] Update LDAP config — admin only."""
config_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config") root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
config_dir = os.path.join(root_dir, "config")
os.makedirs(config_dir, exist_ok=True) os.makedirs(config_dir, exist_ok=True)
config_path = os.path.join(config_dir, "ldap_config.json") config_path = os.path.join(config_dir, "ldap_config.json")
with open(config_path, "w") as f: with open(config_path, "w") as f:

View File

@@ -1,12 +1,12 @@
# TFM aInventory - Caddy Self-Signed Internal Proxy # TFM aInventory - Caddy Self-Signed Internal Proxy
# This replaces the need for `local-ssl-proxy` in Node. # This replaces the need for `local-ssl-proxy` in Node.
:3003 { :{$FRONTEND_SSL_PORT} {
tls internal tls internal
reverse_proxy frontend:3000 reverse_proxy frontend:3000
} }
:3002 { :{$BACKEND_SSL_PORT} {
tls internal tls internal
reverse_proxy backend:8000 reverse_proxy backend:8000
} }

View File

@@ -17,8 +17,8 @@ JWT_SECRET_KEY=change-me-generate-a-secure-random-value
# --- CORS --- # --- CORS ---
# Comma-separated list of allowed frontend origins # Comma-separated list of allowed frontend origins
# Example for LAN deployment: # Example for LAN deployment:
# ALLOWED_ORIGINS=http://192.168.1.100:3000,https://192.168.1.100:3003 # ALLOWED_ORIGINS=http://192.168.84.113:8907,https://192.168.84.113:8909
ALLOWED_ORIGINS=http://localhost:3000,https://localhost:3003 ALLOWED_ORIGINS=http://localhost:8907,https://localhost:8909
# --- Data Paths (overridden by start_server.sh / docker-compose) --- # --- Data Paths (overridden by start_server.sh / docker-compose) ---
# DATA_DIR=/absolute/path/to/data # DATA_DIR=/absolute/path/to/data

19
config/ldap_config.json Normal file
View File

@@ -0,0 +1,19 @@
{
"ldap_enabled": true,
"server_uri": "ldaps://192.168.84.107:6360",
"base_dn": "dc=ldap,dc=lan",
"user_template": "uid={username},ou=people,dc=ldap,dc=lan",
"groups_dn": "ou=groups",
"use_tls": true,
"role_mappings": [
{
"group": "inventory_admins",
"role": "admin"
},
{
"group": "inventory_users",
"role": "user"
}
],
"ignore_cert": true
}

28
config/network_config.env Normal file
View File

@@ -0,0 +1,28 @@
# =============================================================================
# TFM aInventory - Central Network Configuration
# =============================================================================
# Use this file to customize the ports and IP address used by the application
# without needing to modify the source code.
#
# IMPORTANT: After modifying this file, restart the application for changes
# to take effect.
# =============================================================================
# The primary IP address where the application will be accessed.
# Used for CORS settings and documentation links.
SERVER_IP=192.168.84.113
# --- BACKEND PORTS ---
# Internal port for the FastAPI server (Backend)
BACKEND_PORT=8906
# External port for the Backend HTTPS Proxy (Caddy/local-ssl-proxy)
BACKEND_SSL_PORT=8908
# --- FRONTEND PORTS ---
# Internal port for the Next.js dev/prod server
FRONTEND_PORT=8907
# External port for the Frontend HTTPS Proxy (Caddy/local-ssl-proxy)
# This is the port you will use in your browser (e.g. https://192.168.84.113:8909)
FRONTEND_SSL_PORT=8909

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.
--- ---
@@ -50,15 +56,20 @@ The TFM aInventory v1.5.0 has been upgraded with a powerful local-first Box/Cont
## SYSTEM STATE ## SYSTEM STATE
**Active database:** `<project_root>/data/inventory.db` **Active database:** `<project_root>/data/inventory.db`
**LDAP config:** `backend/config/ldap_config.json` **LDAP config:** `config/ldap_config.json`
**Production Bundle:** `aInventory-PROD-v1.4.0.zip` (Clean) **Network config:** `config/network_config.env`
**Proxy config:** `config/Caddyfile`
**Production Bundle:** `aInventory-PROD-v1.8.0.zip` (ConfigSync 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
./start_server.sh ./start_server.sh
``` ```
- Frontend: `https://<LOCAL_IP>:3003` - Frontend: `https://192.168.84.113:8909`
- Backend: `https://<LOCAL_IP>:3002` - Backend: `https://192.168.84.113:8908`
**Environment variables set by start_server.sh:** **Environment variables set by start_server.sh:**
- `ALLOWED_ORIGINS` — auto-detected - `ALLOWED_ORIGINS` — auto-detected

View File

@@ -1,5 +1,3 @@
version: '3.8'
services: services:
backend: backend:
build: build:
@@ -8,7 +6,9 @@ services:
networks: networks:
- inventory_net - inventory_net
ports: ports:
- "8000:8000" - ${BACKEND_PORT:-8000}:8000
env_file:
- ./config/network_config.env
volumes: volumes:
- ./data:/app/data - ./data:/app/data
- ./logs:/app/logs - ./logs:/app/logs
@@ -16,20 +16,23 @@ services:
environment: environment:
- DATA_DIR=/app/data - DATA_DIR=/app/data
- LOGS_DIR=/app/logs - LOGS_DIR=/app/logs
# [M-01] CORS allowed origins — customize for production - SERVER_IP=${SERVER_IP}
- ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3002 - FRONTEND_PORT=${FRONTEND_PORT}
- FRONTEND_SSL_PORT=${FRONTEND_SSL_PORT}
- BACKEND_SSL_PORT=${BACKEND_SSL_PORT}
# [C-01] JWT secret key — GENERATE A SECURE VALUE FOR PRODUCTION! # [C-01] JWT secret key — GENERATE A SECURE VALUE FOR PRODUCTION!
- JWT_SECRET_KEY=${JWT_SECRET_KEY:-change-me-in-production} - JWT_SECRET_KEY=${JWT_SECRET_KEY:-change_me_in_production}
restart: unless-stopped restart: unless-stopped
frontend: frontend:
build: build:
context: . context: ./frontend
dockerfile: frontend/Dockerfile
networks: networks:
- inventory_net - inventory_net
ports: ports:
- "3000:3000" - ${FRONTEND_PORT:-3000}:3000
env_file:
- ./config/network_config.env
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs
# Write Next.js logs to both stdout (docker logs) and file (mapped volume) # Write Next.js logs to both stdout (docker logs) and file (mapped volume)
@@ -41,10 +44,12 @@ services:
networks: networks:
- inventory_net - inventory_net
ports: ports:
- "3002:3002" - ${BACKEND_SSL_PORT:-3002}:${BACKEND_SSL_PORT:-3002}
- "3003:3003" - ${FRONTEND_SSL_PORT:-3003}:${FRONTEND_SSL_PORT:-3003}
env_file:
- ./config/network_config.env
volumes: volumes:
- ./Caddyfile:/etc/caddy/Caddyfile - ./config/Caddyfile:/etc/caddy/Caddyfile
# Persist the internal Caddy certificates so users don't get new certificate warnings constantly # Persist the internal Caddy certificates so users don't get new certificate warnings constantly
- ./data/caddy_data:/data - ./data/caddy_data:/data
- ./data/caddy_config:/config - ./data/caddy_config:/config

View File

@@ -3,8 +3,8 @@
echo "📦 Preparing TFM aInventory Production Bundle..." echo "📦 Preparing TFM aInventory Production Bundle..."
# Extract version from VERSION.json using grep to avoid macOS python/xcode stubs # Extract version from frontend/VERSION.json
VERSION=$(grep '"version"' VERSION.json | head -n 1 | awk -F '"' '{print $4}') VERSION=$(grep '"version"' frontend/VERSION.json | head -n 1 | awk -F '"' '{print $4}')
PROD_DIR="aInventory-PROD-v${VERSION}" PROD_DIR="aInventory-PROD-v${VERSION}"
# Clean previous run if it exists # Clean previous run if it exists
@@ -18,9 +18,11 @@ echo "📂 Copying application components (excluding dev artifacts)..."
rsync -a --exclude 'node_modules' --exclude '.next' frontend/ "$PROD_DIR/frontend/" rsync -a --exclude 'node_modules' --exclude '.next' frontend/ "$PROD_DIR/frontend/"
rsync -a --exclude '__pycache__' --exclude '.pytest_cache' --exclude '.venv' --exclude 'tests' backend/ "$PROD_DIR/backend/" rsync -a --exclude '__pycache__' --exclude '.pytest_cache' --exclude '.venv' --exclude 'tests' backend/ "$PROD_DIR/backend/"
# Orchestration & Scripts # Orchestration, Config & Scripts
mkdir -p "$PROD_DIR/config" "$PROD_DIR/scripts"
cp docker-compose.yml "$PROD_DIR/" cp docker-compose.yml "$PROD_DIR/"
cp Caddyfile "$PROD_DIR/" rsync -a config/ "$PROD_DIR/config/"
rsync -a scripts/ "$PROD_DIR/scripts/"
cp start_server.sh "$PROD_DIR/" cp start_server.sh "$PROD_DIR/"
cp run_standalone.sh "$PROD_DIR/" cp run_standalone.sh "$PROD_DIR/"
cp install_service.sh "$PROD_DIR/" cp install_service.sh "$PROD_DIR/"
@@ -28,7 +30,8 @@ cp inventory.service.template "$PROD_DIR/"
cp USER_GUIDE.md "$PROD_DIR/" cp USER_GUIDE.md "$PROD_DIR/"
cp README.md "$PROD_DIR/INSTALLATION_GUIDE.md" cp README.md "$PROD_DIR/INSTALLATION_GUIDE.md"
cp .git_path "$PROD_DIR/" 2>/dev/null || true cp .git_path "$PROD_DIR/" 2>/dev/null || true
cp VERSION.json "$PROD_DIR/" cp frontend/VERSION.json "$PROD_DIR/"
cp frontend/VERSION.json "$PROD_DIR/frontend/"
# Setup persistent volume skeleton # Setup persistent volume skeleton
mkdir -p "$PROD_DIR/data" mkdir -p "$PROD_DIR/data"
@@ -44,7 +47,7 @@ TO RUN VIA DOCKER (Recommended):
1. Install Docker Desktop or Docker Engine. 1. Install Docker Desktop or Docker Engine.
2. Run: docker-compose build 2. Run: docker-compose build
3. Run: docker-compose up -d 3. Run: docker-compose up -d
4. Access via https://<YOUR-IP>:3003 (Accept the internal security warning). 4. Access via https://<YOUR-IP>:8909 (Accept the internal security warning).
TO INSTALL AS A LINUX SYSTEM SERVICE (Optional): TO INSTALL AS A LINUX SYSTEM SERVICE (Optional):
1. sudo ./install_service.sh 1. sudo ./install_service.sh
@@ -54,7 +57,7 @@ TO RUN BARE-METAL (No Docker):
1. Install Python 3.12+ and Node.js 20+. 1. Install Python 3.12+ and Node.js 20+.
2. Ensure you have network access for npm installs. 2. Ensure you have network access for npm installs.
3. Run: ./start_server.sh 3. Run: ./start_server.sh
4. Access via https://<YOUR-IP>:3003 4. Access via https://<YOUR-IP>:8909
Note: Database and Logs will persist in the /data and /logs directories. Note: Database and Logs will persist in the /data and /logs directories.
EOF EOF

View File

@@ -19,6 +19,7 @@ RUN npm run build
# Step 3: Production image # Step 3: Production image
FROM base AS runner FROM base AS runner
RUN apk add --no-cache su-exec
WORKDIR /app WORKDIR /app
ENV NODE_ENV production ENV NODE_ENV production
@@ -38,11 +39,13 @@ RUN chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs # Copy entrypoint script
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
EXPOSE 3000 EXPOSE 3000
ENV PORT 3000 ENV PORT 3000
ENV HOSTNAME "0.0.0.0" ENV HOSTNAME "0.0.0.0"
# Note: The server.js is created by next build from the standalone output ENTRYPOINT ["/app/entrypoint.sh"]
CMD ["node", "server.js"]

1
frontend/VERSION.json Normal file
View File

@@ -0,0 +1 @@
{"version": "1.9.1", "last_build": "2026-04-13-2052", "codename": "NetworkFix", "commit": "65cc0c7b"}

View File

@@ -83,7 +83,7 @@ export default function AdminPage() {
setBackups(b); setBackups(b);
setDbStats(s); setDbStats(s);
setDbSettings(st); setDbSettings(st);
} catch (err) { } catch (err: any) {
console.error(err); console.error(err);
toast.error("Failed to load admin data"); toast.error("Failed to load admin data");
} finally { } finally {
@@ -101,7 +101,7 @@ export default function AdminPage() {
await inventoryApi.createUser({ username: name, password: pwd, role: 'user' }); await inventoryApi.createUser({ username: name, password: pwd, role: 'user' });
toast.success("User created successfully"); toast.success("User created successfully");
loadData(); loadData();
} catch (err) { } catch (err: any) {
toast.error("Failed to create user"); toast.error("Failed to create user");
} }
}; };
@@ -114,7 +114,7 @@ export default function AdminPage() {
await inventoryApi.deleteUser(id); await inventoryApi.deleteUser(id);
toast.success("User removed"); toast.success("User removed");
loadData(); loadData();
} catch (err) { } catch (err: any) {
toast.error("Delete failed"); toast.error("Delete failed");
} }
}; };
@@ -128,7 +128,7 @@ export default function AdminPage() {
await inventoryApi.createCategory({ name, description: desc }); await inventoryApi.createCategory({ name, description: desc });
toast.success("Category added"); toast.success("Category added");
loadData(); loadData();
} catch (err) { } catch (err: any) {
toast.error("Failed to add category"); toast.error("Failed to add category");
} }
}; };
@@ -140,7 +140,7 @@ export default function AdminPage() {
toast.success("Category updated"); toast.success("Category updated");
setEditingCategory(null); setEditingCategory(null);
loadData(); loadData();
} catch (err) { } catch (err: any) {
toast.error("Update failed"); toast.error("Update failed");
} }
}; };
@@ -152,7 +152,7 @@ export default function AdminPage() {
await inventoryApi.deleteCategory(id); await inventoryApi.deleteCategory(id);
toast.success("Category removed"); toast.success("Category removed");
loadData(); loadData();
} catch (err) { } catch (err: any) {
toast.error(err.response?.data?.detail || "Delete failed"); toast.error(err.response?.data?.detail || "Delete failed");
} }
}; };
@@ -167,7 +167,7 @@ export default function AdminPage() {
toast.success("User updated successfully"); toast.success("User updated successfully");
setEditingUser(null); setEditingUser(null);
loadData(); loadData();
} catch (err) { } catch (err: any) {
toast.error("Update failed"); toast.error("Update failed");
} }
}; };
@@ -183,7 +183,7 @@ export default function AdminPage() {
await inventoryApi.triggerBackup(); await inventoryApi.triggerBackup();
toast.success("Snapshot created successfully"); toast.success("Snapshot created successfully");
loadData(); loadData();
} catch (err) { } catch (err: any) {
toast.error("Backup failed"); toast.error("Backup failed");
} finally { } finally {
setIsBackingUp(false); setIsBackingUp(false);
@@ -198,7 +198,7 @@ export default function AdminPage() {
await inventoryApi.restoreDatabase(filename); await inventoryApi.restoreDatabase(filename);
toast.success("Database restored! Reloading system...", { id: loadingToast }); toast.success("Database restored! Reloading system...", { id: loadingToast });
setTimeout(() => window.location.reload(), 2000); setTimeout(() => window.location.reload(), 2000);
} catch (err) { } catch (err: any) {
toast.error("Restore failed", { id: loadingToast }); toast.error("Restore failed", { id: loadingToast });
} }
}; };
@@ -208,7 +208,7 @@ export default function AdminPage() {
await inventoryApi.updateDbSettings(newSettings); await inventoryApi.updateDbSettings(newSettings);
toast.success("System policy updated"); toast.success("System policy updated");
setDbSettings(newSettings); setDbSettings(newSettings);
} catch (err) { } catch (err: any) {
toast.error("Failed to update settings"); toast.error("Failed to update settings");
} }
}; };
@@ -218,7 +218,7 @@ export default function AdminPage() {
try { try {
await inventoryApi.updateLdapConfig(ldapConfig); await inventoryApi.updateLdapConfig(ldapConfig);
toast.success("Enterprise configuration updated"); toast.success("Enterprise configuration updated");
} catch (err) { } catch (err: any) {
toast.error("Failed to update LDAP config"); toast.error("Failed to update LDAP config");
} finally { } finally {
setLoading(false); setLoading(false);

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');
@@ -79,7 +85,7 @@ export default function InventoryPage() {
// Sync local DB // Sync local DB
await db.items.clear(); await db.items.clear();
await db.items.bulkPut(res); await db.items.bulkPut(res);
} catch (err) { } catch (err: any) {
console.error("Failed to load backend data", err); console.error("Failed to load backend data", err);
} }
}; };
@@ -143,7 +149,7 @@ export default function InventoryPage() {
setIsEditing(false); setIsEditing(false);
setSelectedItem(updated as Item); setSelectedItem(updated as Item);
await loadData(); await loadData();
} catch (err) { } catch (err: any) {
console.error(err); console.error(err);
toast.error("Failed to update item"); toast.error("Failed to update item");
} }
@@ -161,7 +167,7 @@ export default function InventoryPage() {
toast.success("Item deleted"); toast.success("Item deleted");
setSelectedItem(null); setSelectedItem(null);
await loadData(); await loadData();
} catch (err) { } catch (err: any) {
console.error(err); console.error(err);
toast.error("Failed to delete item"); toast.error("Failed to delete item");
} }
@@ -177,11 +183,36 @@ export default function InventoryPage() {
toast.success("Category updated"); toast.success("Category updated");
setEditingCategory(null); setEditingCategory(null);
await loadData(); await loadData();
} catch (err) { } catch (err: any) {
toast.error("Update failed"); toast.error("Update failed");
} }
}; };
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

@@ -54,7 +54,7 @@ export default function LogsPage() {
}); });
setAuditLogs(enrichedLogs); setAuditLogs(enrichedLogs);
} catch (err) { } catch (err: any) {
console.error("Critical log load failure:", err); console.error("Critical log load failure:", err);
} finally { } finally {
setLoading(false); setLoading(false);
@@ -289,7 +289,7 @@ export default function LogsPage() {
{selectedLog.target_snapshot && (() => { {selectedLog.target_snapshot && (() => {
try { try {
const snap = JSON.parse(selectedLog.target_snapshot); const snap = JSON.parse(selectedLog.target_snapshot) as Record<string, any>;
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -299,12 +299,12 @@ export default function LogsPage() {
</div> </div>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{Object.entries(snap).map(([key, val]) => ( {Object.entries(snap).map(([key, val]) => (
val && key !== 'image_url' && ( (val && key !== 'image_url') ? (
<div key={key} className="bg-slate-950/30 p-3 rounded-xl border border-slate-800/40"> <div key={key} className="bg-slate-950/30 p-3 rounded-xl border border-slate-800/40">
<p className="text-[8px] font-black text-slate-600 uppercase mb-1">{key.replace('_', ' ')}</p> <p className="text-[8px] font-black text-slate-600 uppercase mb-1">{key.replace('_', ' ')}</p>
<p className="text-xs font-bold text-slate-300 truncate" title={String(val)}>{String(val)}</p> <p className="text-xs font-bold text-slate-300 truncate" title={String(val)}>{String(val)}</p>
</div> </div>
) ) : null
))} ))}
</div> </div>
</div> </div>

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,
@@ -47,7 +48,7 @@ import { generateBarcode128, getQRCodeURL } from '@/lib/labels';
import { clsx, type ClassValue } from 'clsx'; import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import axios from 'axios'; import axios from 'axios';
import versionData from '../../VERSION.json'; import versionData from '../VERSION.json';
interface User { interface User {
id: number; id: number;
@@ -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' });
@@ -329,7 +342,7 @@ export default function Home() {
setIsEditing(false); setIsEditing(false);
setSelectedItem(updated as Item); setSelectedItem(updated as Item);
await loadInventory(); await loadInventory();
} catch (err) { } catch (err: any) {
console.error(err); console.error(err);
toast.error("Failed to update item"); toast.error("Failed to update item");
} }
@@ -347,7 +360,7 @@ export default function Home() {
toast.success("Item deleted from catalog"); toast.success("Item deleted from catalog");
setSelectedItem(null); setSelectedItem(null);
await loadInventory(); await loadInventory();
} catch (err) { } catch (err: any) {
console.error(err); console.error(err);
toast.error("Failed to delete item"); toast.error("Failed to delete item");
} }
@@ -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>
@@ -861,7 +890,7 @@ export default function Home() {
<Printer size={14} /> Print Label <Printer size={14} /> Print Label
</button> </button>
<button <button
onClick={() => { setQuery(box.toLowerCase()); setShowBoxManager(false); }} onClick={() => { setSearchQuery(box.toLowerCase()); setShowBoxManager(false); }}
className="px-4 py-3 bg-slate-900 border border-slate-800 text-slate-400 text-xs font-bold rounded-xl hover:bg-slate-800" className="px-4 py-3 bg-slate-900 border border-slate-800 text-slate-400 text-xs font-bold rounded-xl hover:bg-slate-800"
> >
View View
@@ -975,7 +1004,7 @@ export default function Home() {
<footer className="mt-20 mb-8 flex flex-col items-center gap-2 opacity-30"> <footer className="mt-20 mb-8 flex flex-col items-center gap-2 opacity-30">
<p className="text-xs font-bold">Powered by TFM Group Software</p> <p className="text-xs font-bold">Powered by TFM Group Software</p>
<div className="h-px w-12 bg-slate-800" /> <div className="h-px w-12 bg-slate-800" />
<p className="text-[9px] font-mono">v{versionData.version} {versionData.last_build} BUILD: dev-{versionData.commit}</p> <p className="text-[9px] font-mono">v{versionData.version} {versionData.last_build} BUILD: dev-{(versionData as any).commit || 'N/A'}</p>
</footer> </footer>
</div> </div>
</PageShell> </PageShell>

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 = {
@@ -50,7 +60,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
Html5QrcodeSupportedFormats.CODE_39, Html5QrcodeSupportedFormats.CODE_39,
Html5QrcodeSupportedFormats.EAN_13, Html5QrcodeSupportedFormats.EAN_13,
Html5QrcodeSupportedFormats.UPC_A, Html5QrcodeSupportedFormats.UPC_A,
Html5QrcodeSupportedFormats.DATAMATRIX Html5QrcodeSupportedFormats.DATA_MATRIX
], ],
videoConstraints: { videoConstraints: {
facingMode: "environment" facingMode: "environment"
@@ -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]);

19
frontend/entrypoint.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
# =============================================================================
# frontend/entrypoint.sh
# =============================================================================
# Docker container entrypoint for TFM aInventory frontend.
# Fixes permissions for the logs volume and starts the Node.js server.
# =============================================================================
set -e
# Fix permissions for the logs directory (useful if mounted as a volume)
if [ -d "/app/logs" ]; then
echo "🐳 [Docker] Fixing /app/logs permissions..."
chown -R nextjs:nodejs /app/logs
fi
# Hand off to the application server as the nextjs user
echo "🐳 [Docker] Starting Next.js standalone server as nextjs user..."
exec su-exec nextjs node server.js

View File

@@ -1,20 +1,48 @@
import axios from 'axios'; import axios from 'axios';
import { getToken, clearAuth } from './auth'; import { getToken, clearAuth } from './auth';
export const getBackendUrl = () => { // Cached config to avoid repeated fetches
if (typeof window === 'undefined') return 'http://localhost:8000'; let cachedConfig: any = null;
/**
* Fetches the network configuration from the public/network.json file.
* This file is generated at startup by start_server.sh or docker-compose.
*/
export const getNetworkConfig = async () => {
if (cachedConfig) return cachedConfig;
if (typeof window === 'undefined') {
return { SERVER_IP: 'localhost', BACKEND_PORT: 8000, BACKEND_SSL_PORT: 8908 };
}
try {
const response = await fetch('/network.json');
if (!response.ok) throw new Error("Config not found");
cachedConfig = await response.json();
return cachedConfig;
} catch (e) {
console.warn("Network config not found, using compiled defaults.");
// Defaults matching the initial reserve ports in case network.json is missing
return { SERVER_IP: 'localhost', BACKEND_PORT: 8906, BACKEND_SSL_PORT: 8908 };
}
};
export const getBackendUrl = async () => {
const config = await getNetworkConfig();
if (typeof window === 'undefined') return `http://localhost:${config.BACKEND_PORT}`;
const host = window.location.hostname; const host = window.location.hostname;
// If we are on HTTPS (Proxy/Mobile mode), we use port 3002 for the backend // If we are on HTTPS (Proxy/Mobile mode), we use the SSL port for the backend
if (window.location.protocol === 'https:') { if (window.location.protocol === 'https:') {
if (host.includes('.loca.lt')) { if (host.includes('.loca.lt')) {
return 'https://inventory-ai-api.loca.lt'; return 'https://inventory-ai-api.loca.lt';
} }
return `https://${host}:3002`; return `https://${host}:${config.BACKEND_SSL_PORT}`;
} }
return `http://${host}:8000`; return `http://${host}:${config.BACKEND_PORT}`;
}; };
/** /**
@@ -23,9 +51,9 @@ export const getBackendUrl = () => {
*/ */
const axiosInstance = axios.create({}); const axiosInstance = axios.create({});
axiosInstance.interceptors.request.use((config) => { axiosInstance.interceptors.request.use(async (config) => {
if (!config.baseURL) { if (!config.baseURL) {
config.baseURL = getBackendUrl(); // called at request time — always correct config.baseURL = await getBackendUrl(); // called at request time — always correct
} }
const token = getToken(); const token = getToken();
if (token) { if (token) {
@@ -75,8 +103,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;
@@ -110,7 +138,8 @@ export const inventoryApi = {
// Users // Users
getUsers: async () => { getUsers: async () => {
// [C-01] Public endpoint — use plain axios to avoid JWT interceptor // [C-01] Public endpoint — use plain axios to avoid JWT interceptor
const res = await axios.get(`${getBackendUrl()}/users/`); const baseUrl = await getBackendUrl();
const res = await axios.get(`${baseUrl}/users/`);
return res.data; return res.data;
}, },
@@ -121,7 +150,8 @@ export const inventoryApi = {
login: async (credentials: any) => { login: async (credentials: any) => {
// [C-01] Login endpoint — NU adaug token header (login e public) // [C-01] Login endpoint — NU adaug token header (login e public)
const res = await axios.post(`${getBackendUrl()}/users/login`, credentials); const baseUrl = await getBackendUrl();
const res = await axios.post(`${baseUrl}/users/login`, credentials);
return res.data; return res.data;
}, },

1
frontend/public/sw.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,17 +7,38 @@ echo "🚀 Starting TFM aInventory in Standalone Mode..."
# Trapping termination signals to clean up child processes # Trapping termination signals to clean up child processes
trap "echo 'Stopping all processes...'; kill 0" SIGINT SIGTERM EXIT trap "echo 'Stopping all processes...'; kill 0" SIGINT SIGTERM EXIT
# --- CONFIGURATION (Match start_server.sh) --- # --- CONFIGURATION (Default values, overridden by network_config.env) ---
BACKEND_PORT=8000 BACKEND_PORT=8000
FRONTEND_PORT=3001 FRONTEND_PORT=3001
BACKEND_SSL_PORT=3002 BACKEND_SSL_PORT=3002
FRONTEND_SSL_PORT=3003 FRONTEND_SSL_PORT=3003
SERVER_IP="localhost"
# Load Configuration from file if it exists
CONFIG_PATH="$(cd "$(dirname "$0")" && pwd)/config/network_config.env"
if [ -f "$CONFIG_PATH" ]; then
echo "⚙️ Loading network configuration from $CONFIG_PATH..."
export $(grep -v '^#' "$CONFIG_PATH" | xargs)
fi
# 1. Activate Environment # 1. Activate Environment
if [ -d ".venv" ]; then if [ -d ".venv" ]; then
source .venv/bin/activate source .venv/bin/activate
fi fi
# 1.5 Sync Network Config to Frontend
echo "🔌 Syncing network configuration to frontend..."
mkdir -p frontend/public
cat <<EOF > frontend/public/network.json
{
"SERVER_IP": "$SERVER_IP",
"BACKEND_PORT": $BACKEND_PORT,
"BACKEND_SSL_PORT": $BACKEND_SSL_PORT,
"FRONTEND_PORT": $FRONTEND_PORT,
"FRONTEND_SSL_PORT": $FRONTEND_SSL_PORT
}
EOF
# 2. Start Backend (No Reload for Prod) # 2. Start Backend (No Reload for Prod)
echo "🔥 Starting Backend (Uvicorn)..." echo "🔥 Starting Backend (Uvicorn)..."
python3 -m uvicorn backend.main:app --host 0.0.0.0 --port $BACKEND_PORT & python3 -m uvicorn backend.main:app --host 0.0.0.0 --port $BACKEND_PORT &

View File

@@ -32,10 +32,10 @@ mkdir -p "$DATA_DIR"
mkdir -p "$LOGS_DIR" mkdir -p "$LOGS_DIR"
# 2. Copy LDAP config from example template if no active config exists yet # 2. Copy LDAP config from example template if no active config exists yet
# NOTE: The application reads LDAP config from backend/config/ldap_config.json # NOTE: The application reads LDAP config from config/ldap_config.json
# (see backend/routers/users.py → get_ldap_config()) # (see backend/routers/users.py → get_ldap_config())
LDAP_CONFIG="$PROJECT_ROOT/backend/config/ldap_config.json" LDAP_CONFIG="$PROJECT_ROOT/config/ldap_config.json"
LDAP_EXAMPLE="$PROJECT_ROOT/backend/config/ldap_config.json.example" LDAP_EXAMPLE="$PROJECT_ROOT/config/ldap_config.json.example"
if [ ! -f "$LDAP_CONFIG" ]; then if [ ! -f "$LDAP_CONFIG" ]; then
if [ -f "$LDAP_EXAMPLE" ]; then if [ -f "$LDAP_EXAMPLE" ]; then

View File

@@ -4,7 +4,7 @@ import os
import sys import sys
from datetime import datetime from datetime import datetime
VERSION_FILE = 'VERSION.json' VERSION_FILE = 'frontend/VERSION.json'
GIT_PATH_FILE = '.git_path' GIT_PATH_FILE = '.git_path'
def get_git_path(): def get_git_path():
@@ -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,17 +34,34 @@ 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
data['last_build'] = datetime.now().strftime("%Y-%m-%d-%H%M") data['last_build'] = datetime.now().strftime("%Y-%m-%d-%H%M")
# Get current git commit (short)
try:
commit_hash = run_command([git, 'rev-parse', '--short', 'HEAD'])
data['commit'] = commit_hash
except Exception:
data['commit'] = 'unknown'
# Optional: Rotate changelog if needed, but for now just update version # Optional: Rotate changelog if needed, but for now just update version
print(f"Incrementing version: {old_version} -> {new_version}") print(f"Incrementing version: {old_version} -> {new_version} (commit: {data['commit']})")
with open(VERSION_FILE, 'w') as f: with open(VERSION_FILE, 'w') as f:
json.dump(data, f, indent=2) json.dump(data, f, indent=2)
@@ -57,11 +74,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__":

View File

@@ -1,10 +1,19 @@
#!/bin/bash #!/bin/bash
# --- CONFIGURATION --- # --- CONFIGURATION (Default values, will be overridden by network_config.env) ---
BACKEND_PORT=8000 BACKEND_PORT=8000
FRONTEND_PORT=3001 FRONTEND_PORT=3001
BACKEND_SSL_PORT=3002 BACKEND_SSL_PORT=3002
FRONTEND_SSL_PORT=3003 FRONTEND_SSL_PORT=3003
SERVER_IP="localhost"
# Load Configuration from file if it exists
CONFIG_PATH="$(cd "$(dirname "$0")" && pwd)/config/network_config.env"
if [ -f "$CONFIG_PATH" ]; then
echo "⚙️ Loading network configuration from $CONFIG_PATH..."
# Export variables from .env file (ignoring comments and empty lines)
export $(grep -v '^#' "$CONFIG_PATH" | xargs)
fi
# Add common Mac paths for npm/node # Add common Mac paths for npm/node
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
@@ -39,6 +48,18 @@ export LOGS_DIR="$(cd "$(dirname "$0")" && pwd)/logs"
echo "🔧 Initializing runtime data directories..." echo "🔧 Initializing runtime data directories..."
bash "$(cd "$(dirname "$0")" && pwd)/scripts/init_data.sh" bash "$(cd "$(dirname "$0")" && pwd)/scripts/init_data.sh"
# 4.1 Sync Network Config to Frontend for runtime discovery
echo "🔌 Syncing network configuration to frontend..."
cat <<EOF > frontend/public/network.json
{
"SERVER_IP": "$SERVER_IP",
"BACKEND_PORT": $BACKEND_PORT,
"BACKEND_SSL_PORT": $BACKEND_SSL_PORT,
"FRONTEND_PORT": $FRONTEND_PORT,
"FRONTEND_SSL_PORT": $FRONTEND_SSL_PORT
}
EOF
echo "🔥 Starting Backend on port $BACKEND_PORT..." echo "🔥 Starting Backend on port $BACKEND_PORT..."
echo " CORS origins: $ALLOWED_ORIGINS" echo " CORS origins: $ALLOWED_ORIGINS"
python3 -m uvicorn backend.main:app --host 0.0.0.0 --port $BACKEND_PORT --reload & python3 -m uvicorn backend.main:app --host 0.0.0.0 --port $BACKEND_PORT --reload &
@@ -67,8 +88,8 @@ echo -e "${GREEN}${BOLD} 🚀 TFM aInventory UNIFIED ACCESS${NC}"
echo -e "${GREEN}=======================================================${NC}" echo -e "${GREEN}=======================================================${NC}"
echo "" echo ""
echo -e " USE THIS URL ON BOTH DESKTOP & MOBILE:" echo -e " USE THIS URL ON BOTH DESKTOP & MOBILE:"
echo -e " 👉 ${GREEN}${BOLD}https://$LOCAL_IP:3003${NC}" echo -e " 👉 ${GREEN}${BOLD}https://$LOCAL_IP:$FRONTEND_SSL_PORT${NC}"
echo -e " (Or ${GREEN}https://localhost:3003${NC} on this Mac)" echo -e " (Or ${GREEN}https://localhost:$FRONTEND_SSL_PORT${NC} on this Mac)"
echo "" echo ""
echo -e " ${YELLOW}${BOLD}NOTE:${NC} If you see a 'Not Private' warning," echo -e " ${YELLOW}${BOLD}NOTE:${NC} If you see a 'Not Private' warning,"
echo -e " Click 'Advanced' -> 'Proceed' to continue." echo -e " Click 'Advanced' -> 'Proceed' to continue."