131 lines
4.8 KiB
Python
131 lines
4.8 KiB
Python
"""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 """
|
|
<html>
|
|
<head>
|
|
<title>Mileage Logger</title>
|
|
</head>
|
|
<body>
|
|
<h1>Mileage Logger</h1>
|
|
<p>Select a Google Takeout JSON file to process. The file
|
|
should contain the "timelineObjects" array from your Semantic
|
|
Location History export.</p>
|
|
<form action="/process" method="post" enctype="multipart/form-data">
|
|
<input type="file" name="file" accept="application/json" required />
|
|
<br/><br/>
|
|
<label for="vehicle">Vehicle description:</label>
|
|
<input type="text" id="vehicle" name="vehicle" value="SH11 DRV (Own 1.6CC Diesel Car/Van)" />
|
|
<br/><br/>
|
|
<label for="job_role">Job role:</label>
|
|
<input type="text" id="job_role" name="job_role" value="ICT Technician" />
|
|
<br/><br/>
|
|
<input type="submit" value="Process" />
|
|
</form>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
@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) |