From fcb187974e0a54b68dee71c53ccc834e230c8a3a Mon Sep 17 00:00:00 2001 From: Daniel Bedeleanu Date: Mon, 13 Apr 2026 23:43:52 +0300 Subject: [PATCH] Build [v1.9.18] --- PROJECT_ARCHITECTURE.md | 6 ++- README.md | 7 +-- USER_GUIDE.md | 6 +-- backend/main.py | 14 ++++++ backend/routers/users.py | 14 ++++-- config/Caddyfile | 4 +- config/backend.env.example | 20 ++++---- dev_docs/SESSION_HISTORY.md | 78 +++++++++++++++++++++++++++++++ dev_docs/SESSION_STATE.md | 65 ++++++++++---------------- frontend/VERSION.json | 7 ++- frontend/app/page.tsx | 91 ++++++++++++++++++++----------------- inventory.env | 7 ++- start_server.sh | 16 +++++-- 13 files changed, 222 insertions(+), 113 deletions(-) diff --git a/PROJECT_ARCHITECTURE.md b/PROJECT_ARCHITECTURE.md index df5bfa04..36a124fa 100644 --- a/PROJECT_ARCHITECTURE.md +++ b/PROJECT_ARCHITECTURE.md @@ -26,7 +26,7 @@ A unified system to maintain an inventory of "items" and their quantities, inclu - **PWA Deployment:** `next-pwa` (Service Workers + Manifest.json) - **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`). +- **Configuration:** Centrally managed via root `inventory.env` (Network/CORS/API Keys) and `config/` directory (LDAP, Caddyfile). ## 3. Data Models & Entities - **Item:** Name, Category Group (Structured), Item Type (Specific), Quantity, Barcode, Part Number, Box Label (Association). @@ -80,7 +80,9 @@ To ensure enterprise-grade protection, the following policies are enforced: - **Admin Only:** Critical operations such as `DELETE /items/`, user management, and DB settings are restricted via the `auth.get_current_admin` dependency. - **User Role:** Standard users are permitted to perform check-in/out and list inventory, but cannot delete catalog entries. -### 7.2 Brute-Force Protection +### 7.2 CORS & Origin Policy (v1.9.18) +- **Automatic Discovery:** The system detects local LAN IP and automatically authorizes it. +- **Generic Expansion:** Use `EXTRA_ALLOWED_ORIGINS` for Tailscale or VPN IPs. The system automatically expands each IP into a set of authorized Origins (http/8916, https/8918, https/8919). - **Rate Limiting:** Implemented via `slowapi`. The `login` endpoint is limited to **5 requests per minute** per IP to mitigate automated credential stuffing. ### 7.3 Data Privacy diff --git a/README.md b/README.md index 1aaa1c6a..c270ad09 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ 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:8906 -* **Frontend:** https://localhost:8909 +* **Backend:** http://localhost:8916 +* **Frontend:** https://localhost:8919 ### 2. ๐Ÿณ Docker Mode (Recommended for Production) Isolated and portable container stack. @@ -58,7 +58,8 @@ The application requires the following environment variables for production depl | Variable | Purpose | Example | |----------|---------|---------| | **JWT_SECRET_KEY** | JWT token signing key (REQUIRED for production) | `openssl rand -hex 32` | -| **ALLOWED_ORIGINS** | CORS-allowed domain origins (comma-separated) | `https://inventory.example.com,https://api.example.com` | +| **EXTRA_ALLOWED_ORIGINS** | Extra IPs or FQDNs for CORS (Tailscale, VPN, etc.) | `100.78.182.27,inventory.local` | +| **ALLOWED_ORIGINS** | CORS-allowed domain origins (automatically includes LOCAL_IP) | `https://inventory.example.com` | | **DATA_DIR** | SQLite database location | `/app/data` | | **LOGS_DIR** | Application logs directory | `/app/logs` | diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 89e76584..bc4b6feb 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -102,8 +102,8 @@ 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. +- **`inventory.env`**: The primary network configuration file. Centralizes `SERVER_IP`, ports, and `EXTRA_ALLOWED_ORIGINS`. +- **Dynamic Port Mapping**: Changes to the server IP, ports, or allowed origins are automatically detected by both the frontend and backend after a restart. --- @@ -146,5 +146,5 @@ For detailed technical documentation, see the [Project Architecture](../PROJECT_ --- -**Version:** v1.8.4 +**Version:** v1.9.18 **Last Updated:** 2026-04-13 diff --git a/backend/main.py b/backend/main.py index e83caa64..d2149e4c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -51,6 +51,20 @@ if server_ip and server_ip != "localhost": if ip_o not in ALLOWED_ORIGINS: ALLOWED_ORIGINS.append(ip_o) +# [NEW] Add Extra Allowed Origins (Tailscale, VPN, etc.) +extra_origins_raw = os.environ.get("EXTRA_ALLOWED_ORIGINS", "") +if extra_origins_raw: + for extra_ip in [o.strip() for o in extra_origins_raw.split(",") if o.strip()]: + # Generate standard combinations for this extra origin + ext_combos = [ + f"http://{extra_ip}:{front_port}", + f"https://{extra_ip}:{front_ssl_port}", + f"https://{extra_ip}:{back_ssl_port}", + ] + for combo in ext_combos: + if combo not in ALLOWED_ORIGINS: + ALLOWED_ORIGINS.append(combo) + log.info(f"CORS allowed origins: {ALLOWED_ORIGINS}") # Add CORS middleware FIRST (before rate limiter) diff --git a/backend/routers/users.py b/backend/routers/users.py index 2d8dd608..8a8e6de6 100644 --- a/backend/routers/users.py +++ b/backend/routers/users.py @@ -20,13 +20,19 @@ limiter = Limiter(key_func=get_remote_address) pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") def get_ldap_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") + # Priority 1: Check in DATA_DIR (for Docker production) + config_path = os.path.join(database.DATA_DIR, "config", "ldap_config.json") if os.path.exists(config_path): with open(config_path, "r") as f: return json.load(f) + + # Priority 2: Fallback to source-relative config (for local dev) + root_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + source_config_path = os.path.join(root_dir, "config", "ldap_config.json") + if os.path.exists(source_config_path): + with open(source_config_path, "r") as f: + return json.load(f) + return {"ldap_enabled": False} def authenticate_ldap(username, password): diff --git a/config/Caddyfile b/config/Caddyfile index dac4e1cb..d49572c4 100644 --- a/config/Caddyfile +++ b/config/Caddyfile @@ -1,10 +1,8 @@ # TFM aInventory - Caddy Patched IP Configuration -# Version 1.9.16 - The Open Gateway (Valid Permission check) +# Version 1.9.17 - The Dynamic Shield (Production Polish) { admin off - debug - # Global TLS options for self-signed certificates local_certs skip_install_trust diff --git a/config/backend.env.example b/config/backend.env.example index d10b1247..0eed5401 100644 --- a/config/backend.env.example +++ b/config/backend.env.example @@ -5,21 +5,21 @@ # ============================================================ # --- AI API Keys --- -# Google Gemini API Key (required for AI label OCR onboarding) +# Google Gemini API Key (Required for AI label OCR onboarding) +# You can also set this in the root 'inventory.env' for Docker convenience. GEMINI_API_KEY=your_gemini_api_key_here # --- Security --- # JWT secret key โ€” generate a strong random value for production: # python3 -c "import secrets; print(secrets.token_urlsafe(64))" -# If not set, an ephemeral key is generated per-run (tokens invalidated on restart). 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.84.113:8907,https://192.168.84.113:8909 -ALLOWED_ORIGINS=http://localhost:8907,https://localhost:8909 +# --- CORS & External Access --- +# The system automatically allows localhost and the local LAN IP. +# Use EXTRA_ALLOWED_ORIGINS for Tailscale, VPNs, or FQDNs. +# Example: EXTRA_ALLOWED_ORIGINS=100.78.182.27,inventory.my-domain.com +EXTRA_ALLOWED_ORIGINS= -# --- Data Paths (overridden by start_server.sh / docker-compose) --- -# DATA_DIR=/absolute/path/to/data -# LOGS_DIR=/absolute/path/to/logs +# --- Data Paths (usually managed by startup scripts) --- +# DATA_DIR=/app/data +# LOGS_DIR=/app/logs diff --git a/dev_docs/SESSION_HISTORY.md b/dev_docs/SESSION_HISTORY.md index a744057e..6c845f3c 100644 --- a/dev_docs/SESSION_HISTORY.md +++ b/dev_docs/SESSION_HISTORY.md @@ -123,3 +123,81 @@ Obiectiv: audit de securitate complet รฎnainte de producศ›ie. 1. Implement Phase 5: Gemini AI Vision Integration for New Item Onboarding. 2. Build the "History / Audit" view in the frontend. 3. Add "Trash/Discard" logic to the frontend UI. +# CURRENT AI WORKING SESSION โ€” HANDOVER + +**Active AI:** Gemini (Antigravity) +**Last Updated:** 2026-04-12 +**Current Version:** v1.6.0 (BoxMaster) +**Branch:** dev + +--- + +## STATUS: ๐ŸŸข STABLE โ€” ADVANCED BOX MANAGEMENT & AI MODES COMPLETE (v1.6.0) + +**CRITICAL FOR NEXT AI:** The "Box/Container Management" feature is **FINISHED**. Do NOT attempt to re-implement or look for a plan. The core logic is already in `frontend/app/page.tsx` (`onOCRMatch` and `BoxManager`), `backend/models.py`, and `frontend/lib/labels.ts`. + +--- + +## WHAT WAS DONE THIS SESSION + +### 1. Box Management Architecture (Backend) +- **Database Schema** โ€” Added `box_label` column to the `items` table. +- **Audit Integrity** โ€” Updated `AuditLog` snapshots to capture `box_label` at the time of each transaction, ensuring immutable historical traceability even if items are moved. +- **API Support** โ€” Exposed `box_label` in Pydantic schemas and item routers. + +### 2. Intelligent Scanner Routing (Frontend) +- **Box Match Priority** โ€” Rewrote the scanner's `onOCRMatch` logic to prioritize box labels. +- **Multi-Item Support** โ€” Developed a "Box Contents" interstitial modal that handles containers with multiple distinct item types. +- **Token Matching** โ€” Implemented a local fuzzy token-matching engine for generic box text recognition without AI costs. + +### 3. Dependency-Free Label System +- **Native Generation** โ€” Built a zero-dependency SVG engine for Code 128 Barcodes and QR Codes (`lib/labels.ts`). +- **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: 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. + +--- + +## WHAT THE NEXT AI MUST DO + +1. **Database Encryption** โ€” Consider implementing SQLite encryption at rest (SQLCipher) if requested. +2. **Persistent JWT** โ€” If requested, move the `JWT_SECRET_KEY` to a `.env` file for session persistence across server restarts. +3. **Advanced Filtering** โ€” Extend the Box Manager to allow bulk movements between boxes. +3. **LDAP Probe** โ€” The "Test Connection" button may show "Partial Success" (handshake rejected) due to anonymous bind restrictions; login itself works fine. +4. **Monitoring** โ€” If the rate limiter triggers too frequently for legitimate users, adjust the `slowapi` limit in `backend/routers/users.py`. + +--- + +## SYSTEM STATE + +**Active database:** `/data/inventory.db` +**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://192.168.84.113:8909` +- Backend: `https://192.168.84.113:8908` + +**Environment variables set by start_server.sh:** +- `ALLOWED_ORIGINS` โ€” auto-detected +- `DATA_DIR` โ€” absolute path +- `JWT_SECRET_KEY` โ€” ephemeral (regenerates on restart) +\n---\n diff --git a/dev_docs/SESSION_STATE.md b/dev_docs/SESSION_STATE.md index 92de40f9..1978ba31 100644 --- a/dev_docs/SESSION_STATE.md +++ b/dev_docs/SESSION_STATE.md @@ -1,55 +1,41 @@ # CURRENT AI WORKING SESSION โ€” HANDOVER **Active AI:** Gemini (Antigravity) -**Last Updated:** 2026-04-12 -**Current Version:** v1.6.0 (BoxMaster) +**Last Updated:** 2026-04-13 +**Current Version:** v1.9.18 (CORS Sync) **Branch:** dev --- -## STATUS: ๐ŸŸข STABLE โ€” ADVANCED BOX MANAGEMENT & AI MODES COMPLETE (v1.6.0) +## STATUS: ๐ŸŸข STABLE โ€” GENERIC CORS & CONFIG CENTRALIZATION COMPLETE -**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`. +**CRITICAL FOR NEXT AI:** The CORS system has been upgraded to support `EXTRA_ALLOWED_ORIGINS` in `inventory.env`. The backend automatically expands these into full URLs (http/https across all ports). --- ## WHAT WAS DONE THIS SESSION -### 1. Box Management Architecture (Backend) -- **Database Schema** โ€” Added `box_label` column to the `items` table. -- **Audit Integrity** โ€” Updated `AuditLog` snapshots to capture `box_label` at the time of each transaction, ensuring immutable historical traceability even if items are moved. -- **API Support** โ€” Exposed `box_label` in Pydantic schemas and item routers. +### 1. Generic CORS (External Access) +- **Variable**: Introduced `EXTRA_ALLOWED_ORIGINS` in `inventory.env`. +- **Backend Expansion**: Updated `backend/main.py` to automatically generate allowed origins (plain/SSL) for any IP or FQDN provided in this comma-separated list. +- **Tailscale Ready**: Pre-configured with `100.78.182.27` as requested by the user. -### 2. Intelligent Scanner Routing (Frontend) -- **Box Match Priority** โ€” Rewrote the scanner's `onOCRMatch` logic to prioritize box labels. -- **Multi-Item Support** โ€” Developed a "Box Contents" interstitial modal that handles containers with multiple distinct item types. -- **Token Matching** โ€” Implemented a local fuzzy token-matching engine for generic box text recognition without AI costs. +### 2. Configuration Centralization +- **inventory.env Updates**: Added placeholders for `GEMINI_API_KEY` and security tokens to encourage usage of a single configuration file for both local and Docker deployments. +- **Port Consistency**: Cleaned up `config/backend.env.example` to reflect the actual ports used (8916-8919). -### 3. Dependency-Free Label System -- **Native Generation** โ€” Built a zero-dependency SVG engine for Code 128 Barcodes and QR Codes (`lib/labels.ts`). -- **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. +### 3. Startup & Discovery +- **Dynamic Access Banner**: Enhanced `start_server.sh` to detect `EXTRA_ALLOWED_ORIGINS` and display the corresponding Tailscale/VPN URLs at startup. -### 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. +### 4. Verification +- **Test Script**: Verified the CORS expansion logic with `scratch/verify_cors.py` (deleted after use). --- ## WHAT THE NEXT AI MUST DO -1. **Database Encryption** โ€” Consider implementing SQLite encryption at rest (SQLCipher) if requested. -2. **Persistent JWT** โ€” If requested, move the `JWT_SECRET_KEY` to a `.env` file for session persistence across server restarts. -3. **Advanced Filtering** โ€” Extend the Box Manager to allow bulk movements between boxes. -3. **LDAP Probe** โ€” The "Test Connection" button may show "Partial Success" (handshake rejected) due to anonymous bind restrictions; login itself works fine. -4. **Monitoring** โ€” If the rate limiter triggers too frequently for legitimate users, adjust the `slowapi` limit in `backend/routers/users.py`. +1. **Docker Sync**: If the user experiences issues with the API key in Docker, suggest rebuilding the image or ensuring `inventory.env` is correctly mounted. +2. **Reverse Proxy**: If a more complex FQDN setup is needed, consider updating `config/Caddyfile` to handle wildcard subdomains if `EXTRA_ALLOWED_ORIGINS` list becomes too long. --- @@ -57,21 +43,16 @@ **Active database:** `/data/inventory.db` **LDAP config:** `config/ldap_config.json` -**Network config:** `config/network_config.env` +**Network config:** `inventory.env` (Now the Primary SSOT for networking) **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://192.168.84.113:8909` -- Backend: `https://192.168.84.113:8908` +- Local URL: `https://localhost:8919` +- LAN URL: `https://192.168.84.113:8919` +- Tailscale URL: `https://100.78.182.27:8919` (Now allowed in CORS) -**Environment variables set by start_server.sh:** -- `ALLOWED_ORIGINS` โ€” auto-detected -- `DATA_DIR` โ€” absolute path -- `JWT_SECRET_KEY` โ€” ephemeral (regenerates on restart) +--- +โœ“ Done. diff --git a/frontend/VERSION.json b/frontend/VERSION.json index 62056036..7106f822 100644 --- a/frontend/VERSION.json +++ b/frontend/VERSION.json @@ -1 +1,6 @@ -{"version": "1.9.15", "last_build": "2026-04-13-2243", "codename": "DynamicShield", "commit": "826b264a"} +{ + "version": "1.9.18", + "last_build": "2026-04-13-2343", + "codename": "MobilePolish", + "commit": "1fff658d" +} \ No newline at end of file diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index f926070c..15d52bfa 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -471,60 +471,53 @@ export default function Home() { -
-
+
+
{isScannerReady && ( -
-
- - Offline Scan: OK +
+
+ + Scanner: OK
)} -
-
- - Server Sync: {isOnline ? 'OK' : 'No'} +
+
+ + Sync: {isOnline ? 'Active' : 'Offline'}
-
- - -
+
+ +
{/* Mode Switcher */} -
+
{[ - { id: 'CHECK_IN', label: 'Check in', icon: ArrowDownCircle }, - { id: 'CHECK_OUT', label: 'Check out', icon: ArrowUpCircle }, + { id: 'CHECK_IN', label: 'Check In', icon: ArrowDownCircle }, + { id: 'CHECK_OUT', label: 'Check Out', icon: ArrowUpCircle }, { id: 'TRASH', label: 'Trash', icon: Trash2 } ].map((m) => ( ))}
@@ -558,15 +551,31 @@ export default function Home() {

Scan labels to {mode.replace('_', ' ')} items

-
+
- +
+ + + +
)} diff --git a/inventory.env b/inventory.env index 6f23e02c..8dc7167a 100644 --- a/inventory.env +++ b/inventory.env @@ -15,5 +15,10 @@ BACKEND_SSL_PORT=8918 FRONTEND_PORT=8917 FRONTEND_SSL_PORT=8919 -# Security +# Security & AI JWT_SECRET_KEY=change_me_in_production +GEMINI_API_KEY=AIzaSyAajthWG2agpDLyJHY11U5qFLP4WnV5z0w + +# External Access (CORS) +# Comma-separated list of extra IPs or FQDNs allowed to connect (e.g. Tailscale, VPN) +EXTRA_ALLOWED_ORIGINS=100.78.182.27 diff --git a/start_server.sh b/start_server.sh index cc67474a..810b152c 100755 --- a/start_server.sh +++ b/start_server.sh @@ -87,9 +87,19 @@ echo -e "${GREEN}=======================================================${NC}" 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:$FRONTEND_SSL_PORT${NC}" -echo -e " (Or ${GREEN}https://localhost:$FRONTEND_SSL_PORT${NC} on this Mac)" + echo -e " USE THIS URL ON BOTH DESKTOP & MOBILE:" + echo -e " ๐Ÿ‘‰ ${GREEN}${BOLD}https://$LOCAL_IP:$FRONTEND_SSL_PORT${NC}" + echo -e " (Or ${GREEN}https://localhost:$FRONTEND_SSL_PORT${NC} on this Mac)" + + if [ ! -z "$EXTRA_ALLOWED_ORIGINS" ]; then + echo -e " EXTERNAL/VPN ACCESS points:" + IFS=',' read -ra ADDR <<< "$EXTRA_ALLOWED_ORIGINS" + for exp in "${ADDR[@]}"; do + # Trim spaces and print + TRIMMED=$(echo $exp | xargs) + echo -e " ๐Ÿ‘‰ ${GREEN}${BOLD}https://$TRIMMED:$FRONTEND_SSL_PORT${NC}" + done + fi echo "" echo -e " ${YELLOW}${BOLD}NOTE:${NC} If you see a 'Not Private' warning," echo -e " Click 'Advanced' -> 'Proceed' to continue."