"""Simple web GUI for the mileage logger. This module exposes a FastAPI application that wraps the core functionality of the mileage logger with a minimal HTML front end. It allows a user to upload a Google Semantic Location History JSON file and returns an Excel workbook containing their mileage claims. The application also renders a basic status page showing the detected itinerary. Usage ----- Run the server using uvicorn: ``` uvicorn mileage_logger.gui:app --reload --port 8000 ``` Then navigate to ``http://localhost:8000`` in your web browser. Use the form to upload a JSON export. After processing, the server will return an Excel file for download. Limitations ----------- This GUI is intentionally lightweight and is not designed for concurrent multi-user access. It does not persist files on disk and does not perform any authentication or authorisation. For production use consider extending it with proper user management and storage. """ from __future__ import annotations import json import os import tempfile from io import BytesIO from typing import Dict, List from fastapi import FastAPI, File, Form, UploadFile from fastapi.responses import HTMLResponse, FileResponse, StreamingResponse from .ingest.semantic_reader import load_place_visits from .logic.detect_itinerary import SiteConfig, detect_itinerary from .distance.resolve import DistanceResolver from .export.excel_writer import build_monthly_rows, write_monthly_workbook # Load configuration once at startup. You can change the path to # config/sites.yml if you have customised it. The route catalogue is # loaded on-demand when handling uploads. DEFAULT_SITE_CONFIG_PATH = os.path.join(os.path.dirname(__file__), "../config/sites.yml") DEFAULT_ROUTE_CSV_PATH = os.path.join(os.path.dirname(__file__), "../tests/data/routes_golden.csv") site_config: SiteConfig = SiteConfig.from_yaml(DEFAULT_SITE_CONFIG_PATH) app = FastAPI(title="Mileage Logger GUI") @app.get("/", response_class=HTMLResponse) async def index() -> str: """Render a simple upload form.""" return """ Mileage Logger

Mileage Logger

Select a Google Takeout JSON file to process. The file should contain the "timelineObjects" array from your Semantic Location History export.







""" @app.post("/process") async def process_file( file: UploadFile = File(...), vehicle: str = Form("SH11 DRV (Own 1.6CC Diesel Car/Van)"), job_role: str = Form("ICT Technician"), ) -> StreamingResponse: """Handle upload and return an Excel workbook. The uploaded file is saved to a temporary file on disk and then passed through the existing CLI pipeline. The resulting workbook contains one sheet per month and is returned as a streaming response. """ # Persist upload to a temporary file with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as tmp_in: contents = await file.read() tmp_in.write(contents) tmp_in.flush() input_path = tmp_in.name # Parse visits and detect itinerary visits = load_place_visits(input_path) hops = detect_itinerary(visits, site_config) resolver = DistanceResolver(route_csv_path=DEFAULT_ROUTE_CSV_PATH, vehicle_label=vehicle, job_role=job_role) rows_by_month = build_monthly_rows(hops, site_config, resolver) # Write workbook to in-memory buffer output_stream = BytesIO() # Use openpyxl to write into BytesIO via our helper # Since write_monthly_workbook writes to a file, create another temp file with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as tmp_out: write_monthly_workbook(rows_by_month, tmp_out.name) tmp_out.flush() # Read the file back into memory tmp_out.seek(0) data = tmp_out.read() output_stream.write(data) # Cleanup temporary files try: os.remove(input_path) except Exception: pass # Prepare response output_stream.seek(0) filename = "mileage.xlsx" headers = {"Content-Disposition": f"attachment; filename={filename}"} return StreamingResponse(output_stream, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers=headers)