commit bbd720e2a2eb28113c95de35d18787923d6ffc6f Author: kamaji Date: Mon Jan 26 04:57:20 2026 -0600 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74f16b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +data/ +*.swp +*.hot +*.bak +*.bak2 +__pycache__/ diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..330f254 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py . + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..c9e5315 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,110 @@ +# main.py +from fastapi import FastAPI, Form, UploadFile, File +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from filelock import FileLock +import aiofiles +import httpx +import os +from datetime import datetime +import openpyxl + +# ------------------------- +# Configuration +# ------------------------- +NEXTCLOUD_BASE_URL = "https://nextcloud.sdanywhere.com/remote.php/webdav/APs" +NEXTCLOUD_USER = "kamaji" +NEXTCLOUD_PASSWORD = "DvB0U2Uj3tOJaD" + +EXCEL_FILENAME = "ap_data.xlsx" +LOCK_FILENAME = f"{EXCEL_FILENAME}.lock" + +app = FastAPI() + +# Allow CORS for frontend (if needed) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +# ------------------------- +# Utility: Upload file to Nextcloud +# ------------------------- +async def upload_to_nextcloud(local_path, remote_name): + url = f"{NEXTCLOUD_BASE_URL}/{remote_name}" + async with httpx.AsyncClient() as client: + with open(local_path, "rb") as f: + r = await client.put(url, content=f, auth=(NEXTCLOUD_USER, NEXTCLOUD_PASSWORD)) + r.raise_for_status() + +# ------------------------- +# Utility: Save Excel safely +# ------------------------- +def append_to_excel(data_row): + lock = FileLock(LOCK_FILENAME) + with lock: + if not os.path.exists(EXCEL_FILENAME): + wb = openpyxl.Workbook() + ws = wb.active + ws.append(["Timestamp", "Site ID", "AP Location", "Cable Length", "Serial Number"]) + wb.save(EXCEL_FILENAME) + + wb = openpyxl.load_workbook(EXCEL_FILENAME) + ws = wb.active + ws.append(data_row) + wb.save(EXCEL_FILENAME) + +# ------------------------- +# Endpoint +# ------------------------- +@app.post("/api/ap/submit") +async def submit_ap( + site_id: str = Form(...), + ap_location: str = Form(...), + cable_length: float = Form(...), + serial_number: str = Form(...), + photo_continuity: UploadFile = File(...), + photo_length: UploadFile = File(...), + photo_verification: UploadFile = File(...), + photo_close: UploadFile = File(...), + photo_distance: UploadFile = File(...), +): + # Capitalize serial number + serial_number = serial_number.upper() + + # Create folder locally before upload + folder_name = f"AP{ap_location}" + os.makedirs(folder_name, exist_ok=True) + + # Save photos locally first + photo_mapping = { + "continuity": photo_continuity, + "length": photo_length, + "verification": photo_verification, + "close": photo_close, + "distance": photo_distance + } + saved_files = [] + for key, file in photo_mapping.items(): + filename = f"AP{ap_location}_{key}.jpg" + local_path = os.path.join(folder_name, filename) + async with aiofiles.open(local_path, "wb") as out_file: + content = await file.read() + await out_file.write(content) + saved_files.append((local_path, filename)) # store for upload + + # Append data to Excel + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + data_row = [timestamp, site_id, ap_location, cable_length, serial_number] + append_to_excel(data_row) + + # Upload Excel to Nextcloud + await upload_to_nextcloud(EXCEL_FILENAME, EXCEL_FILENAME) + + # Upload photos to Nextcloud + for local_path, remote_name in saved_files: + await upload_to_nextcloud(local_path, remote_name) + + return JSONResponse(content={"site_id": site_id, "ap_location": ap_location}) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..b1aca7a --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +python-multipart +openpyxl +portalocker diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..70d7ca4 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3.9" + +services: + backend: + build: ./backend + container_name: ap-backend + volumes: + - ./data:/data + expose: + - "8000" + + nginx: + image: nginx:stable + container_name: ap-nginx + ports: + - "9090:80" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/htpasswd:/etc/nginx/htpasswd:ro + - ./frontend:/usr/share/nginx/html:ro + depends_on: + - backend diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..853642e --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,154 @@ + + + + + +AP Installation Capture + + + +

AP Installation Capture

+ +
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + diff --git a/nginx/htpasswd b/nginx/htpasswd new file mode 100644 index 0000000..1658486 --- /dev/null +++ b/nginx/htpasswd @@ -0,0 +1,2 @@ +technician:$apr1$w4Ix.MV6$WR0HTzCZOkVHJOs/8ndHv/ + diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..8228fb4 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,27 @@ +events {} + +http { + # Allow larger file uploads + client_max_body_size 50M; + + server { + listen 80; + + # Basic authentication + auth_basic "AP Capture"; + auth_basic_user_file /etc/nginx/htpasswd; + + # Serve static frontend + location / { + root /usr/share/nginx/html; + index index.html; + } + + # Proxy API requests to backend + location /api/ { + proxy_pass http://backend:8000/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + } +}