Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f137ded5aa | ||
|
|
946f1787c7 | ||
|
|
09be0b401d | ||
|
|
3069a921e7 | ||
|
|
55e3cf5042 | ||
|
|
c874d27d64 | ||
|
|
939ad8648d | ||
|
|
6f6caf3c5a | ||
|
|
5e648002f0 | ||
|
|
81b775c9ae | ||
|
|
994920bda2 | ||
|
|
0a6368b9f6 | ||
|
|
26e8f034a2 |
121
AI_RULES.md
121
AI_RULES.md
@@ -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.**
|
||||
This is the **Single Source of Truth** for ALL Artificial Intelligence agents (Claude, Gemini, etc.) working on this project.
|
||||
(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).
|
||||
This is the **Single Source of Truth** for ALL AI agents. Refer to [PROJECT_ARCHITECTURE.md](PROJECT_ARCHITECTURE.md) for technical logic.
|
||||
|
||||
## 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**.
|
||||
- **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.
|
||||
---
|
||||
|
||||
## 1. AI MEMORY, TRACEABILITY & HANDOVER
|
||||
- **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.
|
||||
|
||||
## 2. Global 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.
|
||||
- **COMMUNICATION LANGUAGE**: Conversation with the user will be in Romanian or English (preferably Romanian).
|
||||
- **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.
|
||||
- **COMMIT & PUSH STRICT RULE**:
|
||||
- Never push to remote (`git push`) or use force flags (`--hard`, `--force`) unless explicitly requested.
|
||||
- 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.
|
||||
## 2. ENGINEERING & OPERATIONAL LAWS
|
||||
- **ENGLISH ONLY**: Interfaces, code, variables, and docs MUST be in English. Translate any Romanian text found in code immediately. (User conversation: Romanian/English).
|
||||
- **GIT PROTOCOL**: Use the direct binary path in `.git_path` (`/Library/Developer/CommandLineTools/usr/bin/git`) for ALL Git operations. **DO NOT REMOVE OR CHANGE THIS PATH UNDER ANY CIRCUMSTANCES!** Never push or use `--force` unless explicitly asked. Branching: `master` (stable), `dev` (active), `vX` (archive).
|
||||
- **VERSIONING**: Update `VERSION.json` on every commit. Use `scripts/save_version.py` for automated releases.
|
||||
- **DEPENDENCIES**: Update `backend/requirements.txt` with version constraints for every new pip package.
|
||||
- **SSOT INTEGRITY**: Every feature change MUST update: `README.md`, `USER_GUIDE.md`, `PROJECT_ARCHITECTURE.md`, and `export_prod.sh`.
|
||||
|
||||
## 3. UI/UX Fidelity Specifications
|
||||
- **Fidelity First**: Never simplify the UI unless asked. Density and aesthetics must remain "Premium".
|
||||
- **Styling**: Tailwind CSS. Font weights/spacing must be consistent (Inter/Roboto).
|
||||
- **Readability**: NO `uppercase` or `tracking-widest` styles. Use standard camel/Title case.
|
||||
- **Icons**: Use **Lucide Icons** exclusively. NO emojis.
|
||||
- **Interactive Affordance**: All select/dropdown boxes MUST have a `ChevronDown` icon. Masked passwords should have reduced opacity (`text-white/50`).
|
||||
- **Iconography Standardization**:
|
||||
- **Categories**: ALWAYS use `Layers` (Color: `text-primary`).
|
||||
- **Item Types**: ALWAYS use `Package` (Color: `text-green-500`).
|
||||
## 3. UI/UX "PREMIUM" FIDELITY STANDARDS
|
||||
- **Aesthetics**: Density/aesthetics must remain "Premium". Use Tailwind CSS. NO simplification.
|
||||
- **Typography Rules**:
|
||||
- **NO UPPERCASE** or **NO ITALICS** in headers, labels, buttons, or metadata.
|
||||
- **NO `tracking-widest`**. Use standard camel/Title case.
|
||||
- Use `font-black` for main headings.
|
||||
- **Layout**: Main pages MUST use `max-w-7xl`.
|
||||
- **Unified Headers**: Icon box (`p-4 bg-primary/10 border-primary/20`) + Title (`text-3xl font-black`) + Subtitle (`text-xs text-slate-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
|
||||
- **SSOT INTEGRITY**: Whenever a feature is added, modified, or removed, you MUST update all corresponding documentation files:
|
||||
- `README.md` (General usage & technical modes).
|
||||
- `USER_GUIDE.md` (End-user instructions).
|
||||
- `PROJECT_ARCHITECTURE.md` (Technical logic & data models).
|
||||
- `export_prod.sh` (If new scripts or files must be included in the production bundle).
|
||||
- **REAL-TIME UPDATES**: Documentation updates are NOT optional and must be performed within the same session as the code changes.
|
||||
## 4. DATA INTEGRITY & AUDIT POLICY
|
||||
- **RESTRICTED ACTIONS**: `DELETE /items/` and Admin settings require `auth.get_current_admin`.
|
||||
- **AUDIT IMMUTABILITY**: Deleting an `Item` MUST NOT delete its `AuditLog` entries.
|
||||
- **TRACEABILITY**: Log deletions to `logs/backend.log` with `USER[id]`, `ITEM[id]`, `Name`, `PN`.
|
||||
- **CONFIRMATION**:
|
||||
- **Triple Confirmation**: Deleting critical entities (Locations/Items) requires user confirmation 3 times.
|
||||
- **Native Alerts**: Use `window.confirm` for all destructive UI actions and Logout.
|
||||
|
||||
## 6. AI Command Shortcuts
|
||||
- **`save-version`**: When the user triggers this command, the AI MUST:
|
||||
1. Increment the patch version in `VERSION.json`.
|
||||
2. Stage all current changes (`git add .`).
|
||||
3. Commit changes with message `Build [vX.Y.Z]`.
|
||||
4. Create a new branch named `vX.Y.Z` from the current state.
|
||||
5. Generate a production bundle ZIP (calls `./export_prod.sh`).
|
||||
6. Stay on the current branch (`dev`).
|
||||
- *Implementation*: Use `python3 scripts/save_version.py` to ensure consistency.
|
||||
## 5. AI COMMAND SHORTCUTS
|
||||
- **`save-version`**:
|
||||
0. **MANDATORY**: Verify and update ALL documentation (`.md` files: README, USER_GUIDE, ARCHITECTURE, etc.) with explanations of all current changes.
|
||||
1. Increment `VERSION.json`.
|
||||
2. Git add/commit (`Build [vX.Y.Z]`).
|
||||
3. Create branch `vX.Y.Z` (Snapshot).
|
||||
4. Automatic Sync: Merge changes into `master` branch to keep it up-to-date.
|
||||
5. Run `./export_prod.sh`.
|
||||
(Always use `python3 scripts/save_version.py`).
|
||||
|
||||
## 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
|
||||
- **Immutability of Logs**: Deleting an `Item` MUST NOT delete its corresponding entries in the `AuditLog` table in the database.
|
||||
- **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.
|
||||
|
||||
|
||||
## 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.
|
||||
## END OF SESSION PROTOCOL
|
||||
End your final response on a separate line exactly with:
|
||||
```
|
||||
---
|
||||
✓ Done.
|
||||
```
|
||||
|
||||
@@ -24,8 +24,9 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
|
||||
|
||||
### 2.3 Operations & Tooling
|
||||
- **PWA Deployment:** `next-pwa` (Service Workers + Manifest.json)
|
||||
- **HTTPS Proxy:** `local-ssl-proxy` (Required for mobile camera access, Port 3003)
|
||||
- **Servers:** Frontend (`npm run dev` on 3000), Backend (`./start_server.sh` on 8000)
|
||||
- **HTTPS Proxy:** `caddy` or `local-ssl-proxy` (Port 8909)
|
||||
- **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
|
||||
- **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
|
||||
- **Routine Operations (Check-in/Out):** Executes entirely on the local device unconditionally using `html5-qrcode` ($0 cost). No AI is allowed here.
|
||||
- **New Item Onboarding (AI Label OCR):** Uses cloud AI (`gemini-2.0-flash`). The user takes a photo, AI extracts data based on strict templates.
|
||||
- **AI Box Discovery Mode (v1.6.0):** Supports specialized `mode="box"` prompt that focuses exclusively on prominent container names/hand-written labels, ignoring technical spec noise.
|
||||
- **Validation Mask:** AI-extracted data is NEVER saved directly. It is presented in a validation UI for human confirmation.
|
||||
|
||||
### 4.2 Scanner Technical Specs
|
||||
@@ -48,6 +50,7 @@ A unified system to maintain an inventory of "items" and their quantities, inclu
|
||||
- Noise Filtering: Ignores `< 3` chars, decimals, and dates.
|
||||
- Scoring: Exact S/N (+500), Exact P/N (+200), Token match (+50), Category match (+20).
|
||||
- Threshold: Minimum **40 points** for auto-match without user intervention.
|
||||
- **Targeted Field Scanning (v1.6.0):** UI allows "locking" the scanner focus to a specific input field (e.g., `box_label`). The OCR result is then redirected to state without performing regular item lookup.
|
||||
|
||||
### 4.3 Box Labeling & Printing System (v1.5.0)
|
||||
- **Local OCR Priority:** Before checking individual S/Ns, the matching engine searches for `box_label` tokens. If a box is identified:
|
||||
@@ -83,7 +86,12 @@ To ensure enterprise-grade protection, the following policies are enforced:
|
||||
### 7.3 Data Privacy
|
||||
- **Information Scrubbing:** Backend logs are configured to intercept and mask sensitive auth tokens or internal secrets (e.g., `JWT_SECRET_KEY`) during debug output.
|
||||
- **Direct Bind LDAP:** Authentication uses direct user binding to the LDAP server, avoiding the need for a privileged service account with broad search permissions.
|
||||
- **Cryptographic Credential Caching:** To support offline operations, the system caches a **PBKDF2-HMAC-SHA256 hash** of the user's Enterprise credentials upon successful online login. Plain text passwords are NEVER stored.
|
||||
|
||||
### 7.4 PWA Trust & Security
|
||||
- **HTTPS Enforcement:** The system requires TLS (Port 3003) for camera access and secure token transmission.
|
||||
- **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).
|
||||
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.
|
||||
|
||||
16
README.md
16
README.md
@@ -12,21 +12,21 @@ This project supports three distinct operational modes:
|
||||
Ideal for local development on macOS/Linux.
|
||||
* **Command:** `./start_server.sh`
|
||||
* **Details:** Runs FastAPI (backend) and Next.js (frontend) in development mode. Uses `local-ssl-proxy` for HTTPS.
|
||||
* **Backend:** http://localhost:8000
|
||||
* **Frontend:** https://localhost:3003
|
||||
* **Backend:** http://localhost:8906
|
||||
* **Frontend:** https://localhost:8909
|
||||
|
||||
### 2. 🐳 Docker Mode (Recommended for Production)
|
||||
Isolated and portable container stack.
|
||||
* **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`.
|
||||
* **Access:** https://localhost:3003
|
||||
* **Access:** https://localhost:8909
|
||||
|
||||
### 3. 🐧 Standalone Linux Mode (Systemd)
|
||||
Native Linux installation (Alma/Debian/Ubuntu) without Docker dependencies.
|
||||
* **Installation:** `sudo ./install_service.sh`
|
||||
* **Execution:** `sudo systemctl start inventory`
|
||||
* **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:
|
||||
1. Use the AI shortcut command: `save-version`.
|
||||
2. Alternatively, run `./export_prod.sh` manually.
|
||||
3. A `.zip` archive will be created (e.g., `aInventory-PROD-v1.3.6.zip`).
|
||||
3. A `.zip` archive will be created (e.g., `aInventory-PROD-v1.7.0.zip`).
|
||||
4. A backup branch `v.1.3.x` will be created automatically.
|
||||
|
||||
---
|
||||
@@ -77,6 +77,12 @@ export ALLOWED_ORIGINS="https://your-domain.com"
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
@@ -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).
|
||||
- **Change Password:** We recommend changing your password immediately from the Admin settings.
|
||||
- **LDAP/Enterprise Login:** If your administrator has configured LDAP integration, you can log in with your company/domain account. The application will cache your password locally to allow offline access (e.g., in areas without signal like basements).
|
||||
- **LDAP/Enterprise Login:** If your administrator has configured LDAP integration, you can log in with your company/domain account. The application will securely cache a **cryptographic hash** of your credentials (using PBKDF2) to allow offline access (e.g., in areas without signal like basements). **Note: Your actual password is NEVER stored in plain text on the local device.**
|
||||
- **JWT Tokens:** Your login session is secured with JWT bearer tokens that expire after 8 hours. You will be automatically logged out when your token expires.
|
||||
|
||||
---
|
||||
@@ -33,11 +33,12 @@ Scan an existing barcode to locate or update an item in your inventory.
|
||||
|
||||
If no readable text is found, the scanner silently retries on the next cycle.
|
||||
|
||||
### Box & Container Scanning (NEW v1.5.0)
|
||||
You can now scan a generic label on a box (e.g., "SFP Box 1") to identify all its contents at once.
|
||||
- If the box contains **one type of item**, the application takes you directly to the stock adjustment screen.
|
||||
- If the box contains **multiple items**, a list will appear for you to select which specific item you are withdrawing or adding.
|
||||
- This feature works **locally and offline** - no AI costs for scanning boxes.
|
||||
### Box & Container Scanning (NEW v1.6.0)
|
||||
You can now manage containers more efficiently with two specialized methods:
|
||||
|
||||
- **AI Box Discovery**: When adding a new container through **AI Discovery**, use the **"Box / Container"** toggle. Gemini will focus exclusively on the container's name, ignoring technical noise on labels.
|
||||
- **Targeted Field Scanning**: In the **Edit Item** modal, tap the small **Camera icon** next to the "Box / Container Label" field. The scanner will capture the next physical label directly into the text field.
|
||||
- **Automatic Matching**: In the main scanner, scanning a box identifies all its contents. Scanning a box and then an item will suggest linking them together if they aren't already matched.
|
||||
|
||||
---
|
||||
|
||||
@@ -99,6 +100,11 @@ If your organization uses LDAP/Active Directory, administrators can:
|
||||
### Settings
|
||||
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
|
||||
@@ -140,5 +146,5 @@ For detailed technical documentation, see the [Project Architecture](../PROJECT_
|
||||
|
||||
---
|
||||
|
||||
**Version:** v1.5.0
|
||||
**Last Updated:** 2026-04-12
|
||||
**Version:** v1.8.4
|
||||
**Last Updated:** 2026-04-13
|
||||
|
||||
12
VERSION.json
12
VERSION.json
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
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 \
|
||||
build-essential \
|
||||
libldap2-dev \
|
||||
libsasl2-dev \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
@@ -26,14 +27,13 @@ COPY scripts ./scripts
|
||||
ENV DATA_DIR="/app/data"
|
||||
ENV LOGS_DIR="/app/logs"
|
||||
|
||||
# Ensure the appuser can write to data and logs if we pre-create them,
|
||||
# although Docker volumes will handle ownership context.
|
||||
# Pre-create directories and ensure they are writable
|
||||
RUN mkdir -p /app/data /app/logs && chown -R appuser:appuser /app
|
||||
|
||||
# Make initialization scripts executable
|
||||
RUN chmod +x /app/scripts/init_data.sh /app/backend/entrypoint.sh
|
||||
|
||||
USER appuser
|
||||
EXPOSE 8000
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
|
||||
@@ -7,36 +7,55 @@ base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
dotenv_path = os.path.join(base_dir, ".env")
|
||||
load_dotenv(dotenv_path)
|
||||
|
||||
def extract_label_info(image_bytes: bytes):
|
||||
def extract_label_info(image_bytes: bytes, mode: str = "item"):
|
||||
"""
|
||||
Orchestrates extraction across multiple AI providers.
|
||||
Order: Gemini (Flash/Pro) -> Claude (Haiku/Sonnet)
|
||||
"""
|
||||
prompt = """
|
||||
Extract technical inventory information from this label image.
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
1. Look at the most prominent text (usually top 1-2 rows). This is the product NAME and MODEL.
|
||||
2. Extract the PART NUMBER (P/N, Model No, Type). If no explicit Part Number is found, synthesize one from the most unique identifier in the header (e.g. 'OM4-MMF-DX').
|
||||
3. Separate the COLOR (e.g. Turquoise, Yellow, Black).
|
||||
4. Extract CATEGORY based on the item type (e.g. Patchcord, SFP, Connector).
|
||||
5. Extract technical SPECS (e.g. '2.0mm', '10G', '850nm').
|
||||
|
||||
Return ONLY a valid JSON object:
|
||||
{
|
||||
"name": "Full descriptive name from header",
|
||||
"part_number": "Unique identifier for fast scanning",
|
||||
"category": "Broad category",
|
||||
"color": "Color if present",
|
||||
"specs": "Brief tech specs list",
|
||||
"barcode": "Barcode value if visible",
|
||||
"quantity": 1
|
||||
}
|
||||
Modes: 'item' (full technical extraction), 'box' (container discovery)
|
||||
"""
|
||||
if mode == "box":
|
||||
prompt = """
|
||||
Identify the CONTAINER or BOX name from this image.
|
||||
Look for large, prominent, bold, or hand-written text that identifies a storage unit.
|
||||
Ignore small technical details, quantities, or fine print.
|
||||
|
||||
Return ONLY a valid JSON object:
|
||||
{
|
||||
"box_label": "The identified container name",
|
||||
"name": "Same as box_label",
|
||||
"category": "Storage",
|
||||
"specs": "Brief description if useful",
|
||||
"quantity": 1
|
||||
}
|
||||
"""
|
||||
else:
|
||||
prompt = """
|
||||
Extract technical inventory information from this label image.
|
||||
|
||||
CRITICAL INSTRUCTIONS:
|
||||
1. Look at the most prominent text (usually top 1-2 rows). This is the product NAME and MODEL.
|
||||
2. Extract the PART NUMBER (P/N, Model No, Type). If no explicit Part Number is found, synthesize one from the most unique identifier in the header (e.g. 'OM4-MMF-DX').
|
||||
3. Separate the COLOR (e.g. Turquoise, Yellow, Black).
|
||||
4. Extract CATEGORY based on the item type (e.g. Patchcord, SFP, Connector).
|
||||
5. Extract technical SPECS (e.g. '2.0mm', '10G', '850nm').
|
||||
|
||||
Return ONLY a valid JSON object:
|
||||
{
|
||||
"name": "Full descriptive name from header",
|
||||
"part_number": "Unique identifier for fast scanning",
|
||||
"category": "Broad category",
|
||||
"color": "Color if present",
|
||||
"specs": "Brief tech specs list",
|
||||
"barcode": "Barcode value if visible",
|
||||
"quantity": 1
|
||||
}
|
||||
"""
|
||||
|
||||
# 1. Try Gemini
|
||||
result = gemini.extract(image_bytes, prompt)
|
||||
if result:
|
||||
# Maintenance: Ensure fields are mapped if mode was box
|
||||
if mode == "box" and "box_label" in result and "name" not in result:
|
||||
result["name"] = result["box_label"]
|
||||
return result
|
||||
|
||||
# 2. Try Claude (Fallback)
|
||||
|
||||
@@ -23,6 +23,10 @@ export LOGS_DIR="${LOGS_DIR:-/app/logs}"
|
||||
echo "🐳 [Docker] Running data initialization..."
|
||||
bash /app/scripts/init_data.sh
|
||||
|
||||
# Hand off to the application server
|
||||
echo "🐳 [Docker] Starting uvicorn..."
|
||||
exec python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000
|
||||
# Fix permissions for mounted volumes (which might be root-owned by the host)
|
||||
echo "🐳 [Docker] Fixing volume permissions..."
|
||||
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
|
||||
|
||||
@@ -24,7 +24,7 @@ log.info("TFM aInventory API process started.")
|
||||
# Secure fallback: localhost only for development.
|
||||
_raw_origins = os.environ.get(
|
||||
"ALLOWED_ORIGINS",
|
||||
"http://localhost:3000,http://localhost:3002"
|
||||
"http://localhost:8907,https://localhost:8909"
|
||||
)
|
||||
ALLOWED_ORIGINS = [o.strip() for o in _raw_origins.split(",") if o.strip()]
|
||||
log.info(f"CORS allowed origins: {ALLOWED_ORIGINS}")
|
||||
|
||||
@@ -66,6 +66,7 @@ _MAX_IMAGE_SIZE = 10 * 1024 * 1024 # 10 MB
|
||||
async def extract_label(
|
||||
request: Request,
|
||||
file: UploadFile = File(...),
|
||||
mode: str = "item", # 'item' or 'box'
|
||||
current_user: auth.TokenData = Depends(auth.get_current_user)
|
||||
):
|
||||
"""[C-01] Extract label from image — only for authenticated users. [H-02] Rate limit: 10 req/min per IP."""
|
||||
@@ -86,7 +87,7 @@ async def extract_label(
|
||||
detail="File exceeds 10MB limit."
|
||||
)
|
||||
|
||||
result = extract_label_info(contents)
|
||||
result = extract_label_info(contents, mode=mode)
|
||||
return result
|
||||
|
||||
@router.post("/", response_model=schemas.Item, status_code=status.HTTP_201_CREATED)
|
||||
|
||||
@@ -20,8 +20,9 @@ limiter = Limiter(key_func=get_remote_address)
|
||||
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
|
||||
|
||||
def get_ldap_config():
|
||||
# Read from backend/config/ directory
|
||||
config_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config")
|
||||
# Read from root /config directory
|
||||
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")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r") as f:
|
||||
@@ -314,7 +315,8 @@ def update_ldap_settings(
|
||||
current_user: auth.TokenData = Depends(auth.get_current_admin)
|
||||
):
|
||||
"""[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)
|
||||
config_path = os.path.join(config_dir, "ldap_config.json")
|
||||
with open(config_path, "w") as f:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# TFM aInventory - Caddy Self-Signed Internal Proxy
|
||||
# This replaces the need for `local-ssl-proxy` in Node.
|
||||
|
||||
:3003 {
|
||||
:{$FRONTEND_SSL_PORT} {
|
||||
tls internal
|
||||
reverse_proxy frontend:3000
|
||||
}
|
||||
|
||||
:3002 {
|
||||
:{$BACKEND_SSL_PORT} {
|
||||
tls internal
|
||||
reverse_proxy backend:8000
|
||||
}
|
||||
@@ -17,8 +17,8 @@ JWT_SECRET_KEY=change-me-generate-a-secure-random-value
|
||||
# --- CORS ---
|
||||
# Comma-separated list of allowed frontend origins
|
||||
# Example for LAN deployment:
|
||||
# ALLOWED_ORIGINS=http://192.168.1.100:3000,https://192.168.1.100:3003
|
||||
ALLOWED_ORIGINS=http://localhost:3000,https://localhost:3003
|
||||
# ALLOWED_ORIGINS=http://192.168.84.113:8907,https://192.168.84.113:8909
|
||||
ALLOWED_ORIGINS=http://localhost:8907,https://localhost:8909
|
||||
|
||||
# --- Data Paths (overridden by start_server.sh / docker-compose) ---
|
||||
# DATA_DIR=/absolute/path/to/data
|
||||
19
config/ldap_config.json
Normal file
19
config/ldap_config.json
Normal 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
28
config/network_config.env
Normal 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
|
||||
@@ -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
|
||||
**Purpose:** Implementation of security audit recommendations, REST API test suite, PWA asset generation, and visual UI refinements using Modern CSS.
|
||||
**Actions:**
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
**Active AI:** Gemini (Antigravity)
|
||||
**Last Updated:** 2026-04-12
|
||||
**Current Version:** v1.4.0
|
||||
**Current Version:** v1.6.0 (BoxMaster)
|
||||
**Branch:** dev
|
||||
|
||||
---
|
||||
|
||||
## STATUS: 🟢 STABLE — BOX MANAGEMENT & LABEL PRINTING COMPLETE (v1.5.0)
|
||||
## STATUS: 🟢 STABLE — ADVANCED BOX MANAGEMENT & AI MODES COMPLETE (v1.6.0)
|
||||
|
||||
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.
|
||||
- **Hybrid Printing** — Implemented CSS `@media print` for professional desktop printers and "Save as PNG" rasterization for portable Bluetooth printers on mobile.
|
||||
|
||||
### 4. UI/UX Excellence
|
||||
- **Search Integration** — Integrated `box_label` into the global search filter.
|
||||
- **Onboarding Support** — Added Box Label association to the AI-powered onboarding workflow with smart `datalist` suggestions.
|
||||
- **Visual Polish** — Applied Lucide `Package` iconography and consistent branding to the new modules.
|
||||
### 4. UI/UX: Targeted Field Scanning
|
||||
- **Camera Capture** — Added a dedicated scan button in Edit modals that redirects OCR results directly to the "Box Label" field without performing general item matches.
|
||||
|
||||
### 5. Multi-Mode AI Discovery
|
||||
- **Contextual Prompts** — Implemented a dual-mode toggle (Item/Box) in the AI Onboarding screen.
|
||||
- **Box Extraction** — Created a specialized prompt for Gemini 2.0 Flash to extract container names while filtering out technical noise from product labels.
|
||||
|
||||
### 6. Operational Rigor: Step 0 Rule
|
||||
- **Mandatory Documentation** — Updated `AI_RULES.md` to force documentation verification before any `save-version` (git commit) operation.
|
||||
- **Master Branch Sync** — Confirmed `scripts/save_version.py` logic to keep `master` branch in sync with the latest releases automatically.
|
||||
|
||||
---
|
||||
|
||||
@@ -50,15 +56,20 @@ The TFM aInventory v1.5.0 has been upgraded with a powerful local-first Box/Cont
|
||||
## SYSTEM STATE
|
||||
|
||||
**Active database:** `<project_root>/data/inventory.db`
|
||||
**LDAP config:** `backend/config/ldap_config.json`
|
||||
**Production Bundle:** `aInventory-PROD-v1.4.0.zip` (Clean)
|
||||
**LDAP config:** `config/ldap_config.json`
|
||||
**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:**
|
||||
```bash
|
||||
./start_server.sh
|
||||
```
|
||||
- Frontend: `https://<LOCAL_IP>:3003`
|
||||
- Backend: `https://<LOCAL_IP>:3002`
|
||||
- Frontend: `https://192.168.84.113:8909`
|
||||
- Backend: `https://192.168.84.113:8908`
|
||||
|
||||
**Environment variables set by start_server.sh:**
|
||||
- `ALLOWED_ORIGINS` — auto-detected
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
@@ -8,7 +6,9 @@ services:
|
||||
networks:
|
||||
- inventory_net
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- ${BACKEND_PORT:-8000}:8000
|
||||
env_file:
|
||||
- ./config/network_config.env
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
- ./logs:/app/logs
|
||||
@@ -16,20 +16,20 @@ services:
|
||||
environment:
|
||||
- DATA_DIR=/app/data
|
||||
- LOGS_DIR=/app/logs
|
||||
# [M-01] CORS allowed origins — customize for production
|
||||
- ALLOWED_ORIGINS=http://localhost:3000,http://localhost:3002
|
||||
- ALLOWED_ORIGINS=http://localhost:${FRONTEND_PORT:-3001},https://localhost:${FRONTEND_SSL_PORT:-3003},http://${SERVER_IP:-localhost}:${FRONTEND_PORT:-3001},https://${SERVER_IP:-localhost}:${FRONTEND_SSL_PORT:-3003},https://localhost:${BACKEND_SSL_PORT:-3002},https://${SERVER_IP:-localhost}:${BACKEND_SSL_PORT:-3002}
|
||||
# [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
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: frontend/Dockerfile
|
||||
context: ./frontend
|
||||
networks:
|
||||
- inventory_net
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- ${FRONTEND_PORT:-3000}:3000
|
||||
env_file:
|
||||
- ./config/network_config.env
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
# Write Next.js logs to both stdout (docker logs) and file (mapped volume)
|
||||
@@ -41,10 +41,12 @@ services:
|
||||
networks:
|
||||
- inventory_net
|
||||
ports:
|
||||
- "3002:3002"
|
||||
- "3003:3003"
|
||||
- ${BACKEND_SSL_PORT:-3002}:${BACKEND_SSL_PORT:-3002}
|
||||
- ${FRONTEND_SSL_PORT:-3003}:${FRONTEND_SSL_PORT:-3003}
|
||||
env_file:
|
||||
- ./config/network_config.env
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile
|
||||
- ./config/Caddyfile:/etc/caddy/Caddyfile
|
||||
# Persist the internal Caddy certificates so users don't get new certificate warnings constantly
|
||||
- ./data/caddy_data:/data
|
||||
- ./data/caddy_config:/config
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
echo "📦 Preparing TFM aInventory Production Bundle..."
|
||||
|
||||
# Extract version from VERSION.json using grep to avoid macOS python/xcode stubs
|
||||
VERSION=$(grep '"version"' VERSION.json | head -n 1 | awk -F '"' '{print $4}')
|
||||
# Extract version from frontend/VERSION.json
|
||||
VERSION=$(grep '"version"' frontend/VERSION.json | head -n 1 | awk -F '"' '{print $4}')
|
||||
PROD_DIR="aInventory-PROD-v${VERSION}"
|
||||
|
||||
# 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 '__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 Caddyfile "$PROD_DIR/"
|
||||
rsync -a config/ "$PROD_DIR/config/"
|
||||
rsync -a scripts/ "$PROD_DIR/scripts/"
|
||||
cp start_server.sh "$PROD_DIR/"
|
||||
cp run_standalone.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 README.md "$PROD_DIR/INSTALLATION_GUIDE.md"
|
||||
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
|
||||
mkdir -p "$PROD_DIR/data"
|
||||
@@ -44,7 +47,7 @@ TO RUN VIA DOCKER (Recommended):
|
||||
1. Install Docker Desktop or Docker Engine.
|
||||
2. Run: docker-compose build
|
||||
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):
|
||||
1. sudo ./install_service.sh
|
||||
@@ -54,7 +57,7 @@ TO RUN BARE-METAL (No Docker):
|
||||
1. Install Python 3.12+ and Node.js 20+.
|
||||
2. Ensure you have network access for npm installs.
|
||||
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.
|
||||
EOF
|
||||
|
||||
@@ -19,6 +19,7 @@ RUN npm run build
|
||||
|
||||
# Step 3: Production image
|
||||
FROM base AS runner
|
||||
RUN apk add --no-cache su-exec
|
||||
WORKDIR /app
|
||||
|
||||
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/static ./.next/static
|
||||
|
||||
USER nextjs
|
||||
# Copy entrypoint script
|
||||
COPY entrypoint.sh /app/entrypoint.sh
|
||||
RUN chmod +x /app/entrypoint.sh
|
||||
|
||||
EXPOSE 3000
|
||||
ENV PORT 3000
|
||||
ENV HOSTNAME "0.0.0.0"
|
||||
|
||||
# Note: The server.js is created by next build from the standalone output
|
||||
CMD ["node", "server.js"]
|
||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||
|
||||
|
||||
5
frontend/VERSION.json
Normal file
5
frontend/VERSION.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1.8.9",
|
||||
"last_build": "2026-04-13-1954",
|
||||
"codename": "TypeFix"
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export default function AdminPage() {
|
||||
setBackups(b);
|
||||
setDbStats(s);
|
||||
setDbSettings(st);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
toast.error("Failed to load admin data");
|
||||
} finally {
|
||||
@@ -101,7 +101,7 @@ export default function AdminPage() {
|
||||
await inventoryApi.createUser({ username: name, password: pwd, role: 'user' });
|
||||
toast.success("User created successfully");
|
||||
loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to create user");
|
||||
}
|
||||
};
|
||||
@@ -114,7 +114,7 @@ export default function AdminPage() {
|
||||
await inventoryApi.deleteUser(id);
|
||||
toast.success("User removed");
|
||||
loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error("Delete failed");
|
||||
}
|
||||
};
|
||||
@@ -128,7 +128,7 @@ export default function AdminPage() {
|
||||
await inventoryApi.createCategory({ name, description: desc });
|
||||
toast.success("Category added");
|
||||
loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to add category");
|
||||
}
|
||||
};
|
||||
@@ -140,7 +140,7 @@ export default function AdminPage() {
|
||||
toast.success("Category updated");
|
||||
setEditingCategory(null);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error("Update failed");
|
||||
}
|
||||
};
|
||||
@@ -152,7 +152,7 @@ export default function AdminPage() {
|
||||
await inventoryApi.deleteCategory(id);
|
||||
toast.success("Category removed");
|
||||
loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error(err.response?.data?.detail || "Delete failed");
|
||||
}
|
||||
};
|
||||
@@ -167,7 +167,7 @@ export default function AdminPage() {
|
||||
toast.success("User updated successfully");
|
||||
setEditingUser(null);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error("Update failed");
|
||||
}
|
||||
};
|
||||
@@ -183,7 +183,7 @@ export default function AdminPage() {
|
||||
await inventoryApi.triggerBackup();
|
||||
toast.success("Snapshot created successfully");
|
||||
loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error("Backup failed");
|
||||
} finally {
|
||||
setIsBackingUp(false);
|
||||
@@ -198,7 +198,7 @@ export default function AdminPage() {
|
||||
await inventoryApi.restoreDatabase(filename);
|
||||
toast.success("Database restored! Reloading system...", { id: loadingToast });
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error("Restore failed", { id: loadingToast });
|
||||
}
|
||||
};
|
||||
@@ -208,7 +208,7 @@ export default function AdminPage() {
|
||||
await inventoryApi.updateDbSettings(newSettings);
|
||||
toast.success("System policy updated");
|
||||
setDbSettings(newSettings);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to update settings");
|
||||
}
|
||||
};
|
||||
@@ -218,7 +218,7 @@ export default function AdminPage() {
|
||||
try {
|
||||
await inventoryApi.updateLdapConfig(ldapConfig);
|
||||
toast.success("Enterprise configuration updated");
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
toast.error("Failed to update LDAP config");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { db, Item } from '@/lib/db';
|
||||
import { inventoryApi } from '@/lib/api';
|
||||
import PageShell from '@/components/PageShell';
|
||||
import Scanner from '@/components/Scanner';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import {
|
||||
Package,
|
||||
@@ -17,7 +18,8 @@ import {
|
||||
X,
|
||||
AlertTriangle,
|
||||
Tag,
|
||||
Edit2
|
||||
Edit2,
|
||||
Camera
|
||||
} from 'lucide-react';
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
@@ -50,6 +52,10 @@ export default function InventoryPage() {
|
||||
const [catEditedName, setCatEditedName] = useState('');
|
||||
const [catEditedDesc, setCatEditedDesc] = useState('');
|
||||
|
||||
// Scanner state
|
||||
const [showScanner, setShowScanner] = useState(false);
|
||||
const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const savedUser = localStorage.getItem('inventory_user');
|
||||
@@ -79,7 +85,7 @@ export default function InventoryPage() {
|
||||
// Sync local DB
|
||||
await db.items.clear();
|
||||
await db.items.bulkPut(res);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("Failed to load backend data", err);
|
||||
}
|
||||
};
|
||||
@@ -143,7 +149,7 @@ export default function InventoryPage() {
|
||||
setIsEditing(false);
|
||||
setSelectedItem(updated as Item);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
toast.error("Failed to update item");
|
||||
}
|
||||
@@ -161,7 +167,7 @@ export default function InventoryPage() {
|
||||
toast.success("Item deleted");
|
||||
setSelectedItem(null);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
toast.error("Failed to delete item");
|
||||
}
|
||||
@@ -177,11 +183,36 @@ export default function InventoryPage() {
|
||||
toast.success("Category updated");
|
||||
setEditingCategory(null);
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
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
|
||||
const categories = Array.from(new Set(inventory.map(i => i.category)));
|
||||
const filteredCategories = categories.filter(c =>
|
||||
@@ -189,8 +220,9 @@ export default function InventoryPage() {
|
||||
inventory.some(i => i.category === c && i.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
);
|
||||
|
||||
// Extract unique item types for suggestions
|
||||
// Extract unique item types and box labels for suggestions
|
||||
const existingTypes = Array.from(new Set(inventory.map(i => i.type).filter(Boolean))).sort() as string[];
|
||||
const existingBoxes = Array.from(new Set(inventory.map(i => i.box_label).filter(Boolean))).sort() as string[];
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
@@ -200,6 +232,9 @@ export default function InventoryPage() {
|
||||
<datalist id="existing-types">
|
||||
{existingTypes.map(t => <option key={t} value={t} />)}
|
||||
</datalist>
|
||||
<datalist id="existing-boxes">
|
||||
{existingBoxes.map(b => <option key={b} value={b} />)}
|
||||
</datalist>
|
||||
|
||||
<header className="flex items-center gap-5 mb-10">
|
||||
<div className="p-4 bg-primary/10 rounded-[2rem] text-primary border border-primary/20 shadow-xl shadow-primary/5">
|
||||
@@ -412,6 +447,33 @@ export default function InventoryPage() {
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs font-black text-slate-500 ml-1">Box / Container Label</label>
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
list="existing-boxes"
|
||||
value={editedItem.box_label || ''}
|
||||
onChange={e => setEditedItem({...editedItem, box_label: e.target.value})}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 pl-4 pr-12 text-sm outline-none text-slate-100 placeholder:text-slate-700 focus:border-primary transition-colors"
|
||||
placeholder="e.g. SFPs 40G Cisco"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFieldScanning({ active: true, field: 'box_label' });
|
||||
setShowScanner(true);
|
||||
toast.success("Ready to scan Box label...");
|
||||
}}
|
||||
className={cn(
|
||||
"absolute right-2 p-2 rounded-lg transition-all",
|
||||
fieldScanning?.active ? "bg-primary text-white animate-pulse" : "text-slate-500 hover:bg-slate-800"
|
||||
)}
|
||||
>
|
||||
<Camera size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-black text-slate-500 ml-1">Specs / Comments</label>
|
||||
@@ -542,6 +604,13 @@ export default function InventoryPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showScanner && (
|
||||
<Scanner
|
||||
onScanSuccess={onScanSuccess}
|
||||
onOCRMatch={onOCRMatch}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function LogsPage() {
|
||||
});
|
||||
|
||||
setAuditLogs(enrichedLogs);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("Critical log load failure:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -289,7 +289,7 @@ export default function LogsPage() {
|
||||
|
||||
{selectedLog.target_snapshot && (() => {
|
||||
try {
|
||||
const snap = JSON.parse(selectedLog.target_snapshot);
|
||||
const snap = JSON.parse(selectedLog.target_snapshot) as Record<string, any>;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -299,12 +299,12 @@ export default function LogsPage() {
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{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">
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import PageShell from '@/components/PageShell';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import {
|
||||
Package,
|
||||
Camera,
|
||||
Plus,
|
||||
Minus,
|
||||
Trash2,
|
||||
@@ -47,7 +48,7 @@ import { generateBarcode128, getQRCodeURL } from '@/lib/labels';
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import axios from 'axios';
|
||||
import versionData from '../../VERSION.json';
|
||||
import versionData from '../VERSION.json';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@@ -80,6 +81,7 @@ export default function Home() {
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState<any | null>(null);
|
||||
const [categories, setCategories] = useState<any[]>([]);
|
||||
const [fieldScanning, setFieldScanning] = useState<{ active: boolean, field: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('inventory_token')) {
|
||||
@@ -240,6 +242,17 @@ export default function Home() {
|
||||
|
||||
if (tokens.length === 0) return;
|
||||
|
||||
// [NEW] Targeted Field Scan Logic
|
||||
if (fieldScanning?.active) {
|
||||
if (fieldScanning.field === 'box_label') {
|
||||
const potentialLabel = tokens[0] || cleanText;
|
||||
setEditedItem(prev => ({ ...prev, box_label: potentialLabel }));
|
||||
setFieldScanning(null);
|
||||
toast.success(`Captured: ${potentialLabel}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Toast only potentially useful scans
|
||||
toast(`Scanning: ${cleanText.substring(0, 30)}...`, { icon: '🔍', duration: 1500, id: 'ocr-scan' });
|
||||
|
||||
@@ -329,7 +342,7 @@ export default function Home() {
|
||||
setIsEditing(false);
|
||||
setSelectedItem(updated as Item);
|
||||
await loadInventory();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
toast.error("Failed to update item");
|
||||
}
|
||||
@@ -347,7 +360,7 @@ export default function Home() {
|
||||
toast.success("Item deleted from catalog");
|
||||
setSelectedItem(null);
|
||||
await loadInventory();
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
toast.error("Failed to delete item");
|
||||
}
|
||||
@@ -662,14 +675,30 @@ export default function Home() {
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs font-black text-slate-500 ml-1">Box / Container Label</label>
|
||||
<input
|
||||
type="text"
|
||||
list="existing-boxes"
|
||||
value={editedItem.box_label || ''}
|
||||
onChange={e => setEditedItem({...editedItem, box_label: e.target.value})}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 px-4 text-sm outline-none text-slate-100"
|
||||
placeholder="e.g. SFPs 40G Cisco"
|
||||
/>
|
||||
<div className="relative flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
list="existing-boxes"
|
||||
value={editedItem.box_label || ''}
|
||||
onChange={e => setEditedItem({...editedItem, box_label: e.target.value})}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-xl py-3 pl-4 pr-12 text-sm outline-none text-slate-100 placeholder:text-slate-700 focus:border-primary transition-colors"
|
||||
placeholder="e.g. SFPs 40G Cisco"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFieldScanning({ active: true, field: 'box_label' });
|
||||
setShowScanner(true);
|
||||
toast.success("Scanning for BOX label...");
|
||||
}}
|
||||
className={cn(
|
||||
"absolute right-2 p-2 rounded-lg transition-all",
|
||||
fieldScanning?.active ? "bg-primary text-white animate-pulse" : "text-slate-500 hover:bg-slate-800"
|
||||
)}
|
||||
>
|
||||
<Camera size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs font-black text-slate-500 ml-1">Specs</label>
|
||||
@@ -861,7 +890,7 @@ export default function Home() {
|
||||
<Printer size={14} /> Print Label
|
||||
</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"
|
||||
>
|
||||
View
|
||||
@@ -975,7 +1004,7 @@ export default function Home() {
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</PageShell>
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [extractedData, setExtractedData] = useState<any>(null);
|
||||
const [mode, setMode] = useState<'item' | 'box'>('item');
|
||||
|
||||
// Extract unique item types for suggestions
|
||||
const existingTypes = Array.from(new Set(inventory.map(i => i.type).filter(Boolean))).sort() as string[];
|
||||
@@ -42,7 +43,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
|
||||
const formData = new FormData();
|
||||
formData.append('file', blob, 'label.jpg');
|
||||
|
||||
const data = await inventoryApi.analyzeLabel(formData);
|
||||
const data = await inventoryApi.analyzeLabel(formData, mode);
|
||||
|
||||
if (data.error) {
|
||||
toast.error(`AI Error: ${data.error}`);
|
||||
@@ -51,7 +52,11 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
|
||||
}
|
||||
|
||||
setExtractedData(data);
|
||||
toast.success("AI extraction complete!");
|
||||
if (mode === 'box') {
|
||||
toast.success(`Box identified: ${data.box_label || data.name}`);
|
||||
} else {
|
||||
toast.success("AI extraction complete!");
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error("Failed to process image with AI");
|
||||
console.error(error);
|
||||
@@ -97,13 +102,34 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
|
||||
|
||||
{!image ? (
|
||||
<div className="flex-1 flex flex-col gap-6 min-h-0">
|
||||
<div className="flex bg-slate-900/50 p-1.5 rounded-2xl border border-slate-800/50 shrink-0">
|
||||
<button
|
||||
onClick={() => setMode('item')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-bold transition-all ${mode === 'item' ? 'bg-primary text-white shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}
|
||||
>
|
||||
<Package size={18} />
|
||||
<span className="text-xs">Item Label</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('box')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-bold transition-all ${mode === 'box' ? 'bg-primary text-white shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}
|
||||
>
|
||||
<Layers size={18} />
|
||||
<span className="text-xs">Box / Container</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col items-center justify-center border-2 border-dashed border-slate-800 rounded-[2.5rem] bg-slate-900/30 overflow-hidden px-4">
|
||||
<div className="w-20 h-20 bg-slate-900 rounded-3xl flex items-center justify-center mb-6 shadow-inner">
|
||||
<Camera size={32} className="text-slate-500" />
|
||||
<Camera size={32} className={mode === 'box' ? 'text-primary' : 'text-slate-500'} />
|
||||
</div>
|
||||
<p className="text-slate-300 mb-2 text-center font-bold">Label Insight Mode</p>
|
||||
<p className="text-slate-300 mb-2 text-center font-bold">
|
||||
{mode === 'box' ? 'Container Discovery Mode' : 'Label Insight Mode'}
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 px-8 text-center leading-relaxed font-bold">
|
||||
Scan or upload a sharp photo of the item specifications
|
||||
{mode === 'box'
|
||||
? 'Scan the large, prominent label or hand-written name on the container'
|
||||
: 'Scan or upload a sharp photo of the item specifications'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
|
||||
const [isSelecting, setIsSelecting] = useState(false);
|
||||
const [capturedImage, setCapturedImage] = useState<string | null>(null);
|
||||
const isBusy = useRef(false);
|
||||
const isTransitioning = useRef(false); // Flag to track start/stop transitions
|
||||
const scannerId = "reader-container-unique";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -33,11 +34,20 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
|
||||
}
|
||||
|
||||
const startScanner = async () => {
|
||||
if (isBusy.current) return;
|
||||
if (isBusy.current || isTransitioning.current) return;
|
||||
isBusy.current = true;
|
||||
isTransitioning.current = true;
|
||||
|
||||
try {
|
||||
// If already scanning, we MUST stop it first
|
||||
if (html5QrCodeRef.current?.isScanning) {
|
||||
await html5QrCodeRef.current.stop();
|
||||
try {
|
||||
await html5QrCodeRef.current.stop();
|
||||
} catch (e) {
|
||||
console.warn("Graceful stop failed, might be in transition:", e);
|
||||
// If it's already in transition, we just return and wait for the next effect trigger
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const config = {
|
||||
@@ -50,7 +60,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
|
||||
Html5QrcodeSupportedFormats.CODE_39,
|
||||
Html5QrcodeSupportedFormats.EAN_13,
|
||||
Html5QrcodeSupportedFormats.UPC_A,
|
||||
Html5QrcodeSupportedFormats.DATAMATRIX
|
||||
Html5QrcodeSupportedFormats.DATA_MATRIX
|
||||
],
|
||||
videoConstraints: {
|
||||
facingMode: "environment"
|
||||
@@ -85,6 +95,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
|
||||
}
|
||||
} finally {
|
||||
isBusy.current = false;
|
||||
isTransitioning.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -94,11 +105,15 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
|
||||
|
||||
return () => {
|
||||
if (html5QrCodeRef.current?.isScanning) {
|
||||
isTransitioning.current = true;
|
||||
html5QrCodeRef.current.stop()
|
||||
.then(() => {
|
||||
setIsStarted(false);
|
||||
})
|
||||
.catch(e => console.error("Stop failed", e));
|
||||
.catch(e => console.error("Unmount stop failed", e))
|
||||
.finally(() => {
|
||||
isTransitioning.current = false;
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [paused, onScanSuccess]);
|
||||
|
||||
19
frontend/entrypoint.sh
Normal file
19
frontend/entrypoint.sh
Normal 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
|
||||
@@ -1,20 +1,48 @@
|
||||
import axios from 'axios';
|
||||
import { getToken, clearAuth } from './auth';
|
||||
|
||||
export const getBackendUrl = () => {
|
||||
if (typeof window === 'undefined') return 'http://localhost:8000';
|
||||
// Cached config to avoid repeated fetches
|
||||
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;
|
||||
|
||||
// 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 (host.includes('.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({});
|
||||
|
||||
axiosInstance.interceptors.request.use((config) => {
|
||||
axiosInstance.interceptors.request.use(async (config) => {
|
||||
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();
|
||||
if (token) {
|
||||
@@ -75,8 +103,8 @@ export const inventoryApi = {
|
||||
}
|
||||
},
|
||||
|
||||
analyzeLabel: async (formData: FormData) => {
|
||||
const res = await axiosInstance.post('/items/extract-label', formData, {
|
||||
analyzeLabel: async (formData: FormData, mode: string = "item") => {
|
||||
const res = await axiosInstance.post(`/items/extract-label?mode=${mode}`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
return res.data;
|
||||
@@ -110,7 +138,8 @@ export const inventoryApi = {
|
||||
// Users
|
||||
getUsers: async () => {
|
||||
// [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;
|
||||
},
|
||||
|
||||
@@ -121,7 +150,8 @@ export const inventoryApi = {
|
||||
|
||||
login: async (credentials: any) => {
|
||||
// [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;
|
||||
},
|
||||
|
||||
|
||||
1
frontend/public/sw.js
Normal file
1
frontend/public/sw.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/public/workbox-4754cb34.js
Normal file
1
frontend/public/workbox-4754cb34.js
Normal file
File diff suppressed because one or more lines are too long
@@ -7,17 +7,38 @@ echo "🚀 Starting TFM aInventory in Standalone Mode..."
|
||||
# Trapping termination signals to clean up child processes
|
||||
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
|
||||
FRONTEND_PORT=3001
|
||||
BACKEND_SSL_PORT=3002
|
||||
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
|
||||
if [ -d ".venv" ]; then
|
||||
source .venv/bin/activate
|
||||
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)
|
||||
echo "🔥 Starting Backend (Uvicorn)..."
|
||||
python3 -m uvicorn backend.main:app --host 0.0.0.0 --port $BACKEND_PORT &
|
||||
|
||||
@@ -32,10 +32,10 @@ mkdir -p "$DATA_DIR"
|
||||
mkdir -p "$LOGS_DIR"
|
||||
|
||||
# 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())
|
||||
LDAP_CONFIG="$PROJECT_ROOT/backend/config/ldap_config.json"
|
||||
LDAP_EXAMPLE="$PROJECT_ROOT/backend/config/ldap_config.json.example"
|
||||
LDAP_CONFIG="$PROJECT_ROOT/config/ldap_config.json"
|
||||
LDAP_EXAMPLE="$PROJECT_ROOT/config/ldap_config.json.example"
|
||||
|
||||
if [ ! -f "$LDAP_CONFIG" ]; then
|
||||
if [ -f "$LDAP_EXAMPLE" ]; then
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
VERSION_FILE = 'VERSION.json'
|
||||
VERSION_FILE = 'frontend/VERSION.json'
|
||||
GIT_PATH_FILE = '.git_path'
|
||||
|
||||
def get_git_path():
|
||||
@@ -24,7 +24,7 @@ def run_command(cmd):
|
||||
def main():
|
||||
git = get_git_path()
|
||||
|
||||
# 1. Read and Increment Version
|
||||
# 1. Read and Parse Version
|
||||
if not os.path.exists(VERSION_FILE):
|
||||
print(f"Error: {VERSION_FILE} not found.")
|
||||
sys.exit(1)
|
||||
@@ -34,10 +34,20 @@ def main():
|
||||
|
||||
old_version = data.get('version', '1.0.0')
|
||||
parts = old_version.split('.')
|
||||
if len(parts) == 3:
|
||||
parts[2] = str(int(parts[2]) + 1)
|
||||
if len(parts) < 3:
|
||||
parts.extend(['0'] * (3 - len(parts)))
|
||||
|
||||
# Check for --minor or --major flags
|
||||
if '--minor' in sys.argv:
|
||||
parts[1] = str(int(parts[1]) + 1)
|
||||
parts[2] = '0'
|
||||
elif '--major' in sys.argv:
|
||||
parts[0] = str(int(parts[0]) + 1)
|
||||
parts[1] = '0'
|
||||
parts[2] = '0'
|
||||
else:
|
||||
parts.append('1')
|
||||
# Default: patch increment
|
||||
parts[2] = str(int(parts[2]) + 1)
|
||||
|
||||
new_version = '.'.join(parts)
|
||||
data['version'] = new_version
|
||||
@@ -57,11 +67,18 @@ def main():
|
||||
branch_name = f"v{new_version}"
|
||||
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...")
|
||||
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.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# --- CONFIGURATION ---
|
||||
# --- CONFIGURATION (Default values, will be overridden by network_config.env) ---
|
||||
BACKEND_PORT=8000
|
||||
FRONTEND_PORT=3001
|
||||
BACKEND_SSL_PORT=3002
|
||||
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
|
||||
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..."
|
||||
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 " CORS origins: $ALLOWED_ORIGINS"
|
||||
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 ""
|
||||
echo -e " USE THIS URL ON BOTH DESKTOP & MOBILE:"
|
||||
echo -e " 👉 ${GREEN}${BOLD}https://$LOCAL_IP:3003${NC}"
|
||||
echo -e " (Or ${GREEN}https://localhost:3003${NC} on this Mac)"
|
||||
echo -e " 👉 ${GREEN}${BOLD}https://$LOCAL_IP:$FRONTEND_SSL_PORT${NC}"
|
||||
echo -e " (Or ${GREEN}https://localhost:$FRONTEND_SSL_PORT${NC} on this Mac)"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}${BOLD}NOTE:${NC} If you see a 'Not Private' warning,"
|
||||
echo -e " Click 'Advanced' -> 'Proceed' to continue."
|
||||
|
||||
Reference in New Issue
Block a user