# 08 - RouteTrack Pi — Local Web Dashboard (Flask API + Leaflet Map)

#### **Date:** December 25, 2025  
**Category:** Raspberry Pi / GPS / Flask / Leaflet / Dashboard  
**Backlink:** [07 – RouteTrack Pi — Automated Route Processing (systemd Service + Timer)](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/07-routetrack-pi-automated-route-processing-systemd-service-timer)

---

## Project Goal

This phase creates a **local web dashboard** hosted on the Pi that:

- Shows the recorded route on a map (Leaflet)
- Displays stop markers
- Shows daily summary stats
- Reads from SQLite only (safe, no DB lock risk)

RouteTrack now becomes usable *in real time* via a browser on the local network.

---

## Dashboard Architecture

<table id="bkmrk-component-purpose-fl"><thead><tr><th>Component</th><th>Purpose</th></tr></thead><tbody><tr><td>Flask app</td><td>Serves API + webpage</td></tr><tr><td>SQLite</td><td>Data source (`gps_points`, `stop_events`, `daily_summary`)</td></tr><tr><td>Leaflet</td><td>Map rendering (browser)</td></tr><tr><td>OpenStreetMap tiles</td><td>Basemap tiles</td></tr></tbody></table>

---

## Install Dashboard Dependencies (venv)

You already confirmed Flask is installed in the venv. For Leaflet, we don’t need a Python package — it’s loaded in the browser.

If you want date parsing helpers later, we can add them, but for now **keep it minimal**.

(You already did these earlier, included here for completeness.)

```bash
/opt/routetrack/venv/bin/pip install --upgrade pip
/opt/routetrack/venv/bin/pip install flask gunicorn

```

Sanity check:

```bash
/opt/routetrack/venv/bin/python -c "import flask; print('Flask OK')"

```

---

## Create Flask App Folder

```bash
sudo mkdir -p /opt/routetrack/web/templates /opt/routetrack/web/static
sudo chown -R $USER:$USER /opt/routetrack/web

```

---

## Create the Flask API App

Create:

```bash
sudo nano /opt/routetrack/web/app.py

```

Paste:

```python
<span class="token comment">#!/usr/bin/env python3</span>
<span class="token triple-quoted-string string">"""
RouteTrack Local Dashboard (Flask)
----------------------------------

Provides:
- Web UI page (Leaflet map)
- JSON API endpoints:
  - /api/summary/<date>
  - /api/points/<date>
  - /api/stops/<date>

Notes:
- This dashboard is READ-ONLY.
- It never writes to SQLite (avoids lock contention).
"""</span>

<span class="token keyword">import</span> sqlite3
<span class="token keyword">from</span> flask <span class="token keyword">import</span> Flask<span class="token punctuation">,</span> jsonify<span class="token punctuation">,</span> render_template

DB_PATH <span class="token operator">=</span> <span class="token string">"/opt/routetrack/data/routetrack.sqlite"</span>

app <span class="token operator">=</span> Flask<span class="token punctuation">(</span>__name__<span class="token punctuation">)</span>

<span class="token keyword">def</span> <span class="token function">db</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    conn <span class="token operator">=</span> sqlite3<span class="token punctuation">.</span>connect<span class="token punctuation">(</span>DB_PATH<span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>row_factory <span class="token operator">=</span> sqlite3<span class="token punctuation">.</span>Row
    <span class="token keyword">return</span> conn

<span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>route</span><span class="token punctuation">(</span><span class="token string">"/"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">index</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token keyword">return</span> render_template<span class="token punctuation">(</span><span class="token string">"index.html"</span><span class="token punctuation">)</span>

<span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>route</span><span class="token punctuation">(</span><span class="token string">"/api/summary/<day>"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_summary</span><span class="token punctuation">(</span>day<span class="token punctuation">)</span><span class="token punctuation">:</span>
    conn <span class="token operator">=</span> db<span class="token punctuation">(</span><span class="token punctuation">)</span>
    cur <span class="token operator">=</span> conn<span class="token punctuation">.</span>cursor<span class="token punctuation">(</span><span class="token punctuation">)</span>
    cur<span class="token punctuation">.</span>execute<span class="token punctuation">(</span><span class="token string">"SELECT * FROM daily_summary WHERE date = ?"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>day<span class="token punctuation">,</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    row <span class="token operator">=</span> cur<span class="token punctuation">.</span>fetchone<span class="token punctuation">(</span><span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>close<span class="token punctuation">(</span><span class="token punctuation">)</span>

    <span class="token keyword">if</span> <span class="token keyword">not</span> row<span class="token punctuation">:</span>
        <span class="token keyword">return</span> jsonify<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"No summary for this date"</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">404</span>

    <span class="token keyword">return</span> jsonify<span class="token punctuation">(</span><span class="token builtin">dict</span><span class="token punctuation">(</span>row<span class="token punctuation">)</span><span class="token punctuation">)</span>

<span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>route</span><span class="token punctuation">(</span><span class="token string">"/api/points/<day>"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_points</span><span class="token punctuation">(</span>day<span class="token punctuation">)</span><span class="token punctuation">:</span>
    conn <span class="token operator">=</span> db<span class="token punctuation">(</span><span class="token punctuation">)</span>
    cur <span class="token operator">=</span> conn<span class="token punctuation">.</span>cursor<span class="token punctuation">(</span><span class="token punctuation">)</span>

    start <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>day<span class="token punctuation">}</span></span><span class="token string">T00:00:00Z"</span></span>
    end <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>day<span class="token punctuation">}</span></span><span class="token string">T23:59:59Z"</span></span>

    cur<span class="token punctuation">.</span>execute<span class="token punctuation">(</span><span class="token triple-quoted-string string">"""
        SELECT ts, lat, lon
        FROM gps_points
        WHERE ts >= ? AND ts <= ?
          AND mode = 3
          AND lat IS NOT NULL
          AND lon IS NOT NULL
        ORDER BY ts
    """</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>start<span class="token punctuation">,</span> end<span class="token punctuation">)</span><span class="token punctuation">)</span>

    rows <span class="token operator">=</span> cur<span class="token punctuation">.</span>fetchall<span class="token punctuation">(</span><span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>close<span class="token punctuation">(</span><span class="token punctuation">)</span>

    <span class="token comment"># Return as list of [lat, lon]</span>
    points <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">[</span>r<span class="token punctuation">[</span><span class="token string">"lat"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> r<span class="token punctuation">[</span><span class="token string">"lon"</span><span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token keyword">for</span> r <span class="token keyword">in</span> rows<span class="token punctuation">]</span>
    <span class="token keyword">return</span> jsonify<span class="token punctuation">(</span>points<span class="token punctuation">)</span>

<span class="token decorator annotation punctuation">@app<span class="token punctuation">.</span>route</span><span class="token punctuation">(</span><span class="token string">"/api/stops/<day>"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_stops</span><span class="token punctuation">(</span>day<span class="token punctuation">)</span><span class="token punctuation">:</span>
    conn <span class="token operator">=</span> db<span class="token punctuation">(</span><span class="token punctuation">)</span>
    cur <span class="token operator">=</span> conn<span class="token punctuation">.</span>cursor<span class="token punctuation">(</span><span class="token punctuation">)</span>

    start <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>day<span class="token punctuation">}</span></span><span class="token string">T00:00:00Z"</span></span>
    end <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>day<span class="token punctuation">}</span></span><span class="token string">T23:59:59Z"</span></span>

    cur<span class="token punctuation">.</span>execute<span class="token punctuation">(</span><span class="token triple-quoted-string string">"""
        SELECT start_ts, end_ts, duration_seconds, lat, lon
        FROM stop_events
        WHERE start_ts >= ? AND start_ts <= ?
        ORDER BY start_ts
    """</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>start<span class="token punctuation">,</span> end<span class="token punctuation">)</span><span class="token punctuation">)</span>

    rows <span class="token operator">=</span> cur<span class="token punctuation">.</span>fetchall<span class="token punctuation">(</span><span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>close<span class="token punctuation">(</span><span class="token punctuation">)</span>

    stops <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token builtin">dict</span><span class="token punctuation">(</span>r<span class="token punctuation">)</span> <span class="token keyword">for</span> r <span class="token keyword">in</span> rows<span class="token punctuation">]</span>
    <span class="token keyword">return</span> jsonify<span class="token punctuation">(</span>stops<span class="token punctuation">)</span>

<span class="token keyword">if</span> __name__ <span class="token operator">==</span> <span class="token string">"__main__"</span><span class="token punctuation">:</span>
    <span class="token comment"># Local dev run</span>
    app<span class="token punctuation">.</span>run<span class="token punctuation">(</span>host<span class="token operator">=</span><span class="token string">"0.0.0.0"</span><span class="token punctuation">,</span> port<span class="token operator">=</span><span class="token number">5000</span><span class="token punctuation">,</span> debug<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span>

```

Make executable:

```bash
sudo chmod +x /opt/routetrack/web/app.py

```

---

## Create the Leaflet Web Page

Create:

```bash
sudo nano /opt/routetrack/web/templates/index.html

```

Paste:

```html
<span class="token doctype"><span class="token punctuation"><!</span><span class="token doctype-tag">doctype</span> <span class="token name">html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">charset</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>utf-8<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>title</span><span class="token punctuation">></span></span>RouteTrack Dashboard<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>title</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>viewport<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>width=device-width, initial-scale=1<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>

  <span class="token comment"><!-- Leaflet (CDN) --></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span>
    <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stylesheet<span class="token punctuation">"</span></span>
    <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://unpkg.com/leaflet@1.9.4/dist/leaflet.css<span class="token punctuation">"</span></span>
  <span class="token punctuation">/></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>https://unpkg.com/leaflet@1.9.4/dist/leaflet.js<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css">
    <span class="token selector">body</span> <span class="token punctuation">{</span> <span class="token property">margin</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span> <span class="token property">font-family</span><span class="token punctuation">:</span> Arial<span class="token punctuation">,</span> sans-serif<span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">#topbar</span> <span class="token punctuation">{</span> <span class="token property">padding</span><span class="token punctuation">:</span> 10px<span class="token punctuation">;</span> <span class="token property">background</span><span class="token punctuation">:</span> #111<span class="token punctuation">;</span> <span class="token property">color</span><span class="token punctuation">:</span> #fff<span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">#map</span> <span class="token punctuation">{</span> <span class="token property">height</span><span class="token punctuation">:</span> 70vh<span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">#stats</span> <span class="token punctuation">{</span> <span class="token property">padding</span><span class="token punctuation">:</span> 10px<span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">.row</span> <span class="token punctuation">{</span> <span class="token property">margin</span><span class="token punctuation">:</span> 6px 0<span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">code</span> <span class="token punctuation">{</span> <span class="token property">background</span><span class="token punctuation">:</span> #eee<span class="token punctuation">;</span> <span class="token property">padding</span><span class="token punctuation">:</span> 2px 4px<span class="token punctuation">;</span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 4px<span class="token punctuation">;</span> <span class="token punctuation">}</span>
  </span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>topbar<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>RouteTrack<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> — Local Dashboard
    <span class="token entity named-entity" title=" ">&nbsp;</span> | <span class="token entity named-entity" title=" ">&nbsp;</span>
    Date: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>day<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>date<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>button</span> <span class="token attr-name">onclick</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>loadAll()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Load<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>button</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>

  <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>map<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stats<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h3</span><span class="token punctuation">></span></span>Daily Summary<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h3</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>summary<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>h3</span><span class="token punctuation">></span></span>Stops<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>h3</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>stops<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span>
  // Default date = today (browser local time)
  const dayInput = document.getElementById("day");
  dayInput.valueAsDate = new Date();

  const map = L.map("map").setView([38.7153, -89.94], 13);

  L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
    maxZoom: 19,
    attribution: "<span class="token entity named-entity" title="©">&copy;</span> OpenStreetMap contributors"
  }).addTo(map);

  let routeLine = null;
  let stopMarkers = [];

  async function loadAll() {
    const day = dayInput.value;
    await loadRoute(day);
    await loadStops(day);
    await loadSummary(day);
  }

  async function loadRoute(day) {
    const res = await fetch(`/api/points/${day}`);
    const pts = await res.json();

    if (routeLine) map.removeLayer(routeLine);
    if (!pts.length) return;

    routeLine = L.polyline(pts, { weight: 4 }).addTo(map);
    map.fitBounds(routeLine.getBounds());
  }

  async function loadStops(day) {
    // clear old markers
    stopMarkers.forEach(m => map.removeLayer(m));
    stopMarkers = [];

    const res = await fetch(`/api/stops/${day}`);
    const stops = await res.json();

    const stopsDiv = document.getElementById("stops");
    stopsDiv.innerHTML = "";

    if (!Array.isArray(stops) || !stops.length) {
      stopsDiv.innerHTML = "<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">'</span>row<span class="token punctuation">'</span></span><span class="token punctuation">></span></span>No stops found.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>";
      return;
    }

    stops.forEach(s => {
      const durMin = Math.round(s.duration_seconds / 60);
      stopsDiv.innerHTML += `<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
        Stop: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>code</span><span class="token punctuation">></span></span>${s.start_ts}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>code</span><span class="token punctuation">></span></span> → <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>code</span><span class="token punctuation">></span></span>${s.end_ts}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>code</span><span class="token punctuation">></span></span>
        (${durMin} min)
      <span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>`;

      if (s.lat && s.lon) {
        const m = L.marker([s.lat, s.lon]).addTo(map)
          .bindPopup(`Stop (${durMin} min)<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>br</span><span class="token punctuation">></span></span>${s.start_ts}`);
        stopMarkers.push(m);
      }
    });
  }

  async function loadSummary(day) {
    const summaryDiv = document.getElementById("summary");
    summaryDiv.innerHTML = "";

    const res = await fetch(`/api/summary/${day}`);
    const data = await res.json();

    if (data.error) {
      summaryDiv.innerHTML = `<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>No summary for ${day}. Run processor first.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>`;
      return;
    }

    summaryDiv.innerHTML = `
      <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Start: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>code</span><span class="token punctuation">></span></span>${data.start_ts}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>code</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>End: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>code</span><span class="token punctuation">></span></span>${data.end_ts}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>code</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Distance: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>${data.total_distance_miles}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> miles<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Moving: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>${Math.round(data.moving_time_seconds/60)}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> minutes<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Stopped: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>${Math.round(data.stopped_time_seconds/60)}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> minutes<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>row<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Stops: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>${data.stop_count}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
    `;
  }

  // Auto-load on page open
  loadAll();
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>html</span><span class="token punctuation">></span></span>

```

---

## Run the Dashboard (Manual Test)

```bash
/opt/routetrack/venv/bin/python /opt/routetrack/web/app.py

```

Then browse from your LAN:

- `http://<PI-IP>:5000`

Find your Pi IP:

```bash
hostname -I

```

Stop the server with `Ctrl+C`.

---

## Next Step (After Manual Test)

Next phase is productionizing the dashboard:

- systemd service for Flask (Gunicorn)
- optional Nginx reverse proxy
- optional local authentication
- optional “live view” tracking