Compare commits

..

7 Commits

Author SHA1 Message Date
511e8ebde4 Notes printing function 2026-02-18 11:57:37 -05:00
1a82776657 Add notes API only 2026-02-18 06:15:48 -05:00
fa192247fe Private network fixes 2026-02-11 15:35:27 -05:00
65f951dfcc MQTT broker add 2026-02-09 15:28:48 -05:00
62bb5cb60d Update frontend print logic 2026-02-07 09:54:52 -05:00
7fd2e0050b Printer realtime status 2026-02-07 09:38:36 -05:00
20d8cefe22 Tidy up 2026-02-07 08:46:08 -05:00
10 changed files with 504 additions and 44 deletions

View File

@@ -90,6 +90,23 @@ environment:
- DATABASE_URL=sqlite:///./drugs.db
```
## MQTT
The system includes an MQTT broker (Mosquitto) with WebSocket support:
- **MQTT**: `localhost:1883`
- **WebSocket**: `localhost:9001` or `/mqtt` via nginx
To create a new MQTT user with a custom password:
```bash
docker run --rm -v $(pwd)/mosquitto/config:/temp eclipse-mosquitto mosquitto_passwd -b /temp/pwfile username password
```
Then restart the containers:
```bash
docker compose restart mosquitto
```
## Development
When you run `docker-compose up`, the backend automatically reloads when code changes (`--reload` flag). Just refresh your browser to see updates.

View File

@@ -6,7 +6,7 @@ from datetime import datetime, timedelta
from .database import engine, get_db, Base
from .models import Drug, DrugVariant, Dispensing, User
from .auth import hash_password, verify_password, create_access_token, get_current_user, get_current_admin_user, ACCESS_TOKEN_EXPIRE_MINUTES
from .mqtt_service import publish_label_print
from .mqtt_service import publish_label_print_with_response
from pydantic import BaseModel
# Create tables
@@ -133,6 +133,17 @@ class LabelPrintResponse(BaseModel):
success: bool
message: str
class NotesVariables(BaseModel):
animal_name: str
notes: str
class NotesPrintRequest(BaseModel):
variables: NotesVariables
class NotesPrintResponse(BaseModel):
success: bool
message: str
# Authentication Routes
@router.post("/auth/register", response_model=TokenResponse)
def register(user_data: UserCreate, db: Session = Depends(get_db)):
@@ -491,6 +502,25 @@ def get_variant_dispensings(variant_id: int, db: Session = Depends(get_db), curr
return db.query(Dispensing).filter(Dispensing.drug_variant_id == variant_id).order_by(Dispensing.dispensed_at.desc()).all()
# Helper function to capitalize text for labels
def capitalize_label_text(text: str) -> str:
"""Capitalize the first letter of each sentence in the text"""
if not text:
return text
# Capitalize first letter of the entire string
result = text[0].upper() + text[1:] if len(text) > 1 else text.upper()
# Also capitalize after periods and common sentence breaks
for delimiter in ['. ', '! ', '? ']:
parts = result.split(delimiter)
result = delimiter.join([
part[0].upper() + part[1:] if part else part
for part in parts
])
return result
# Label printing endpoint
@router.post("/labels/print", response_model=LabelPrintResponse)
def print_label(label_request: LabelPrintRequest, current_user: User = Depends(get_current_user)):
@@ -507,27 +537,48 @@ def print_label(label_request: LabelPrintRequest, current_user: User = Depends(g
label_size = os.getenv("LABEL_SIZE", "29x90")
test_mode = os.getenv("LABEL_TEST", "false").lower() == "true"
# Capitalize all text fields for better presentation
variables = label_request.variables.dict()
variables["practice_name"] = capitalize_label_text(variables["practice_name"])
variables["animal_name"] = capitalize_label_text(variables["animal_name"])
variables["drug_name"] = capitalize_label_text(variables["drug_name"])
variables["dosage"] = capitalize_label_text(variables["dosage"])
variables["quantity"] = capitalize_label_text(variables["quantity"])
# expiry_date doesn't need capitalization
# Convert the request to the MQTT message format
mqtt_message = {
"template_id": template_id,
"label_size": label_size,
"variables": label_request.variables.dict(),
"variables": variables,
"test": test_mode
}
# Publish to MQTT
success = publish_label_print(mqtt_message)
# Publish to MQTT and wait for response
success, response = publish_label_print_with_response(mqtt_message, timeout=10.0)
print(f"Label print result: success={success}, response={response}")
if success:
return LabelPrintResponse(
result = LabelPrintResponse(
success=True,
message="Label print request sent successfully"
message=response.get("message", "Label printed successfully")
)
print(f"Returning success response: {result}")
return result
else:
raise HTTPException(
status_code=500,
detail="Failed to send label print request to MQTT broker"
# Return error details from printer
# Check both 'message' and 'error' fields for error details
if response:
error_msg = response.get("message") or response.get("error", "Unknown error")
else:
error_msg = "No response from printer"
result = LabelPrintResponse(
success=False,
message=f"Print failed: {error_msg}"
)
print(f"Returning error response: {result}")
return result
except Exception as e:
raise HTTPException(
@@ -535,5 +586,66 @@ def print_label(label_request: LabelPrintRequest, current_user: User = Depends(g
detail=f"Error sending label print request: {str(e)}"
)
# Notes printing endpoint
@router.post("/notes/print", response_model=NotesPrintResponse)
def print_notes(notes_request: NotesPrintRequest, current_user: User = Depends(get_current_user)):
"""
Print notes by publishing an MQTT message
This endpoint publishes a notes print request to the MQTT broker,
which will be picked up by the label printing service.
"""
try:
# Get notes template configuration from environment
import os
template_id = os.getenv("NOTES_TEMPLATE_ID", "notes_template")
label_size = os.getenv("LABEL_SIZE", "29x90")
test_mode = os.getenv("LABEL_TEST", "false").lower() == "true"
# Capitalize text fields for better presentation
variables = notes_request.variables.dict()
variables["animal_name"] = capitalize_label_text(variables["animal_name"])
variables["notes"] = capitalize_label_text(variables["notes"])
# Convert the request to the MQTT message format
mqtt_message = {
"template_id": template_id,
"label_size": label_size,
"variables": variables,
"test": test_mode
}
# Publish to MQTT and wait for response
success, response = publish_label_print_with_response(mqtt_message, timeout=10.0)
print(f"Notes print result: success={success}, response={response}")
if success:
result = NotesPrintResponse(
success=True,
message=response.get("message", "Notes printed successfully")
)
print(f"Returning success response: {result}")
return result
else:
# Return error details from printer
# Check both 'message' and 'error' fields for error details
if response:
error_msg = response.get("message") or response.get("error", "Unknown error")
else:
error_msg = "No response from printer"
result = NotesPrintResponse(
success=False,
message=f"Print failed: {error_msg}"
)
print(f"Returning error response: {result}")
return result
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error sending notes print request: {str(e)}"
)
# Include router with /api prefix
app.include_router(router)

View File

@@ -1,8 +1,12 @@
import os
import json
import logging
from typing import Optional
import uuid
import time
from typing import Optional, Dict, Any
import paho.mqtt.client as mqtt
import threading
import atexit
logger = logging.getLogger(__name__)
@@ -12,10 +16,169 @@ MQTT_BROKER_PORT = int(os.getenv("MQTT_BROKER_PORT", "1883"))
MQTT_USERNAME = os.getenv("MQTT_USERNAME", "")
MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", "")
MQTT_LABEL_TOPIC = os.getenv("MQTT_LABEL_TOPIC", "vet/labels/print")
MQTT_STATUS_TOPIC = os.getenv("MQTT_STATUS_TOPIC", "vet/labels/status")
# Store responses by job_id
_response_store: Dict[str, Any] = {}
_response_lock = threading.Lock()
# Persistent MQTT client
_mqtt_client: Optional[mqtt.Client] = None
_client_connected = threading.Event()
_client_lock = threading.Lock()
_initialization_attempted = False
def on_connect(client, userdata, flags, rc):
"""Callback when connected to broker"""
if rc == 0:
print(f"MQTT: Connected to broker at {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}")
# Subscribe to status topic
result = client.subscribe(MQTT_STATUS_TOPIC, qos=1)
print(f"MQTT: Subscribed to {MQTT_STATUS_TOPIC}, result={result}")
_client_connected.set()
else:
print(f"MQTT: Failed to connect, return code: {rc}")
_client_connected.clear()
def on_disconnect(client, userdata, rc):
"""Callback when disconnected from broker"""
print(f"MQTT: Disconnected, return code: {rc}")
_client_connected.clear()
def on_status_message(client, userdata, message):
"""Callback for status messages"""
print(f"MQTT: Received message on topic '{message.topic}': {message.payload.decode()[:200]}")
try:
payload = json.loads(message.payload.decode())
job_id = payload.get("job_id")
if job_id:
with _response_lock:
_response_store[job_id] = payload
print(f"MQTT: Stored response for job {job_id}: {payload.get('status')}")
else:
print(f"MQTT: Message has no job_id: {payload}")
except Exception as e:
print(f"MQTT: Error processing status message: {str(e)}")
def get_mqtt_client():
"""Get or initialize the persistent MQTT client"""
global _mqtt_client, _initialization_attempted
with _client_lock:
# If client exists and is connected, return it
if _mqtt_client is not None and _client_connected.is_set():
return _mqtt_client
# If we already tried and failed recently, don't retry immediately
if _initialization_attempted and _mqtt_client is None:
return None
_initialization_attempted = True
try:
print(f"MQTT: Initializing client connection to {MQTT_BROKER_HOST}:{MQTT_BROKER_PORT}")
# Clean up old client if exists
if _mqtt_client is not None:
try:
_mqtt_client.loop_stop()
_mqtt_client.disconnect()
except:
pass
# Create new MQTT client
_mqtt_client = mqtt.Client(client_id=f"drug-inventory-main")
# Set username and password if provided
if MQTT_USERNAME and MQTT_PASSWORD:
_mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
# Set up callbacks
_mqtt_client.on_connect = on_connect
_mqtt_client.on_disconnect = on_disconnect
_mqtt_client.on_message = on_status_message
# Connect to broker
_mqtt_client.connect(MQTT_BROKER_HOST, MQTT_BROKER_PORT, 60)
# Start network loop in background
_mqtt_client.loop_start()
# Wait for connection (max 3 seconds)
if _client_connected.wait(timeout=3.0):
print("MQTT: Client initialized successfully")
return _mqtt_client
else:
print("MQTT: Connection timeout")
_mqtt_client.loop_stop()
_mqtt_client = None
return None
except Exception as e:
print(f"MQTT: Error initializing client: {str(e)}")
_mqtt_client = None
return None
def publish_label_print_with_response(label_data: dict, timeout: float = 5.0) -> tuple[bool, Optional[dict]]:
"""
Publish a label print request to MQTT broker and wait for response
Args:
label_data: Dictionary containing label print information
timeout: Maximum time to wait for response in seconds
Returns:
Tuple of (success, response_data)
"""
# Get or initialize MQTT client
client = get_mqtt_client()
if client is None:
print("MQTT: Client not available")
return False, {"status": "error", "message": "MQTT client not connected"}
job_id = str(uuid.uuid4())
label_data["job_id"] = job_id
try:
# Publish message
message = json.dumps(label_data)
result = client.publish(MQTT_LABEL_TOPIC, message, qos=1)
if result.rc != mqtt.MQTT_ERR_SUCCESS:
print(f"MQTT: Failed to publish, rc={result.rc}")
return False, {"status": "error", "message": "Failed to publish message"}
print(f"MQTT: Published job {job_id}")
# Wait for response
start_time = time.time()
while time.time() - start_time < timeout:
with _response_lock:
if job_id in _response_store:
response = _response_store.pop(job_id)
# Check if print was successful
status = response.get("status", "").lower()
success = status in ["success", "completed", "ok"]
print(f"MQTT: Job {job_id} completed with status: {status}")
return success, response
time.sleep(0.05) # Check every 50ms
# Timeout - no response received
print(f"MQTT: Timeout waiting for job {job_id}")
# Clean up in case response arrives late
with _response_lock:
_response_store.pop(job_id, None)
return False, {"status": "timeout", "message": "No response from printer"}
except Exception as e:
print(f"MQTT: Error in publish_label_print_with_response: {str(e)}")
return False, {"status": "error", "message": str(e)}
def publish_label_print(label_data: dict) -> bool:
"""
Publish a label print request to MQTT broker
Publish a label print request to MQTT broker (fire and forget)
Args:
label_data: Dictionary containing label print information

View File

@@ -5,8 +5,6 @@ services:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "${BACKEND_PORT}:8000"
volumes:
- ./backend/app:/app/app
- ./data:/app/data
@@ -14,21 +12,45 @@ services:
- DATABASE_URL=sqlite:///./data/drugs.db
- PUID=1001
- PGID=1001
- MQTT_BROKER_HOST=${MQTT_BROKER_HOST:-localhost}
- MQTT_BROKER_HOST=${MQTT_BROKER_HOST:-mosquitto}
- MQTT_BROKER_PORT=${MQTT_BROKER_PORT:-1883}
- MQTT_USERNAME=${MQTT_USERNAME:-}
- MQTT_PASSWORD=${MQTT_PASSWORD:-}
- MQTT_LABEL_TOPIC=${MQTT_LABEL_TOPIC:-vet/labels/print}
- MQTT_STATUS_TOPIC=${MQTT_STATUS_TOPIC:-vet/labels/status}
- LABEL_TEMPLATE_ID=${LABEL_TEMPLATE_ID:-vet_label}
- LABEL_SIZE=${LABEL_SIZE:-29x90}
- LABEL_TEST=${LABEL_TEST:-false}
mosquitto:
image: eclipse-mosquitto:latest
volumes:
- ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
- ./mosquitto/config/pwfile:/mosquitto/config/pwfile:ro
- mosquitto_data:/mosquitto/data
- mosquitto_logs:/mosquitto/log
environment:
- PUID=1001
- PGID=1001
frontend:
image: nginx:alpine
ports:
- "${FRONTEND_PORT}:80"
container_name: drugsprod
volumes:
- ./frontend:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- backend
- mosquitto
networks:
- default
- webapps
volumes:
mosquitto_data:
mosquitto_logs:
networks:
default:
webapps:
external: true

View File

@@ -172,14 +172,17 @@ function setupEventListeners() {
const dispenseForm = document.getElementById('dispenseForm');
const prescribeForm = document.getElementById('prescribeForm');
const editForm = document.getElementById('editForm');
const printNotesForm = document.getElementById('printNotesForm');
const addModal = document.getElementById('addModal');
const addVariantModal = document.getElementById('addVariantModal');
const editVariantModal = document.getElementById('editVariantModal');
const dispenseModal = document.getElementById('dispenseModal');
const prescribeModal = document.getElementById('prescribeModal');
const editModal = document.getElementById('editModal');
const printNotesModal = document.getElementById('printNotesModal');
const addDrugBtn = document.getElementById('addDrugBtn');
const dispenseBtn = document.getElementById('dispenseBtn');
const printNotesBtn = document.getElementById('printNotesBtn');
const cancelAddBtn = document.getElementById('cancelAddBtn');
const cancelVariantBtn = document.getElementById('cancelVariantBtn');
const cancelEditVariantBtn = document.getElementById('cancelEditVariantBtn');
@@ -202,8 +205,10 @@ function setupEventListeners() {
if (dispenseForm) dispenseForm.addEventListener('submit', handleDispenseDrug);
if (prescribeForm) prescribeForm.addEventListener('submit', handlePrescribeDrug);
if (editForm) editForm.addEventListener('submit', handleEditDrug);
if (printNotesForm) printNotesForm.addEventListener('submit', handlePrintNotes);
if (addDrugBtn) addDrugBtn.addEventListener('click', () => openModal(addModal));
if (printNotesBtn) printNotesBtn.addEventListener('click', () => openModal(printNotesModal));
if (dispenseBtn) dispenseBtn.addEventListener('click', () => {
updateDispenseDrugSelect();
openModal(dispenseModal);
@@ -216,6 +221,9 @@ function setupEventListeners() {
if (cancelPrescribeBtn) cancelPrescribeBtn.addEventListener('click', () => closeModal(prescribeModal));
if (cancelEditBtn) cancelEditBtn.addEventListener('click', closeEditModal);
const cancelPrintNotesBtn = document.getElementById('cancelPrintNotesBtn');
if (cancelPrintNotesBtn) cancelPrintNotesBtn.addEventListener('click', () => closeModal(printNotesModal));
const closeHistoryBtn = document.getElementById('closeHistoryBtn');
if (closeHistoryBtn) closeHistoryBtn.addEventListener('click', () => closeModal(document.getElementById('historyModal')));
@@ -394,7 +402,7 @@ function renderDrugs() {
</div>
</div>
<div class="variant-actions">
<button class="btn btn-primary btn-small" onclick="prescribeVariant(${variant.id}, '${drug.name.replace(/'/g, "\\'")}', '${variant.strength.replace(/'/g, "\\'")}')">🏷️ Prescribe</button>
<button class="btn btn-primary btn-small" onclick="prescribeVariant(${variant.id}, '${drug.name.replace(/'/g, "\\'")}', '${variant.strength.replace(/'/g, "\\'")}', '${variant.unit.replace(/'/g, "\\'")}')">🏷️ Prescribe & Print</button>
<button class="btn btn-success btn-small" onclick="dispenseVariant(${variant.id})">💊 Dispense</button>
<button class="btn btn-warning btn-small" onclick="openEditVariantModal(${variant.id})">Edit</button>
<button class="btn btn-danger btn-small" onclick="deleteVariant(${variant.id})">Delete</button>
@@ -702,7 +710,44 @@ async function handlePrescribeDrug(e) {
const formattedExpiry = `${expiryParts[2]}/${expiryParts[1]}/${expiryParts[0]}`;
try {
// First, dispense the drug (decrement inventory)
// First, print the label
const labelData = {
variables: {
practice_name: "Many Tears Animal Rescue",
animal_name: animalName,
drug_name: `${drugName} ${variantStrength}`,
dosage: dosage,
quantity: `${quantity} ${unit}`,
expiry_date: formattedExpiry
}
};
const labelResponse = await apiCall('/labels/print', {
method: 'POST',
body: JSON.stringify(labelData)
});
if (!labelResponse.ok) {
const error = await labelResponse.json();
throw new Error(error.detail || 'Label printing request failed');
}
const labelResult = await labelResponse.json();
console.log('Label print result:', labelResult);
if (!labelResult.success) {
// Label printing failed - don't dispense the drug
const isError = labelResult.message && (
labelResult.message.includes('not found') ||
labelResult.message.includes('error') ||
labelResult.message.includes('failed')
);
const toastType = isError ? 'error' : 'warning';
showToast('Cannot dispense: ' + labelResult.message, toastType, 5000);
return;
}
// Label printed successfully, now dispense the drug
const dispensingData = {
drug_variant_id: variantId,
quantity: quantity,
@@ -721,29 +766,8 @@ async function handlePrescribeDrug(e) {
throw new Error(error.detail || 'Failed to dispense drug');
}
// Second, print the label
const labelData = {
variables: {
practice_name: "Many Tears Animal Rescue",
animal_name: animalName,
drug_name: `${drugName} ${variantStrength}`,
dosage: dosage,
quantity: `${quantity} ${unit}`,
expiry_date: formattedExpiry
}
};
const labelResponse = await apiCall('/labels/print', {
method: 'POST',
body: JSON.stringify(labelData)
});
if (!labelResponse.ok) {
console.error('Label printing failed, but drug was dispensed');
showToast('Drug prescribed successfully, but label printing failed', 'warning', 5000);
} else {
showToast('Drug prescribed and label sent to printer!', 'success');
}
// Both operations succeeded
showToast('Drug prescribed and label printed successfully!', 'success');
document.getElementById('prescribeForm').reset();
closeModal(document.getElementById('prescribeModal'));
@@ -754,6 +778,63 @@ async function handlePrescribeDrug(e) {
}
}
// Handle print notes form submission
async function handlePrintNotes(e) {
e.preventDefault();
const animalName = document.getElementById('notesAnimalName').value.trim();
const notes = document.getElementById('notesContent').value.trim();
if (!animalName || !notes) {
showToast('Please fill in all required fields', 'warning');
return;
}
try {
// Send notes to print endpoint
const notesData = {
variables: {
animal_name: animalName,
notes: notes
}
};
const response = await apiCall('/notes/print', {
method: 'POST',
body: JSON.stringify(notesData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Notes printing request failed');
}
const result = await response.json();
console.log('Notes print result:', result);
if (!result.success) {
// Printing failed
const isError = result.message && (
result.message.includes('not found') ||
result.message.includes('error') ||
result.message.includes('failed')
);
const toastType = isError ? 'error' : 'warning';
showToast(result.message, toastType, 5000);
return;
}
// Printing succeeded
showToast('Notes printed successfully!', 'success');
document.getElementById('printNotesForm').reset();
closeModal(document.getElementById('printNotesModal'));
} catch (error) {
console.error('Error printing notes:', error);
showToast('Failed to print notes: ' + error.message, 'error');
}
}
// Delete variant
async function deleteVariant(variantId) {
if (!confirm('Are you sure you want to delete this variant?')) return;

View File

@@ -55,6 +55,7 @@
<div class="section-header">
<h2>Current Inventory</h2>
<div class="header-actions">
<button id="printNotesBtn" class="btn btn-primary btn-small">📝 Print Notes</button>
<button id="addDrugBtn" class="btn btn-primary btn-small"> Add Drug</button>
</div>
<div class="filters">
@@ -429,6 +430,30 @@
</div>
</div>
</div>
<!-- Print Notes Modal -->
<div id="printNotesModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<h2>Print Notes</h2>
<form id="printNotesForm" novalidate>
<div class="form-group">
<label for="notesAnimalName">Animal Name/ID *</label>
<input type="text" id="notesAnimalName" required>
</div>
<div class="form-group">
<label for="notesContent">Notes *</label>
<textarea id="notesContent" rows="6" placeholder="Enter notes to print..." required></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Print Notes</button>
<button type="button" class="btn btn-secondary" id="cancelPrintNotesBtn">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<script src="app.js"></script>

View File

@@ -252,7 +252,9 @@ label {
input[type="text"],
input[type="number"],
input[type="password"],
select {
input[type="date"],
select,
textarea {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
@@ -262,10 +264,17 @@ select {
transition: border-color 0.2s;
}
textarea {
resize: vertical;
min-height: 100px;
}
input[type="text"]:focus,
input[type="number"]:focus,
input[type="password"]:focus,
select:focus {
input[type="date"]:focus,
select:focus,
textarea:focus {
outline: none;
border-color: var(--secondary-color);
box-shadow: 0 0 5px rgba(52, 152, 219, 0.3);
@@ -291,7 +300,6 @@ select:focus {
.btn-primary {
background-color: var(--secondary-color);
color: var(--white);
width: 100%;
}
.btn-primary:hover {

View File

@@ -0,0 +1,20 @@
listener 1883
listener 9001
protocol websockets
persistence true
persistence_location /mosquitto/data/
# Authentication
allow_anonymous false
password_file /mosquitto/config/pwfile
# Logging
log_dest stdout
log_timestamp true
#log_type all
# Performance
max_connections -1
max_queued_messages 1000

1
mosquitto/config/pwfile Normal file
View File

@@ -0,0 +1 @@
mqtt:$7$1000$x5foCeSdbDKyb+S/CkGI37HWJEIAQil+PfMwREfPHHbRGsUwTWKcoHXcOC4l15mj1HMH8GovYvGGOVDHkOrcBA==$kXYD9LRqSkThHyWTivw8l8/NBdXvpZ9d8qwmIJcGabyrohRdkfXcEgRbEJP7sJ43r6nPX7+p1lb+nF0Actt4ww==

View File

@@ -16,5 +16,16 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /mqtt {
proxy_pass http://mosquitto:9001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}