# 06 - Raspberry Pi, Python & Linux Tips

# Project: RouteTrack Pi

This is a project of mine using a Raspberri Pi 3 B+ and a GPS Dongle.

# 00 - RouteTrack Pi — Project Overview

#### **Project Name:** RouteTrack Pi  
**Category:** Embedded Linux · GPS Telemetry · Data Processing · Flask  
**Platform:** Raspberry Pi  
**Status:** Active / Production-Ready Prototype

---

## Project Summary

RouteTrack Pi is a **vehicle-mounted, headless GPS telemetry and route analysis system** built on a Raspberry Pi.  
It continuously logs GPS data, processes that data into meaningful metrics (mileage, stops, time-on-site), and presents results through a **local web dashboard** — all without requiring constant internet connectivity.

The system is designed with **reliability, recoverability, and real-world vehicle use** in mind:

- Frequent power loss
- Offline operation
- GPS signal dropouts
- Long-running background services
- Minimal SD card wear

This project mirrors architectural patterns used in **fleet tracking and telemetry systems**, scaled down to run on low-power hardware.

---

## Core Capabilities

- Continuous GPS route logging
- Accurate mileage calculation (Haversine)
- Stop detection &amp; time-on-site tracking
- Daily route summaries
- Shift-based session tracking
- Local interactive map dashboard (Leaflet)
- Fully automated background services
- Offline-first, local-only data storage
- Safe daily processing via systemd timers

---

## Hardware Used

<table id="bkmrk-component-descriptio"><thead><tr><th>Component</th><th>Description</th></tr></thead><tbody><tr><td>**Raspberry Pi 3 B+**</td><td>Primary compute platform</td></tr><tr><td>**GlobalSat BU-353N USB GPS Receiver**</td><td>High-sensitivity USB GPS (NMEA 0183)</td></tr><tr><td>**128 GB microSD card**</td><td>OS + data storage</td></tr><tr><td>**Always-on cooling fan**</td><td>Wired directly to 5V rail for vehicle use</td></tr><tr><td>**Vehicle USB power**</td><td>Portable, ignition-controlled</td></tr></tbody></table>

**Why this GPS receiver:**  
The BU-353N is widely supported on Linux, requires no proprietary drivers, and is well-suited for mobile deployments.

---

## Operating System &amp; Base Software

- **Raspberry Pi OS Lite (64-bit)**
- Headless (no desktop environment)
- SSH-only administration
- NetworkManager for Wi-Fi management
- systemd for all service orchestration

---

## Networking Design

- Multiple saved Wi-Fi networks
- Automatic failover:
    
    
    - Home Wi-Fi (higher priority)
    - Phone hotspot (fallback)
- Seamless switching without reboot
- Fully functional offline

---

## GPS Subsystem Architecture

```powershell
USB GPS Receiver
   ↓
Linux USB-Serial Driver
   ↓
/dev/gps0 (udev symlink)
   ↓
gpsd-standalone.service
   ↓
TCP localhost:2947
   ↓
RouteTrack Logger
```

### Key Design Choices

- **gpsd socket activation disabled**
- **Dedicated standalone gpsd service**
- Fixed baud rate (4800)
- Stable device path using udev
- TCP access instead of UNIX socket (permission stability)

### GPS Service

- **Service Name:** `gpsd-standalone.service`
- **Binary:** `/usr/sbin/gpsd`
- **Device:** `/dev/gps0`
- **Port:** `127.0.0.1:2947`

---

## RouteTrack Directory Layout

All project files live under a single, predictable root:

```powershell
/opt/routetrack/
├── bin/        # Executable scripts
├── data/       # SQLite database and exports
├── logs/       # File-based logs (future use)
├── config/     # Database schema and config files
├── venv/       # Python virtual environment
└── web/        # Flask dashboard application
```

---

## Python Virtual Environment

- **Location:** `/opt/routetrack/venv`
- Isolates RouteTrack dependencies
- Avoids modifying OS-managed Python
- Used by all RouteTrack services

### Installed Python Packages

- `flask`
- `gunicorn`
- Standard library only for GPS, SQLite, math

---

## Data Storage (SQLite)

### Database File

```powershell
/opt/routetrack/data/routetrack.sqlite
```

### Tables

<table id="bkmrk-table-purpose-gps_po"><thead><tr><th>Table</th><th>Purpose</th></tr></thead><tbody><tr><td>`gps_points`</td><td>Raw GPS telemetry (append-only)</td></tr><tr><td>`stop_events`</td><td>Derived stationary events</td></tr><tr><td>`daily_summary`</td><td>Aggregated per-day metrics</td></tr><tr><td>`shifts`</td><td>User-controlled session boundaries</td></tr></tbody></table>

### Design Philosophy

- SQLite in WAL mode
- Raw data is **never modified**
- Derived data is **fully regenerable**
- Optimized for long-running, low-power systems

---

## GPS Logging Service

### Script

```powershell
/opt/routetrack/bin/routetrack-logger.py
```

### systemd Service

```powershell
routetrack-logger.service
```

### Responsibilities

- Connect to gpsd via TCP
- Subscribe to JSON stream
- Filter TPV messages
- Batch inserts into SQLite
- Auto-reconnect on failures
- Journald logging

**Why this matters:**  
The logger is intentionally lightweight and resilient — it prioritizes **never stopping**, even during GPS hiccups or reboots.

---

## 🧠 Route Processing &amp; Intelligence

### Processing Script

```powershell
/opt/routetrack/bin/routetrack-process.py
```

### What It Calculates

- Mileage (Haversine distance)
- Moving vs stopped time
- Stop events (speed + dwell)
- Daily summaries

### Key Rules

- Only `mode = 3` fixes are trusted
- GPS drift filtered
- Speed thresholds applied
- Stop dwell time enforced

---

## Automated Processing (systemd)

### Wrapper Script

```powershell
/opt/routetrack/bin/routetrack-run-processor.sh
```

### Services

- `routetrack-processor.service` (oneshot)
- `routetrack-processor.timer` (daily)

### Automation Workflow

1. Stop logger (release DB lock)
2. Run processor
3. Restart logger

This ensures **zero SQLite locking issues**.

---

## Local Web Dashboard

### Stack

- Flask (read-only API)
- Gunicorn (production WSGI)
- Leaflet + OpenStreetMap

### Dashboard Files

```powershell
/opt/routetrack/web/
├── app.py
├── templates/
│   └── index.html
└── static/
```

### API Endpoints

- `/api/points/<date>`
- `/api/stops/<date>`
- `/api/summary/<date>`

### systemd Service

```powershell
routetrack-dashboard.service
```

- Starts on boot
- Auto-restarts
- LAN-accessible
- No database writes (safe concurrency)

---

## Shift Mode

Shift Mode introduces **user-defined session boundaries** independent of calendar days.

### Table

```powershell
shifts
```

### Purpose

- Accurate per-shift metrics
- Handles overnight work
- Prevents test runs from polluting data
- Designed for vehicle usage patterns

---

## Reliability &amp; Operational Design

- All components managed by **systemd**
- Centralized logging via `journalctl`
- Safe service dependencies
- No cron jobs
- SD-card friendly writes
- Offline-first operation
- Power-loss tolerant

---

## Why This Project Matters

RouteTrack Pi demonstrates:

- Embedded Linux service orchestration
- Real-world GPS data handling
- Data pipeline design
- SQLite optimization
- systemd automation
- Web API + dashboard integration
- Production-style architecture on constrained hardware

This is **not a script** - it’s a **system**.

---

## Future Enhancements (Planned)

- GeoJSON / CSV exports
- VPS sync &amp; backups
- Map styling improvements
- GPS health watchdogs
- Authentication (optional)
- Historical route replay

# 01 - RouteTrack Pi — Initial Setup & Networking

#### **Date:** December 21st, 2025   
**Category:** Raspberry Pi / Linux / Networking  
**Backlink:** [RouteTrack Pi Overview](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/chapter/project-routetrack-pi)

---

## Project Overview

This page documents the **initial setup and networking foundation** for a Raspberry Pi–based GPS logging and mapping project designed for in-vehicle use.

The long-term goal of this project is to build a reliable, headless system capable of:

- GPS route logging
- Automatic stop detection
- Time-on-site tracking
- Mileage calculation
- Syncing data to a VPS for web-based visualization

This entry focuses on **hardware bring-up, OS selection, headless access, and resilient Wi-Fi configuration**.

---

## Hardware Used (Initial Phase)

- **Raspberry Pi 3 B+**
- **128 GB microSD card**
- Active **cooling fan** (wired directly to 5 V)
- Phone hotspot (temporary network access)
- Home Wi-Fi (persistent network access)
    
    <table border="1" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 33.244%;"></col><col style="width: 33.244%;"></col><col style="width: 33.244%;"></col></colgroup><tbody><tr><td>[![PXL_20251221_143834980.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/JjA9WeUs2g6asUud-pxl-20251221-143834980.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/JjA9WeUs2g6asUud-pxl-20251221-143834980.jpg)</td><td>[![PXL_20251221_144014396.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/XQ2hmktEk5WegRrm-pxl-20251221-144014396.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/XQ2hmktEk5WegRrm-pxl-20251221-144014396.jpg)</td><td>[![PXL_20251221_144535466.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/veLerZmba0dE9ADw-pxl-20251221-144535466.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/veLerZmba0dE9ADw-pxl-20251221-144535466.jpg)</td></tr><tr><td>[![PXL_20251221_144757810.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/a7kQFDPh3KSMo2x2-pxl-20251221-144757810.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/a7kQFDPh3KSMo2x2-pxl-20251221-144757810.jpg)</td><td>[![PXL_20251221_150315849.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/y6Tx5mk6S50mtusc-pxl-20251221-150315849.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/y6Tx5mk6S50mtusc-pxl-20251221-150315849.jpg)</td><td>[![PXL_20251221_150509809.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/8bmBrtif7jwsLIqk-pxl-20251221-150509809.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/8bmBrtif7jwsLIqk-pxl-20251221-150509809.jpg)</td></tr><tr><td>[![PXL_20251221_151938820.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/iGeLchRjz3xVX1Rg-pxl-20251221-151938820.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/iGeLchRjz3xVX1Rg-pxl-20251221-151938820.jpg)</td><td>[![PXL_20251221_151936565.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/vBKRIKJgmbFLzwmS-pxl-20251221-151936565.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/vBKRIKJgmbFLzwmS-pxl-20251221-151936565.jpg)</td><td>[![PXL_20251221_152951942.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/YzM5K8QT3rfRoRWL-pxl-20251221-152951942.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/YzM5K8QT3rfRoRWL-pxl-20251221-152951942.jpg)</td></tr></tbody></table>

---

## Cooling &amp; Power Verification

The cooling fan was wired directly to the Raspberry Pi’s 5 V rail:

- **Pin 4** → 5 V (red wire)
- **Pin 6** → Ground (black wire)

Results:

- Fan spins immediately on power-up
- No GPIO or software control required
- Consistent airflow suitable for a vehicle environment

An always-on fan was chosen for simplicity and reliability.

---

## Operating System Selection

**Installed OS:**

- **Raspberry Pi OS Lite (64-bit)**

**Reasoning:**

- Lower memory overhead (ideal for Pi 3 B+ with 1 GB RAM)
- Headless by design (no desktop services running)
- Best compatibility with:
    
    
    - `gpsd`
    - Python and Node.js tooling
    - systemd services
    - Networking utilities
- Proven stability for long-running deployments

The OS was written using **Raspberry Pi Imager** with Advanced Options enabled:

- SSH enabled
- Username and password set
- Wi-Fi configured for initial hotspot access
- Custom hostname configured (`pi-gps`)

---

## Headless SSH Access

After first boot:

- The Pi appeared on the phone hotspot
- SSH access was established using **JuiceSSH (Android)**
- No monitor or keyboard was required

This confirmed:

- OS integrity
- Network stack functionality
- Full remote administration capability

---

## Wi-Fi Management (Scanning, Adding, Deleting)

This project uses **NetworkManager** on Raspberry Pi OS, so Wi-Fi is managed using the `nmcli` command-line tool.

### Scan for available Wi-Fi networks

```bash
sudo nmcli connection show
```

```bash
sudo nmcli dev wifi rescan
sudo nmcli dev wifi list
```

[![2025-12-21 13_39_55-Greenshot.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/nz9sCXRdIl4D8kU4-2025-12-21-13-39-55-greenshot.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/nz9sCXRdIl4D8kU4-2025-12-21-13-39-55-greenshot.png)

### Check current network status

```bash
sudo nmcli device status
sudo nmcli -f GENERAL.CONNECTION,GENERAL.STATE dev show wlan0
```

### Add / connect to a Wi-Fi network

(This also saves the network for future use.)

```bash
sudo nmcli dev wifi connect "SSID" password "PASSWORD"
```

If the SSID is hidden:

```bash
sudo nmcli dev wifi connect "SSID" password "PASSWORD" hidden yes
```

### List saved Wi-Fi connections

```bash
sudo nmcli -f NAME,TYPE,DEVICE connection show
```

### Switch networks manually (useful for testing)

```bash
sudo nmcli connection up "HomeWiFi"
# or
sudo nmcli connection up "PhoneHotspot"
```

### Rename a saved connection

(Helps keep connection names readable.)

```bash
sudo nmcli connection modify "OldName" connection.id "NewName"
```

### Delete a saved Wi-Fi connection

Delete by **connection NAME** (from `nmcli connection show`), not necessarily the SSID.

```bash
sudo nmcli connection delete "ConnectionName"
```

### Set auto-connect priorities

(Higher number = preferred when multiple known networks are available.)

```bash
sudo nmcli connection modify "HomeWiFi" connection.autoconnect yes
sudo nmcli connection modify "HomeWiFi" connection.autoconnect-priority 10

sudo nmcli connection modify "PhoneHotspot" connection.autoconnect yes
sudo nmcli connection modify "PhoneHotspot" connection.autoconnect-priority 1
```

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/M1YZElVIL1o8yoJO-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/M1YZElVIL1o8yoJO-image.png)

### Restart networking (if things get weird)

```bash
sudo systemctl restart NetworkManager
```

## Multi-Wi-Fi Configuration

The Pi is intended to operate across **multiple networks**:

- Home Wi-Fi when taken home
- Phone hotspot while mobile

Wi-Fi management is handled by **NetworkManager**, allowing:

- Multiple saved Wi-Fi profiles
- Automatic reconnection
- Priority-based network selection

Saved connections:

- **HomeWiFi**
- **PhoneHotspot**

Verified using:

```bash
sudo nmcli connection show
```

---

## Automatic Network Failover

Network priorities were configured to prefer home Wi-Fi:

```bash
sudo nmcli connection modify HomeWiFi connection.autoconnect-priority 10
sudo nmcli connection modify PhoneHotspot connection.autoconnect-priority 1
```

Behavior:

- Home Wi-Fi is preferred when available
- Phone hotspot is used automatically when mobile
- Switching occurs without reboot or manual intervention

Failover was verified by disabling the hotspot and confirming the Pi automatically connected to the home network.

---

## Wi-Fi Band Notes (Pi 3 B+)

- The Pi 3 B+ supports **dual-band Wi-Fi (2.4 GHz and 5 GHz)**
- 5 GHz networks require:
    
    
    - Correct WLAN country configuration
    - Non-DFS channels (36–48)
- 2.4 GHz is preferred for mobile hotspot reliability
- Both bands were successfully tested during setup

---

## Current Status

At this stage, the system has a solid foundation:

- OS installed and verified
- SSH access confirmed
- Cooling operational
- Multiple Wi-Fi networks configured
- Automatic failover tested and working
- Network behavior stable for mobile use

The Raspberry Pi is now **ready for GPS hardware integration**.

---

## Next Steps

Upcoming phases will document:

- GPS hardware installation
- `gpsd` configuration and testing
- Route and stop logging
- Mileage calculation
- Local web UI
- VPS synchronization and map visualization

# 02 - RouteTrack Pi — Connecting GPS Hardware

#### **Date:** December 24th, 2025  
**Category:** Raspberry Pi / GPS / Hardware  
**Backlink:** [RouteTrack Pi – Initial Setup &amp; Networking](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/routetrack-pi-initial-setup-networking)

---

## Project Context

This page documents the **physical GPS hardware selection and connection phase** of the RouteTrack Pi project.

At this stage, the Raspberry Pi has:

- A verified Raspberry Pi OS Lite (64-bit) installation
- Stable headless SSH access
- Multiple Wi‑Fi networks configured with automatic failover
- Reliable cooling and power

The goal of this phase is to **introduce the GPS hardware only**, verify that it is detected correctly by the operating system, and prepare the system for GPS daemon (`gpsd`) integration in the next phase.

No GPS services are configured on this page.

---

## GPS Hardware Used

**Device:** GlobalSat BU‑353N USB GPS Receiver

The GlobalSat BU‑353N was selected due to its long-standing Linux compatibility, high sensitivity, and suitability for vehicle-based deployments.

Key characteristics:

- USB-powered (no external power required)
- High-sensitivity GPS receiver
- NMEA 0183 output
- Built‑in magnetic mount for vehicle use
- Water‑resistant housing (IPX6)
- Wide operating temperature range
- Native compatibility with Linux, macOS, Windows, and Android

This model is commonly used with `gpsd` and does not require proprietary drivers.

---

## GPS Hardware Photos

The following photos document the exact GPS hardware used for this project.

**Photos included:**

- Retail packaging (front)
- Retail packaging (specifications)
- OS compatibility indicators
- Model and part number label

<table border="1" id="bkmrk--3" style="border-collapse: collapse; width: 100%;"><colgroup><col style="width: 50%;"></col><col style="width: 50%;"></col></colgroup><tbody><tr><td>[![PXL_20251222_220732944.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/adbV2WUpnoVr6cbW-pxl-20251222-220732944.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/adbV2WUpnoVr6cbW-pxl-20251222-220732944.jpg)

</td><td>[![PXL_20251222_220738714.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/OZCrnH4EXhouSKFC-pxl-20251222-220738714.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/OZCrnH4EXhouSKFC-pxl-20251222-220738714.jpg)

</td></tr><tr><td>[![PXL_20251222_220742867.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/fX1tAXM1I0q2aARk-pxl-20251222-220742867.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/fX1tAXM1I0q2aARk-pxl-20251222-220742867.jpg)

</td><td>[![PXL_20251222_220752135.jpg](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/2hEHwo0eVPx9UtCM-pxl-20251222-220752135.jpg)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/2hEHwo0eVPx9UtCM-pxl-20251222-220752135.jpg)

</td></tr></tbody></table>

---

## Physical Connection

The GPS receiver was connected directly to the Raspberry Pi using a standard USB port.

Connection notes:

- No drivers were installed manually
- The device powered on immediately upon connection
- No GPIO wiring or configuration was required
- The receiver will ultimately be mounted in a vehicle using the integrated magnetic base

At this point, the GPS device is physically present but not yet consumed by any software services.

---

## USB Device Detection

After connecting the GPS receiver, the system was checked to ensure that the USB device was detected correctly by the Linux kernel.

The following commands are used to validate USB detection:

```bash
lsusb
```

```bash
ls -l /dev/ttyUSB*
```

```bash
dmesg | grep -i tty
```

Expected results:

- The GlobalSat device appears in `lsusb`
- A serial device (typically `/dev/ttyUSB0`) is created
- Kernel messages indicate a USB‑to‑serial adapter attachment

Here is the results of those commands:

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/q5S8aMnBfZruJBvh-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/q5S8aMnBfZruJBvh-image.png)

---

## GPS Data Flow Overview

Before configuring any services, it is important to understand the intended data flow:

```bash
USB GPS Receiver
      ↓
Linux USB‑Serial Driver
      ↓
/dev/ttyUSB*
      ↓
gpsd
      ↓
Applications / Logging / Web UI
```

This project uses `gpsd` as the central interface between raw GPS data and higher‑level applications.

Configuration and validation of `gpsd` will be covered in the next phase.

---

## Current Status

At the conclusion of this phase:

- GPS hardware has been physically connected
- USB device detection has been verified
- No drivers or custom configuration were required
- No GPS services have been enabled yet

The system is now ready for GPS daemon installation and validation.

---

## Next Steps

The next phase of the project will cover:

- Installing `gpsd` and GPS client utilities
- Verifying socket activation
- Confirming live GPS fixes using `gpspipe`
- Validating TPV and satellite data

# 03 - RouteTrack Pi — gpsd Installation & GPS Validation

#### **Date:** December 24th, 2025  
**Category:** Raspberry Pi / GPS / Linux Services  
**Backlink:** [RouteTrack Pi — Connecting GPS Hardware](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/02-routetrack-pi-connecting-gps-hardware)

---

## Goal

This page covers:

- Installing `gpsd` and GPS tools (Global Positioning Satellite Daemon Tools)
- Confirming the USB GPS receiver is outputting raw NMEA sentences
- Creating a stable `/dev/gps0` device symlink via udev rules
- Running `gpsd` reliably as a **standalone systemd service**
- Validating that GPS data is readable through `gpspipe`
- I had to use a stand alone service because `gpsd` wouldn't work out of the box with my GPS Reciever.
- This is a workaround for that on this page.
- Some GPS units will work without bypassing TTP and using a seperate standalone systemd service.
- This page is specific to the GPS I used that needed a workaround.

#### **Device:** GlobalSat BU‑353N USB GPS Receiver

---

## Install gpsd + tools

Run:

```bash
sudo apt update
sudo apt install -y gpsd gpsd-clients
```

This installs:

- `gpsd` (daemon)
- `gpspipe`, `cgps` (client tools)

---

## Confirm the GPS receiver is detected

Plug in the USB GPS receiver, then verify the device appears:

```bash
ls -l /dev/ttyUSB*
```

example:

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/KxVqI2dyRiXtFMU9-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/KxVqI2dyRiXtFMU9-image.png)

Linux sees the GPS receiver.

---

## Validate raw NMEA output from the GPS

Before involving gpsd, verify the GPS is actually transmitting:

```bash
sudo stty -F /dev/ttyUSB0 4800 cs8 -cstopb -parenb -ixon -ixoff -crtscts -echo
sudo timeout 8 cat /dev/ttyUSB0 | head -n 20
```

Expected output should look like:

```text
$GPGGA,...
$GPGSA,...
$GPRMC,...

```

If you see NMEA sentences, the receiver is working at **4800 baud**.

---

## Create a stable GPS symlink `/dev/gps0` (udev rule)

The GPS device may sometimes appear as a different tty device, so we create a stable symlink called `/dev/gps0`.

### Create the udev rule

```bash
sudo nano /etc/udev/rules.d/10-gps-pl2303.rules
```

Paste:

```text
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="23a3", SYMLINK+="gps0"
```

These id's come from udev's info:

- `idVendor=067b`
- `idProduct=23a3`

Reload udev and trigger:

```bash
sudo udevadm control --reload-rules
sudo udevadm trigger
sudo udevadm settle
```

Verify:

```bash
ls -l /dev/gps0
```

Expected:

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/bRye2nBFV92WFIka-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/bRye2nBFV92WFIka-image.png)

---

## Disable gpsd.socket and build a standalone gpsd service

Socket activation can cause inconsistent behavior during testing, so this project uses a dedicated standalone systemd service.

### Disable/mask the socket unit

```bash
sudo systemctl disable --now gpsd.socket
sudo systemctl mask gpsd.socket
```

---

## Create `gpsd-standalone.service`

Create the service:

```bash
sudo nano /etc/systemd/system/gpsd-standalone.service
```

This is the code for setting up the new service:

```ini
[Unit]
Description=GPSD Standalone (RouteTrack)
After=network.target
Wants=network.target

[Service]
Type=simple
User=gpsd
Group=dialout
ExecStart=/usr/sbin/gpsd -N -n -b -s 4800 -S 2947 /dev/gps0
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target

```

### Important Notes

- `-s 4800` forces correct baud rate
- `-S 2947` binds gpsd to port 2947 (localhost)
- We intentionally **do not use** `-F /run/gpsd.sock` because it caused permission errors for the `gpsd` user under systemd.
- This service uses `/dev/gps0` so the device name stays consistent.

Enable + start:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now gpsd-standalone.service
```

Check status:

```bash
systemctl status gpsd-standalone.service --no-pager -l
```

**Notice it is enabled, active and running the line from the ExecStart:**

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/pJUOP9mxsosnWHDu-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/pJUOP9mxsosnWHDu-image.png)

---

## Confirm gpsd is listening on port 2947

Run:

```bash
ss -ltnp | grep 2947
```

It is listening on ipv4 and ipv6 ports:

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/3A4nRGQPpGNF0kIR-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/3A4nRGQPpGNF0kIR-image.png)

---

## Validate GPS data through gpsd (JSON output)

Run:

```bash
gpspipe -w -n 25
```

✅ Expected:

- `TPV` messages
- `SKY` messages
- `mode: 3` when a fix is established
- latitude/longitude values updating

Example indicators showing some GPS coordinates:

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/PA3lEqgczyiP5lUy-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/PA3lEqgczyiP5lUy-image.png)

---

Verifying Satellites and that everything is working:  
  
use:

```powershell
cgps
```

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/jFpCHUAAECJvmyEj-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/jFpCHUAAECJvmyEj-image.png)

### Next Steps

The GPS subsystem is now stable, validated, and running as a dedicated systemd service. Upcoming work will build on this foundation:

- **Integrate gpsd with RouteTrack**
    
    
    - Consume GPS data via localhost port `2947`
    - Parse TPV updates for real-time position tracking
- **Add health checks**
    
    
    - Monitor gpsd service status
    - Alert if GPS fix drops below `mode: 3`
- **Visualization**
    
    
    - Map GPS coordinates to a live or logged route view
    - Export NMEA or JSON logs for later analysis

# 04 - RouteTrack Pi — GPS Data Logging Service

#### **Date:** December 24th, 2025  
**Category:** Raspberry Pi / GPS / Logging / Linux Services  
**Backlink:** [RouteTrack Pi — gpsd Installation &amp; GPS Validation](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/03-routetrack-pi-gpsd-installation-gps-validation)

---

## Project Goal

This page adds the next layer on top of the now-stable GPS subsystem:

- Continuously **collect TPV updates** from `gpsd` (localhost port **2947**)
- Write the data to a **local database** for later:
    
    
    - Route mapping (GeoJSON export)
    - Mileage calculation
    - Stop detection / time-on-site
    - Daily totals (hours, distance, etc.)

This is a **local-first** design so the system still works even when the truck is offline.

---

## Prerequisites

This page assumes:

- `gpsd-standalone.service` is enabled and running
- `gpsd` is listening on **127.0.0.1:2947**
- `gpspipe -w -n 25` shows TPV + SKY messages and `mode: 3` once a fix is obtained

---

## Data We Will Log (Minimum Viable Dataset)

From `TPV` messages, we will store:

- `time` (UTC timestamp from gpsd)
- `lat`, `lon`
- `alt` (optional)
- `speed`
- `track` (heading)
- `mode` (0/1/2/3 — we only trust **3** for real routes)
- `epx`, `epy`, `eps` (accuracy-ish fields if present)

This is enough to:

- Render a map route
- Compute distance
- Identify movement vs stops
- Generate daily summaries

---

Perfect — this is exactly the right moment to clean this up 👍  
Below is a **fully copy-pasteable replacement** for **both sections** you showed: **Folder Layout** and **Install Dependencies**, rewritten to match the **venv + dashboard** direction and your clean BookStack style.

You can drop this in **as-is** and delete what’s there now.

---

## Folder Layout

This project uses a structured directory layout under `/opt/routetrack` to keep scripts, data, logs, configuration, and the Python virtual environment clearly separated.

Create the directory structure:

```bash
sudo mkdir -p /opt/routetrack/{bin,data,logs,config}
sudo chown -R $USER:$USER /opt/routetrack
```

### Directory Purpose

- **bin/**  
    Application scripts and executables  
    (e.g. GPS logger, web dashboard entrypoints)
- **data/**  
    Persistent application data  
    (SQLite database, exports)
- **logs/**  
    Application logs  
    (GPS logging, web service logs)
- **config/**  
    Configuration files  
    (database schema, environment variables)
- **venv/**  
    Python virtual environment  
    (created in a later step)

### Reference Paths

These paths are used consistently throughout the RouteTrack project:

- **Logger script:**  
    `/opt/routetrack/bin/routetrack-logger.py`
- **SQLite database:**  
    `/opt/routetrack/data/routetrack.sqlite`
- **Application logs:**  
    `/opt/routetrack/logs/routetrack.log`
- **Database schema / config files:**  
    `/opt/routetrack/config/schema.sql`

---

## Install Dependencies

RouteTrack runs on **Raspberry Pi OS Lite 64 bit** and uses a Python virtual environment to avoid modifying the system Python installation.

Update package lists:

```bash
sudo apt update
```

Install required system packages:

```bash
sudo apt install -y python3-venv python3-pip sqlite3
```

### Package Purpose

- **python3-venv**  
    Creates an isolated Python environment for RouteTrack
- **python3-pip**  
    Installs Python packages inside the virtual environment
- **sqlite3**  
    Lightweight local database for GPS data and summaries

Using a virtual environment avoids conflicts with the OS-managed Python environment and allows RouteTrack to safely install web and dashboard dependencies.

Nice — and you’re right to call that out. Your BookStack section should explicitly include the **venv package install** step since that’s where you’re at now.

Here’s a **drop-in BookStack section** you can paste **immediately after “Install Dependencies”** (or as the last part of it). It documents exactly what you did, cleanly.

---

## Create the Python Virtual Environment (venv)

RouteTrack uses a dedicated Python virtual environment stored under `/opt/routetrack/venv`.  
This avoids installing packages into the OS-managed Python environment and keeps the project portable and stable.

Create the virtual environment:

```bash
python3 -m venv /opt/routetrack/venv
```

---

## Install Web Dashboard Dependencies (Flask + Gunicorn)

Upgrade `pip` inside the virtual environment:

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

Install the local dashboard requirements:

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

### Verify Flask Works

Run a quick import test:

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

Expected output:

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/WeUK9eDpRGpFcH91-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/WeUK9eDpRGpFcH91-image.png)

---

## Current Status

At this point:

- Folder layout exists under `/opt/routetrack`
- Python virtual environment is created at `/opt/routetrack/venv`
- Flask + Gunicorn are installed and verified successfully

The next phase will initialize the SQLite database schema and begin logging `gpsd` TPV points into the database for mapping and reporting.

---

## Creating the SQLite Database Schema

This project uses a **file-based SQL schema** to define the RouteTrack database structure.  
Storing the schema in a dedicated `.sql` file makes it easier to review, document, and extend later as new features (stops, daily summaries, exports) are added.

---

### Creating the Schema File

Create a schema file in the RouteTrack configuration directory:

```bash
sudo nano /opt/routetrack/config/schema.sql
```

Paste the following contents:

```sqlite
PRAGMA journal_mode=WAL;

CREATE TABLE IF NOT EXISTS gps_points (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  ts TEXT NOT NULL,
  lat REAL,
  lon REAL,
  speed REAL,
  track REAL,
  alt REAL,
  mode INTEGER,
  epx REAL,
  epy REAL,
  eps REAL
);

CREATE INDEX IF NOT EXISTS idx_gps_points_ts
  ON gps_points(ts);
```

Save and exit:

- `CTRL + O` → Enter or `CTRL + S`
- `CTRL + X`

---

### Applying the Schema to the Database

Run the schema file once to initialize the SQLite database:

```bash
sqlite3 /opt/routetrack/data/routetrack.sqlite \
  < /opt/routetrack/config/schema.sql
```

If the database file does not already exist, SQLite will create it automatically.

---

### Verify Database Creation

Confirm that the table was created successfully:

```bash
sqlite3 /opt/routetrack/data/routetrack.sqlite ".tables"
```

Expected output:

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/5MxBkYokolw8GH5s-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/5MxBkYokolw8GH5s-image.png)

To inspect the table structure:

```bash
sqlite3 /opt/routetrack/data/routetrack.sqlite ".schema gps_points"
```

---

### Why This Approach

SQLite was chosen because it is:

- **Lightweight** and ideal for Raspberry Pi hardware
- **Reliable** for long-running, vehicle-mounted deployments
- **Easy to export** later to GeoJSON or CSV
- Well-suited for **daily summaries** and route analysis

Using a standalone schema file keeps database changes explicit and versionable, which aligns with the long-term goal of expanding RouteTrack into a full route-tracking and reporting system

---

## Create the RouteTrack Logger Script (SQLite + gpsd)

This logger is responsible for continuously collecting GPS position updates from `gpsd` (localhost port `2947`) and storing them in the local SQLite database.

### Design Notes (Why this works well on a vehicle Pi)

- Connects to `gpsd` over TCP (`127.0.0.1:2947`) using newline-delimited JSON
- Stores only `TPV` class updates (time/position/speed/heading)
- Commits inserts in small batches to reduce SD card write amplification
- Runs under `systemd` so it starts on boot and self-heals if `gpsd` restarts

---

### Create the Logger Script File

Create/edit the logger script:

```bash
sudo nano /opt/routetrack/bin/routetrack-logger.py
```

Paste the following script:

```python
<span class="token comment">#!/usr/bin/env python3</span>
<span class="token triple-quoted-string string">"""
RouteTrack GPS Logger
---------------------

Purpose:
  - Connect to gpsd (localhost:2947)
  - Subscribe to JSON streaming (WATCH)
  - Extract TPV messages (Time/Position/Velocity)
  - Insert points into SQLite (gps_points table)
  - Commit periodically for SD-card friendly writes
  - Print logs to stdout so systemd journald captures them

Key assumptions:
  - gpsd is already running as gpsd-standalone.service and listening on port 2947
  - SQLite DB exists at /opt/routetrack/data/routetrack.sqlite
  - Table gps_points exists (created by schema.sql)
"""</span>

<span class="token keyword">import</span> json
<span class="token keyword">import</span> socket
<span class="token keyword">import</span> sqlite3
<span class="token keyword">import</span> time
<span class="token keyword">from</span> datetime <span class="token keyword">import</span> datetime<span class="token punctuation">,</span> timezone


<span class="token comment"># gpsd host/port (your standalone service binds gpsd to localhost:2947)</span>
GPSD_HOST <span class="token operator">=</span> <span class="token string">"127.0.0.1"</span>
GPSD_PORT <span class="token operator">=</span> <span class="token number">2947</span>

<span class="token comment"># SQLite database path created earlier</span>
DB_PATH <span class="token operator">=</span> <span class="token string">"/opt/routetrack/data/routetrack.sqlite"</span>

<span class="token comment"># Commit every N points:</span>
<span class="token comment"># - Reduces disk writes vs committing each insert</span>
<span class="token comment"># - Helps SD card longevity in vehicle deployments</span>
COMMIT_EVERY <span class="token operator">=</span> <span class="token number">10</span>


<span class="token keyword">def</span> <span class="token function">utc_now</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token builtin">str</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""Return a UTC timestamp string for logging."""</span>
    <span class="token keyword">return</span> datetime<span class="token punctuation">.</span>now<span class="token punctuation">(</span>timezone<span class="token punctuation">.</span>utc<span class="token punctuation">)</span><span class="token punctuation">.</span>isoformat<span class="token punctuation">(</span><span class="token punctuation">)</span>


<span class="token keyword">def</span> <span class="token function">db_connect</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> sqlite3<span class="token punctuation">.</span>Connection<span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""
    Open SQLite connection and apply performance/safety pragmas.

    - WAL (Write-Ahead Logging) mode is already enabled via schema.sql,
      but repeating it here is harmless.
    - synchronous=NORMAL is a common setting for WAL mode:
      better performance with good durability.
    """</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> timeout<span class="token operator">=</span><span class="token number">30</span><span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>execute<span class="token punctuation">(</span><span class="token string">"PRAGMA journal_mode=WAL;"</span><span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>execute<span class="token punctuation">(</span><span class="token string">"PRAGMA synchronous=NORMAL;"</span><span class="token punctuation">)</span>
    <span class="token keyword">return</span> conn


<span class="token keyword">def</span> <span class="token function">insert_point</span><span class="token punctuation">(</span>cur<span class="token punctuation">:</span> sqlite3<span class="token punctuation">.</span>Cursor<span class="token punctuation">,</span> tpv<span class="token punctuation">:</span> <span class="token builtin">dict</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token boolean">None</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""
    Insert a TPV message into gps_points.

    We use tpv.get(...) so missing keys do not crash the logger.
    This keeps the service robust when gpsd emits partial data during fix acquisition.
    """</span>
    cur<span class="token punctuation">.</span>execute<span class="token punctuation">(</span>
        <span class="token triple-quoted-string string">"""
        INSERT INTO gps_points (ts, lat, lon, speed, track, alt, mode, epx, epy, eps)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """</span><span class="token punctuation">,</span>
        <span class="token punctuation">(</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"time"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>   <span class="token comment"># gpsd's UTC timestamp (string)</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"lat"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>    <span class="token comment"># latitude (float)</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"lon"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>    <span class="token comment"># longitude (float)</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"speed"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token comment"># speed (typically meters/second)</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"track"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>  <span class="token comment"># heading/course (degrees)</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"alt"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>    <span class="token comment"># altitude (meters)</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"mode"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>   <span class="token comment"># 0/1/2/3 (3 = best fix quality)</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"epx"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>    <span class="token comment"># estimated longitude error (meters)</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"epy"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>    <span class="token comment"># estimated latitude error (meters)</span>
            tpv<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"eps"</span><span class="token punctuation">)</span><span class="token punctuation">,</span>    <span class="token comment"># estimated speed error</span>
        <span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">)</span>


<span class="token keyword">def</span> <span class="token function">main</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token boolean">None</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""
    Main loop:
      - Connect to SQLite
      - Forever:
          - Connect to gpsd
          - Send WATCH request to enable JSON streaming
          - Read gpsd lines (newline-delimited JSON)
          - Store TPV messages to SQLite
      - On disconnect/errors:
          - commit anything pending
          - sleep briefly
          - reconnect
    """</span>
    <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>utc_now<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token string"> RouteTrack logger starting…"</span></span><span class="token punctuation">,</span> flush<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>

    <span class="token comment"># Create DB connection and cursor once.</span>
    <span class="token comment"># SQLite is local, fast, and lightweight for Pi deployments.</span>
    conn <span class="token operator">=</span> db_connect<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>

    <span class="token comment"># Count uncommitted inserts so we can batch commits.</span>
    pending <span class="token operator">=</span> <span class="token number">0</span>

    <span class="token keyword">while</span> <span class="token boolean">True</span><span class="token punctuation">:</span>
        <span class="token keyword">try</span><span class="token punctuation">:</span>
            <span class="token comment"># Establish TCP connection to gpsd service.</span>
            <span class="token keyword">with</span> socket<span class="token punctuation">.</span>create_connection<span class="token punctuation">(</span><span class="token punctuation">(</span>GPSD_HOST<span class="token punctuation">,</span> GPSD_PORT<span class="token punctuation">)</span><span class="token punctuation">,</span> timeout<span class="token operator">=</span><span class="token number">10</span><span class="token punctuation">)</span> <span class="token keyword">as</span> s<span class="token punctuation">:</span>

                <span class="token comment"># Enable JSON output streaming from gpsd.</span>
                <span class="token comment"># gpsd will emit multiple classes (TPV, SKY, etc.); we filter for TPV.</span>
                s<span class="token punctuation">.</span>sendall<span class="token punctuation">(</span><span class="token string">b'?WATCH={"enable":true,"json":true}\n'</span><span class="token punctuation">)</span>

                <span class="token comment"># gpsd responses arrive in chunks; accumulate until newline.</span>
                buf <span class="token operator">=</span> <span class="token string">b""</span>

                <span class="token keyword">while</span> <span class="token boolean">True</span><span class="token punctuation">:</span>
                    chunk <span class="token operator">=</span> s<span class="token punctuation">.</span>recv<span class="token punctuation">(</span><span class="token number">4096</span><span class="token punctuation">)</span>
                    <span class="token keyword">if</span> <span class="token keyword">not</span> chunk<span class="token punctuation">:</span>
                        <span class="token comment"># Socket closed; force reconnect</span>
                        <span class="token keyword">raise</span> RuntimeError<span class="token punctuation">(</span><span class="token string">"gpsd socket closed"</span><span class="token punctuation">)</span>

                    buf <span class="token operator">+=</span> chunk

                    <span class="token comment"># Process all complete lines currently buffered.</span>
                    <span class="token keyword">while</span> <span class="token string">b"\n"</span> <span class="token keyword">in</span> buf<span class="token punctuation">:</span>
                        line<span class="token punctuation">,</span> buf <span class="token operator">=</span> buf<span class="token punctuation">.</span>split<span class="token punctuation">(</span><span class="token string">b"\n"</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span>

                        <span class="token keyword">if</span> <span class="token keyword">not</span> line<span class="token punctuation">.</span>strip<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
                            <span class="token keyword">continue</span>

                        <span class="token comment"># Convert bytes -> string -> JSON dict</span>
                        <span class="token keyword">try</span><span class="token punctuation">:</span>
                            msg <span class="token operator">=</span> json<span class="token punctuation">.</span>loads<span class="token punctuation">(</span>line<span class="token punctuation">.</span>decode<span class="token punctuation">(</span><span class="token string">"utf-8"</span><span class="token punctuation">,</span> errors<span class="token operator">=</span><span class="token string">"replace"</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
                        <span class="token keyword">except</span> json<span class="token punctuation">.</span>JSONDecodeError<span class="token punctuation">:</span>
                            <span class="token comment"># Skip malformed lines without crashing</span>
                            <span class="token keyword">continue</span>

                        <span class="token comment"># Only store TPV messages (position/time/speed)</span>
                        <span class="token keyword">if</span> msg<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"class"</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token string">"TPV"</span><span class="token punctuation">:</span>
                            <span class="token keyword">continue</span>

                        <span class="token comment"># Skip TPV messages without time.</span>
                        <span class="token comment"># This can occur before a real fix is established.</span>
                        <span class="token keyword">if</span> <span class="token string">"time"</span> <span class="token keyword">not</span> <span class="token keyword">in</span> msg<span class="token punctuation">:</span>
                            <span class="token keyword">continue</span>

                        <span class="token comment"># Insert into SQLite</span>
                        insert_point<span class="token punctuation">(</span>cur<span class="token punctuation">,</span> msg<span class="token punctuation">)</span>
                        pending <span class="token operator">+=</span> <span class="token number">1</span>

                        <span class="token comment"># Commit every N points to reduce write load</span>
                        <span class="token keyword">if</span> pending <span class="token operator">>=</span> COMMIT_EVERY<span class="token punctuation">:</span>
                            conn<span class="token punctuation">.</span>commit<span class="token punctuation">(</span><span class="token punctuation">)</span>
                            pending <span class="token operator">=</span> <span class="token number">0</span>

        <span class="token keyword">except</span> Exception <span class="token keyword">as</span> e<span class="token punctuation">:</span>
            <span class="token comment"># If gpsd restarts, USB hiccups, or anything breaks, we reconnect.</span>
            <span class="token comment"># Commit any pending inserts first (best effort).</span>
            <span class="token keyword">try</span><span class="token punctuation">:</span>
                conn<span class="token punctuation">.</span>commit<span class="token punctuation">(</span><span class="token punctuation">)</span>
            <span class="token keyword">except</span> Exception<span class="token punctuation">:</span>
                <span class="token keyword">pass</span>

            <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string-interpolation"><span class="token string">f"</span><span class="token interpolation"><span class="token punctuation">{</span>utc_now<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">}</span></span><span class="token string"> ERROR: </span><span class="token interpolation"><span class="token punctuation">{</span>e<span class="token punctuation">}</span></span><span class="token string"> (reconnecting in 3s)"</span></span><span class="token punctuation">,</span> flush<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>
            time<span class="token punctuation">.</span>sleep<span class="token punctuation">(</span><span class="token number">3</span><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>
    main<span class="token punctuation">(</span><span class="token punctuation">)</span>

```

Make the script executable:

```bash
sudo chmod +x /opt/routetrack/bin/routetrack-logger.py
```

---

## Create the systemd Service (RouteTrack Logger)

This service ensures the logger starts at boot, stays running, and is tied to the GPS subsystem.

Create the unit file:

```bash
sudo nano /etc/systemd/system/routetrack-logger.service
```

Paste:

```ini
[Unit]
Description=RouteTrack GPS Logger
# Start after gpsd is online and networking is available
After=gpsd-standalone.service network.target
# Pull gpsd up if needed, and fail if gpsd is missing
Wants=gpsd-standalone.service
Requires=gpsd-standalone.service

[Service]
Type=simple

# Run from /opt/routetrack so relative paths (if added later) behave predictably
WorkingDirectory=/opt/routetrack

# IMPORTANT:
# Use the virtual environment Python so packages (Flask, etc.) remain isolated
ExecStart=/opt/routetrack/venv/bin/python /opt/routetrack/bin/routetrack-logger.py

# Always restart if the logger exits (gpsd restarts, USB dropouts, etc.)
Restart=always
RestartSec=3

# Send script output to journald
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
```

Reload systemd + enable and start the service:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now routetrack-logger.service
```

Check service status:

```bash
sudo systemctl status routetrack-logger.service --no-pager -l
```

View logs live:

```bash
sudo journalctl -u routetrack-logger -f
```

---

## Verify GPS Data is Being Written to SQLite

Confirm row count is increasing:

```bash
sqlite3 /opt/routetrack/data/routetrack.sqlite "SELECT COUNT(*) FROM gps_points;"
```

View the latest 10 points:

```bash
sqlite3 /opt/routetrack/data/routetrack.sqlite \
"SELECT ts, lat, lon, speed, mode FROM gps_points ORDER BY id DESC LIMIT 10;"
```

Optional: verify you are receiving `mode: 3` fixes consistently:

```bash
sqlite3 /opt/routetrack/data/routetrack.sqlite \
"SELECT mode, COUNT(*) FROM gps_points GROUP BY mode ORDER BY mode;"
```

---

## Notes for Later Phases (Mileage + Stops)

- gpsd `speed` values are typically **meters/second**
    
    
    - mph conversion: `mph = mps * 2.23694`
- Some “movement” may appear when parked due to GPS drift.
    
    
    - Mileage calculations should apply filtering later:
        
        
        - count movement only when `mode = 3`
        - ignore points under a speed threshold (example: `>= 0.5 m/s`)

---

## Log Rotation (Prevent SD Card Bloat)

RouteTrack services write their runtime output to **systemd journald** (viewable via `journalctl`). journald handles rotation automatically and is capped by the retention settings configured in `/etc/systemd/journald.conf`.

In addition, a `logrotate` policy is created for any **file-based logs** that may be added later under `/opt/routetrack/logs/` (for example: helper scripts, exporters, or future components that write `.log` files).

### Create the logrotate Policy (File Logs)

Create a logrotate config for RouteTrack:

```bash
sudo nano /etc/logrotate.d/routetrack

```

Paste:

```conf
/opt/routetrack/logs/*.log {
  daily
  rotate 14
  compress
  delaycompress
  missingok
  notifempty
  copytruncate
}

```

### Fix: logrotate “Insecure Permissions” Error

logrotate may refuse to rotate logs if the parent directory is writable by non-root users (reported as “insecure permissions”). To resolve this securely, the RouteTrack logs directory is locked down to root ownership:

```bash
sudo chown root:root /opt/routetrack/logs
sudo chmod 755 /opt/routetrack/logs

```

Verify permissions:

```bash
ls -ld /opt/routetrack /opt/routetrack/logs

```

### Test logrotate

Force a rotation to validate the config:

```bash
sudo logrotate -f /etc/logrotate.d/routetrack

```

### RouteTrack Logging Note (Primary Logging)

The RouteTrack logger service outputs to **journald**, so you can view logs with:

```bash
sudo journalctl -u routetrack-logger -f

```

This is the primary logging method. File-based log rotation is included for future scripts or components that write to `/opt/routetrack/logs/*.log`.

---

### Current Status

- File-based RouteTrack logs are rotated daily
- Old logs are compressed and retained safely
- Disk usage remains controlled for long-running deployments

---

## Next Steps

Next phase will build the actual “route intelligence”:

- **Mileage calculation**
    
    
    - Haversine distance between points
    - Only count `mode: 3` fixes
    - Ignore drift when speed is near zero
- **Stop detection**
    
    
    - Define stop events (speed threshold + dwell time)
    - Write stop events to a second table
- **Daily summaries**
    
    
    - Total mileage
    - Total moving time
    - Total stopped time (time-on-site)
    - Start time / end time per shift

# 05 - RouteTrack Pi — Route Intelligence & Metrics Engine

#### **Date:** December 24th, 2025  
**Category:** Raspberry Pi / GPS / Data Processing  
**Backlink:** [RouteTrack Pi — GPS Data Logging Service](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/04-routetrack-pi-gps-data-logging-service)

---

## Project Goal

This phase introduces the **route intelligence layer** for RouteTrack.

Raw GPS points alone are not useful for reporting or visualization. This page defines how RouteTrack transforms logged GPS data into meaningful metrics such as:

- Mileage
- Moving vs stopped time
- Stop events (time-on-site)
- Daily route summaries

These metrics will later power:

- The local web dashboard (Leaflet)
- Historical route review
- Daily summaries and exports

---

## Data Inputs

This phase consumes GPS data already being logged into SQLite:

**Table:** `gps_points`

Key fields used:

- `ts` — GPS timestamp (UTC)
- `lat`, `lon` — geographic position
- `speed` — meters per second
- `mode` — GPS fix quality (0–3)

Only **`mode = 3`** records are considered trustworthy for route calculations.

---

## Route Mileage Calculation

### Method: Haversine Distance

Mileage is calculated using the **Haversine formula**, which computes the great-circle distance between two latitude/longitude points on Earth.

This approach is:

- Accurate for vehicle-scale distances
- Lightweight (no external geo libraries required)
- Suitable for Raspberry Pi hardware

### Rules Applied

To avoid false mileage caused by GPS drift:

- Only include points where:
    
    
    - `mode = 3`
    - `speed >= movement_threshold`
- Distance is accumulated **only between consecutive valid points**

### Movement Threshold

A minimum speed threshold is applied:

- **Default:** `0.5 m/s` (~1.1 mph)

This filters out:

- Stationary GPS jitter
- Minor antenna noise when parked

---

## Stop Detection (Time-on-Site)

Stops are inferred from GPS behavior rather than ignition signals.

### Stop Definition

A **stop event** occurs when:

- Speed remains below `movement_threshold`
- For longer than `stop_dwell_time`

**Recommended defaults:**

- `movement_threshold`: `0.5 m/s`
- `stop_dwell_time`: `120 seconds`

This prevents brief slowdowns (traffic, turns) from being classified as stops.

---

## Stop Events Table (Planned)

Detected stops will be stored in a dedicated table.

### Table: `stop_events` (planned)

Fields:

- `id`
- `start_ts`
- `end_ts`
- `duration_seconds`
- `lat`
- `lon`

Each stop represents a **single continuous stationary period**.

---

## Daily Route Summaries

RouteTrack will generate daily summaries based on processed GPS data.

### Metrics Tracked Per Day

- **Total mileage**
- **Total moving time**
- **Total stopped time**
- **Shift start time**
- **Shift end time**
- **Number of stops**

### Daily Summary Table (Planned)

**Table:** `daily_summary`

Fields:

- `date`
- `start_ts`
- `end_ts`
- `total_distance_miles`
- `moving_time_seconds`
- `stopped_time_seconds`
- `stop_count`

Daily summaries allow:

- Fast dashboard loading
- Simple reporting
- Long-term trend analysis

---

## Processing Strategy

Route intelligence will be computed using **post-processing scripts**, not in the logger itself.

Reasons:

- Keeps the logger lightweight and reliable
- Allows recalculation if thresholds change
- Makes testing and validation easier

Processing can be triggered:

- On demand
- On a schedule (cron)
- Before dashboard rendering

---

## Relationship to Local Dashboard

The local dashboard will **not** calculate metrics in real time.

Instead, it will:

- Read pre-computed route data
- Display routes, stops, and summaries using Leaflet
- Query SQLite via a lightweight API

This keeps the UI responsive and the system scalable.

---

## Current Status

At this stage:

- GPS data is logged continuously
- SQLite schema is stable
- Logger service is running reliably
- No route intelligence calculations are active yet

---

## Next Steps

The next phase will implement:

1. **Route processing script**
    
    
    - Compute mileage
    - Detect stops
    - Populate summary tables
2. **Database schema extensions**
    
    
    - `stop_events`
    - `daily_summary`
3. **Local Web Dashboard**
    
    
    - Flask backend
    - Leaflet-based map
    - Live and historical views

---

### Why This Page Matters

This page clearly separates:

- **Data collection**
- **Data interpretation**
- **Visualization**

It is just a thoughtful write up on my next page which will be integrating the data before bringing up the Flask + Leaflet dashboard so it will launch with meaningful data!

# 06 - RouteTrack Pi — Route Processing & Summary Generation

#### **Date:** December 25, 2025  
**Category:** Raspberry Pi / GPS / Data Processing  
**Backlink:** [RouteTrack Pi — GPS Logging &amp; Data Ingestion](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/04-routetrack-pi-gps-data-logging-service)

---

## Project Goal

This phase transforms RouteTrack from a **raw GPS logger** into a **route intelligence system**.

Instead of calculating metrics at ingestion time, RouteTrack uses a **post-processing model**:

- Raw GPS points are logged continuously
- Route intelligence is calculated later
- Derived data can be safely regenerated if logic changes

This mirrors how professional telemetry and fleet-tracking systems are built.

---

## High-Level Architecture

<table id="bkmrk-layer-responsibility"><thead><tr><th>Layer</th><th>Responsibility</th></tr></thead><tbody><tr><td>GPS Logger</td><td>Writes raw telemetry (`gps_points`)</td></tr><tr><td>Route Processor</td><td>Computes stops, mileage, summaries</td></tr><tr><td>SQLite</td><td>Stores raw + derived data</td></tr><tr><td>Dashboard (next phase)</td><td>Reads only processed tables</td></tr></tbody></table>

Only **one component writes raw data**.  
All intelligence is derived afterward.

---

## Unified Database Schema

All RouteTrack data structures are defined in a **single schema file**:

```bash
/opt/routetrack/config/schema.sql
```

This file defines:

- Raw GPS telemetry
- Derived stop events
- Aggregated daily summaries

---

### SQLite Schema (with WAL enabled)

```sql
-- ============================================================
-- RouteTrack SQLite Schema
-- ============================================================

PRAGMA journal_mode=WAL;

-- ============================================================
-- TABLE: gps_points (RAW TELEMETRY)
-- ============================================================
CREATE TABLE IF NOT EXISTS gps_points (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  ts TEXT NOT NULL,
  lat REAL,
  lon REAL,
  speed REAL,
  track REAL,
  alt REAL,
  mode INTEGER,
  epx REAL,
  epy REAL,
  eps REAL
);

CREATE INDEX IF NOT EXISTS idx_gps_points_ts
  ON gps_points(ts);

-- ============================================================
-- TABLE: stop_events (DERIVED)
-- ============================================================
CREATE TABLE IF NOT EXISTS stop_events (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  start_ts TEXT NOT NULL,
  end_ts TEXT NOT NULL,
  duration_seconds INTEGER NOT NULL,
  lat REAL,
  lon REAL
);

CREATE INDEX IF NOT EXISTS idx_stop_events_start_ts
  ON stop_events(start_ts);

-- ============================================================
-- TABLE: daily_summary (AGGREGATED)
-- ============================================================
CREATE TABLE IF NOT EXISTS daily_summary (
  date TEXT PRIMARY KEY,
  start_ts TEXT,
  end_ts TEXT,
  total_distance_miles REAL,
  moving_time_seconds INTEGER,
  stopped_time_seconds INTEGER,
  stop_count INTEGER
);

```

---

## Schema Design Breakdown

### `gps_points` — Source of Truth

- Stores raw TPV messages from `gpsd`
- Append-only
- Never modified or recalculated
- All other tables derive from this data

---

### `stop_events` — Route Intelligence

- Represents **continuous stationary periods**
- Derived using:
    
    
    - Speed threshold
    - Minimum dwell time
- Used for:
    
    
    - Time-on-site tracking
    - Map stop markers
    - Shift analysis

---

### `daily_summary` — Fast Reporting

- One row per calendar day
- Stores:
    
    
    - Start / end timestamps
    - Total mileage
    - Moving vs stopped time
    - Stop count
- Prevents rescanning raw GPS points for dashboards

---

## Applying the Schema (Important!)

SQLite requires an **exclusive lock** for schema changes.

### Safe Workflow

```bash
sudo systemctl stop routetrack-logger.service
sqlite3 /opt/routetrack/data/routetrack.sqlite < /opt/routetrack/config/schema.sql
sqlite3 /opt/routetrack/data/routetrack.sqlite ".tables"
sudo systemctl start routetrack-logger.service
```

Expected tables:

```
gps_points
stop_events
daily_summary
```

---

## Route Processing Script

The route processor converts raw GPS points into usable metrics.

### Responsibilities

- Filter invalid fixes (`mode != 3`)
- Ignore GPS drift
- Calculate distance (Haversine)
- Track moving vs stopped time
- Detect stop events
- Populate `stop_events` and `daily_summary`

---

## Route Processor Script (Final Version Used)

**File:**

```bash
/opt/routetrack/bin/routetrack-process.py
```

```python
#!/usr/bin/env python3
"""
RouteTrack Route Processor (Daily)
"""

import math
import sqlite3
import sys
from datetime import datetime, date, timezone

DB_PATH = "/opt/routetrack/data/routetrack.sqlite"

# 5 mph ≈ 2.235 m/s
MOVEMENT_THRESHOLD_MPS = 2.235
STOP_DWELL_SECONDS = 120
EARTH_RADIUS_KM = 6371.0

def haversine_meters(lat1, lon1, lat2, lon2):
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    dphi = math.radians(lat2 - lat1)
    dlambda = math.radians(lon2 - lon1)

    a = (
        math.sin(dphi / 2) ** 2 +
        math.cos(phi1) * math.cos(phi2) *
        math.sin(dlambda / 2) ** 2
    )
    return 2 * EARTH_RADIUS_KM * 1000 * math.atan2(
        math.sqrt(a), math.sqrt(1 - a)
    )

def parse_ts(ts):
    return datetime.fromisoformat(ts.replace("Z", "+00:00"))

def main():
    day = sys.argv[1] if len(sys.argv) == 2 else date.today().isoformat()

    start = f"{day}T00:00:00Z"
    end = f"{day}T23:59:59Z"

    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()

    cur.execute("""
        SELECT ts, lat, lon, speed, mode
        FROM gps_points
        WHERE ts >= ? AND ts <= ?
        ORDER BY ts
    """, (start, end))

    rows = cur.fetchall()
    if not rows:
        print("No GPS data for this date.")
        return

    cur.execute("DELETE FROM stop_events WHERE start_ts >= ? AND start_ts <= ?", (start, end))
    cur.execute("DELETE FROM daily_summary WHERE date = ?", (day,))

    total_dist = 0
    moving = 0
    stopped = 0
    stops = []

    last = None
    stop_start = None

    for ts, lat, lon, speed, mode in rows:
        if mode != 3 or lat is None or lon is None:
            continue

        now = parse_ts(ts)

        if last:
            prev_t, prev_lat, prev_lon = last
            dt = (now - prev_t).total_seconds()

            if speed and speed >= MOVEMENT_THRESHOLD_MPS:
                total_dist += haversine_meters(prev_lat, prev_lon, lat, lon)
                moving += int(dt)

                if stop_start:
                    dur = int((now - stop_start[0]).total_seconds())
                    if dur >= STOP_DWELL_SECONDS:
                        stops.append((stop_start[0], now, dur, stop_start[1], stop_start[2]))
                        stopped += dur
                    stop_start = None
            else:
                if not stop_start:
                    stop_start = (now, lat, lon)

        last = (now, lat, lon)

    for s in stops:
        cur.execute("""
            INSERT INTO stop_events
            (start_ts, end_ts, duration_seconds, lat, lon)
            VALUES (?, ?, ?, ?, ?)
        """, (s[0].isoformat(), s[1].isoformat(), s[2], s[3], s[4]))

    miles = total_dist * 0.000621371

    cur.execute("""
        INSERT INTO daily_summary
        (date, start_ts, end_ts, total_distance_miles,
         moving_time_seconds, stopped_time_seconds, stop_count)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    """, (day, rows[0][0], rows[-1][0], round(miles, 2), moving, stopped, len(stops)))

    conn.commit()
    conn.close()

    print(f"Processed {day}: miles={round(miles,2)} stops={len(stops)}")

if __name__ == "__main__":
    main()

```

Make executable:

```bash
sudo chmod +x /opt/routetrack/bin/routetrack-process.py
```

---

## Running the Processor (Safe Method)

Because SQLite needs exclusive access for deletes/inserts:

```bash
sudo systemctl stop routetrack-logger.service
/opt/routetrack/venv/bin/python /opt/routetrack/bin/routetrack-process.py
sudo systemctl start routetrack-logger.service
```

---

## Verification Queries

### Daily Summary

```bash
sqlite3 /opt/routetrack/data/routetrack.sqlite \
"SELECT * FROM daily_summary ORDER BY date DESC LIMIT 1;"
```

### Stop Events

```bash
sqlite3 /opt/routetrack/data/routetrack.sqlite \
"SELECT start_ts,end_ts,duration_seconds FROM stop_events ORDER BY id DESC LIMIT 5;"
```

---

## Real-World Validation (Stationary Test)

With the GPS unit **not moving at all**:

<table id="bkmrk-metric-result-distan"><thead><tr><th>Metric</th><th>Result</th></tr></thead><tbody><tr><td>Distance</td><td>0.0 miles</td></tr><tr><td>Moving time</td><td>0 seconds</td></tr><tr><td>Stops</td><td>1</td></tr><tr><td>Stopped time</td><td>Entire duration</td></tr></tbody></table>

This confirmed:

- GPS drift was eliminated
- Movement detection is stable
- Stop logic behaves correctly

---

## Why This Matters

This phase turns RouteTrack into a **true telemetry system**:

- Accurate mileage
- Reliable stop detection
- Regenerable summaries
- Dashboard-ready data model

The UI is now just a **viewer**, not a calculator.

---

## Next Steps

The next phase will focus on:

1. Automating route processing (systemd timer)
2. Local Flask API for data access
3. Leaflet map dashboard for:
    
    
    - Routes
    - Stops
    - Daily summaries

# 07 - RouteTrack Pi — Automated Route Processing (systemd Service + Timer)

#### **Date:** December 25, 2025  
**Category:** Raspberry Pi / GPS / Automation / systemd  
**Backlink:** 06 – [RouteTrack Pi — Route Processing &amp; Summary Generation](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/06-routetrack-pi-route-processing-summary-generation)

---

## Project Goal

This phase automates RouteTrack’s daily processing workflow so I don’t have to manually run the processor.

Because the GPS logger continuously writes to SQLite, the route processor must run with **exclusive database access**. The automation is designed to safely:

1. Stop `routetrack-logger.service` (release DB lock)
2. Run `routetrack-process.py` using the venv Python
3. Restart `routetrack-logger.service`
4. Log all output to systemd journal for review

This creates a repeatable, production-style “ingest → process → report” pipeline.

---

## Why systemd Timer (instead of cron)

I used a systemd timer because it provides:

- Reliable scheduling with clear “next run” info
- Centralized logs via `journalctl`
- Easy enable/disable and status checks
- Clean separation of concerns (service runs once; timer schedules it)

---

## Create Processor Wrapper Script

This wrapper script is the “safe runner” that prevents SQLite lock errors.

### Create the file

```bash
sudo nano /opt/routetrack/bin/routetrack-run-processor.sh

```

### Script used

```bash
#!/usr/bin/env bash
set -euo pipefail

# ------------------------------------------------------------
# RouteTrack - Safe Processor Runner
# ------------------------------------------------------------
# Stops the GPS logger (releases SQLite locks),
# runs the daily processor, then starts the logger again.
# ------------------------------------------------------------

LOGGER_SERVICE="routetrack-logger.service"
PROCESSOR="/opt/routetrack/bin/routetrack-process.py"
PYTHON="/opt/routetrack/venv/bin/python"

echo "$(date -Is) RouteTrack processor wrapper starting..."

echo "$(date -Is) Stopping ${LOGGER_SERVICE}..."
systemctl stop "${LOGGER_SERVICE}"

# small pause so file handles release cleanly
sleep 2

echo "$(date -Is) Running route processor..."
"${PYTHON}" "${PROCESSOR}"

echo "$(date -Is) Starting ${LOGGER_SERVICE}..."
systemctl start "${LOGGER_SERVICE}"

echo "$(date -Is) RouteTrack processor wrapper completed."

```

Make executable:

```bash
sudo chmod +x /opt/routetrack/bin/routetrack-run-processor.sh

```

---

## Create the systemd Service (oneshot)

The systemd service runs the wrapper script one time.

### Create the unit file

```bash
sudo nano /etc/systemd/system/routetrack-processor.service

```

### Unit file used

```ini
[Unit]
Description=RouteTrack Daily Route Processor
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
User=root
Group=root
ExecStart=/opt/routetrack/bin/routetrack-run-processor.sh
StandardOutput=journal
StandardError=journal

```

Reload units:

```bash
sudo systemctl daemon-reload

```

---

## Test the Service Manually

Run it once to confirm it works:

```bash
sudo systemctl start routetrack-processor.service

```

View the logs:

```bash
journalctl -u routetrack-processor.service --no-pager -l

```

---

## Confirm Logger Restarted Properly

```bash
systemctl status routetrack-logger.service --no-pager -l

```

The logger returned to an **active (running)** state immediately after processing.

---

## Create the systemd Timer

The timer schedules the processor to run automatically every day.

### Create the timer file

```bash
sudo nano /etc/systemd/system/routetrack-processor.timer

```

### Timer used (daily at 2:10 AM local time)

```ini
[Unit]
Description=Run RouteTrack Processor Daily

[Timer]
OnCalendar=*-*-* 02:10:00
Persistent=true
RandomizedDelaySec=30
Unit=routetrack-processor.service

[Install]
WantedBy=timers.target

```

Reload and enable:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now routetrack-processor.timer

```

---

## Verify the Timer is Active

Show timers:

```bash
systemctl list-timers --all | grep routetrack

```

Check timer status:

```bash
systemctl status routetrack-processor.timer --no-pager -l

```

This confirms:

- Timer is **active (waiting)**
- Next trigger time is scheduled
- It triggers `routetrack-processor.service`

---

## Confirm Data Was Written

After a successful run, verify the summary table:

```bash
sqlite3 /opt/routetrack/data/routetrack.sqlite \
"SELECT * FROM daily_summary ORDER BY date DESC LIMIT 3;"

```

This confirms the processor populated:

- `daily_summary`
- `stop_events`

and is producing real computed metrics.

---

## Run On Demand (Manual Trigger Block)

This is the clean “run it right now” workflow (and verify it worked) without touching the timer schedule.

### Run the processor service now

```bash
sudo systemctl start routetrack-processor.service

```

### View the last 50 log lines from the run

```bash
journalctl -u routetrack-processor.service -n 50 --no-pager -l

```

### Confirm the next scheduled timer run is still set

```bash
systemctl list-timers --all | grep routetrack

```

---

## Operational Notes

### Why the logger must stop

SQLite needs exclusive access for derived-table regeneration (`DELETE` + `INSERT`).  
Running the processor while the logger is inserting can produce:

- `sqlite3.OperationalError: database is locked`

This wrapper workflow prevents that completely.

### Where logs live

Everything is stored in systemd journal:

```bash
journalctl -u routetrack-processor.service --since "today" --no-pager -l

```

---

## Next Steps

Now that processing is automated daily, the next phase is the dashboard:

- Flask web service (local)
- Leaflet map page
- Route drawing from `gps_points`
- Stop markers from `stop_events`
- Daily totals from `daily_summary`

# 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

# 09 - RouteTrack Pi — Dashboard Autostart (Gunicorn + systemd)

#### **Date:** December 25, 2025  
**Category:** Raspberry Pi / GPS / Flask / systemd  
**Backlink:** 08 – [RouteTrack Pi — Local Web Dashboard (Flask API + Leaflet Map)](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/08-routetrack-pi-local-web-dashboard-flask-api-leaflet-map)

---

## Project Goal

At this stage RouteTrack already has a working local web dashboard (Flask + Leaflet).  
This page productionizes that dashboard so it behaves like a real appliance:

- Starts automatically on boot
- Stays running in the background
- Restarts if it crashes
- Runs the Flask app using **Gunicorn** (production WSGI server)

This is especially important because this Pi will be powered off frequently and used on-the-go.

---

## Why Gunicorn + systemd?

### Gunicorn

Gunicorn is a production-grade Python WSGI server that runs Flask reliably with multiple workers.

### systemd

systemd provides:

- Autostart on boot
- Automatic restart on failure
- Central logging via `journalctl`
- Clean service management (`start/stop/status`)

---

## Install Gunicorn (venv)

Install into the existing RouteTrack virtual environment:

```bash
/opt/routetrack/venv/bin/pip install gunicorn

```

My output confirmed it was already installed:

```
Requirement already satisfied: gunicorn in /opt/routetrack/venv/lib/python3.13/site-packages (23.0.0)

```

---

## Create the Dashboard systemd Service

Create the service file:

```bash
sudo nano /etc/systemd/system/routetrack-dashboard.service

```

Paste the unit file:

```ini
[Unit]
Description=RouteTrack Local Dashboard (Gunicorn)
After=network-online.target
Wants=network-online.target

[Service]
# Run the service under my normal user (not root)
User=zippyb
Group=zippyb

# Ensure relative paths work (app:app loads from this folder)
WorkingDirectory=/opt/routetrack/web

# Start Gunicorn on port 5000, listening on all interfaces
# -w 2 = 2 worker processes (lightweight + responsive on Pi)
ExecStart=/opt/routetrack/venv/bin/gunicorn -w 2 -b 0.0.0.0:5000 app:app

# Keep it alive if it crashes
Restart=always
RestartSec=3

# Basic hardening
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

```

---

## Reload systemd and Enable the Service

Reload units:

```bash
sudo systemctl daemon-reload

```

Enable + start the service:

```bash
sudo systemctl enable --now routetrack-dashboard.service

```

---

## Verify the Service Is Running

Check status:

```bash
systemctl status routetrack-dashboard.service --no-pager -l

```

My output confirmed it is running and listening properly:

- Service is **active (running)**
- Gunicorn started successfully
- Listening on `http://0.0.0.0:5000`
- Two workers booted

Example output:

```
Active: active (running)
Main PID: 9358 (gunicorn)
Listening at: http://0.0.0.0:5000 (9358)
Booting worker with pid: 9359
Booting worker with pid: 9360

```

---

## Operational Notes

### Where logs are stored

Because this is managed by systemd, logs are available via:

```bash
journalctl -u routetrack-dashboard.service --no-pager -l

```

Last 50 lines:

```bash
journalctl -u routetrack-dashboard.service -n 50 --no-pager -l

```

### Accessing the dashboard

From any device on the same network:

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

Find the Pi’s IP:

```bash
hostname -I

```

---

## Run On Demand (Manual Service Controls)

Start:

```bash
sudo systemctl start routetrack-dashboard.service

```

Stop:

```bash
sudo systemctl stop routetrack-dashboard.service

```

Restart:

```bash
sudo systemctl restart routetrack-dashboard.service

```

Status:

```bash
systemctl status routetrack-dashboard.service --no-pager -l

```

---

## Next Steps

Now that the dashboard is always online, the next improvements will focus on making RouteTrack truly “truck ready”:

1. Shift mode (Start Shift / End Shift buttons)
2. Offline mapping options (for no cell coverage)
3. Exports (GeoJSON/CSV) and backup strategy for on-the-go use

# 10 - RouteTrack Pi — Shift Mode (SQLite + Flask API + Dashboard Controls)

#### **Date:** December 25, 2025  
**Category:** Raspberry Pi / GPS / SQLite / Flask / Leaflet  
**Backlink:** [09 – RouteTrack Pi — Dashboard Autostart (Gunicorn + systemd)](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/09-routetrack-pi-dashboard-autostart-gunicorn-systemd)

---

## Project Goal

This phase introduces **Shift Mode** to RouteTrack.

Shift Mode allows RouteTrack to track work sessions independently of calendar days, which is essential for a portable, vehicle-mounted system that:

- Is powered down frequently
- Moves between locations
- Does not follow strict midnight-to-midnight boundaries
- Needs accurate per-shift metrics (hours, stops, time-on-site)

The dashboard now supports a simple workflow: **Start Shift → Drive → End Shift**

---

## Why Shift Mode Matters

Daily summaries work well for reporting, but they don’t match real-world truck usage:

- Overnight work can cross calendar boundaries
- Reboots/power loss interrupt sessions
- Short test runs clutter daily totals

Shift Mode solves this by creating a clean “session boundary” that the user controls.

---

## Database Changes

### New `shifts` Table

A new SQLite table stores shift metadata independently of GPS data.

**Table:** `shifts`

```sql
<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> <span class="token keyword">IF</span> <span class="token operator">NOT</span> <span class="token keyword">EXISTS</span> shifts <span class="token punctuation">(</span>
  id <span class="token keyword">INTEGER</span> <span class="token keyword">PRIMARY</span> <span class="token keyword">KEY</span> AUTOINCREMENT<span class="token punctuation">,</span>
  start_ts <span class="token keyword">TEXT</span> <span class="token operator">NOT</span> <span class="token boolean">NULL</span><span class="token punctuation">,</span>
  end_ts <span class="token keyword">TEXT</span><span class="token punctuation">,</span>
  note <span class="token keyword">TEXT</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token keyword">CREATE</span> <span class="token keyword">INDEX</span> <span class="token keyword">IF</span> <span class="token operator">NOT</span> <span class="token keyword">EXISTS</span> idx_shifts_start_ts
  <span class="token keyword">ON</span> shifts<span class="token punctuation">(</span>start_ts<span class="token punctuation">)</span><span class="token punctuation">;</span>

```

**Design Notes:**

- `start_ts` and `end_ts` stored as UTC ISO-8601 strings
- `end_ts` stays `NULL` while a shift is active
- Only one active shift allowed at a time
- Lightweight, isolated table to minimize lock contention

---

## Applying the Schema Safely

Because GPS logging writes constantly to SQLite, stop the logger before applying schema changes.

```bash
sudo systemctl stop routetrack-logger.service
sqlite3 /opt/routetrack/data/routetrack.sqlite < /opt/routetrack/config/schema.sql
sqlite3 /opt/routetrack/data/routetrack.sqlite ".tables"
sudo systemctl start routetrack-logger.service

```

Expected tables:

```
daily_summary  gps_points  shifts  stop_events

```

---

## Flask API Enhancements (Full `app.py`)

Shift Mode is implemented via additional Flask API endpoints.

### New Endpoints

<table id="bkmrk-method-endpoint-purp"><thead><tr><th>Method</th><th>Endpoint</th><th>Purpose</th></tr></thead><tbody><tr><td>GET</td><td>`/api/shift/active`</td><td>Returns the active shift (if any)</td></tr><tr><td>POST</td><td>`/api/shift/start`</td><td>Starts a new shift</td></tr><tr><td>POST</td><td>`/api/shift/end`</td><td>Ends the active shift</td></tr><tr><td>GET</td><td>`/api/shift/summary`</td><td>Returns live stats for the active shift</td></tr></tbody></table>

### Replace `/opt/routetrack/web/app.py`

Edit:

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

```

Paste the full file:

```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>

Shift Mode endpoints:
  - GET  /api/shift/active
  - POST /api/shift/start
  - POST /api/shift/end
  - GET  /api/shift/summary

Notes:
- This dashboard is READ-ONLY for GPS-derived tables:
    gps_points, stop_events, daily_summary
- Shift Mode DOES write to SQLite, but only into the "shifts" table.
  This avoids lock contention with the logger and keeps writes minimal.
"""</span>

<span class="token keyword">import</span> sqlite3
<span class="token keyword">from</span> datetime <span class="token keyword">import</span> datetime<span class="token punctuation">,</span> timezone

<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<span class="token punctuation">,</span> request

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>
    <span class="token triple-quoted-string string">"""
    Open SQLite connection with Row output so we can jsonify results
    via dict(row).
    """</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 keyword">def</span> <span class="token function">utc_now_iso</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""
    Return current UTC timestamp in ISO-8601 format (no microseconds).
    Example: 2025-12-25T16:05:00+00:00
    """</span>
    <span class="token keyword">return</span> datetime<span class="token punctuation">.</span>now<span class="token punctuation">(</span>timezone<span class="token punctuation">.</span>utc<span class="token punctuation">)</span><span class="token punctuation">.</span>replace<span class="token punctuation">(</span>microsecond<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">.</span>isoformat<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">"/"</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 triple-quoted-string string">"""Serve the dashboard HTML page (Leaflet UI)."""</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 comment"># ============================================================</span>
<span class="token comment"># Existing Daily Views (READ-ONLY)</span>
<span class="token comment"># ============================================================</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>
    <span class="token triple-quoted-string string">"""Return the daily_summary row for YYYY-MM-DD."""</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>
    <span class="token triple-quoted-string string">"""
    Return route points for a given day as a list of [lat, lon]
    suitable for drawing a Leaflet polyline.
    """</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 keyword">return</span> jsonify<span class="token punctuation">(</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 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>
    <span class="token triple-quoted-string string">"""
    Return stop events that START on a given day.
    """</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>

    <span class="token keyword">return</span> jsonify<span class="token punctuation">(</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 punctuation">)</span>


<span class="token comment"># ============================================================</span>
<span class="token comment"># Shift Mode (writes only to shifts table)</span>
<span class="token comment"># ============================================================</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/shift/active"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_shift_active</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""
    Returns the currently active shift (where end_ts is NULL),
    or {"active": false} if none exists.
    """</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 triple-quoted-string string">"""
        SELECT *
        FROM shifts
        WHERE end_ts IS NULL
        ORDER BY id DESC
        LIMIT 1
    """</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">"active"</span><span class="token punctuation">:</span> <span class="token boolean">False</span><span class="token punctuation">}</span><span class="token punctuation">)</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/shift/start"</span><span class="token punctuation">,</span> methods<span class="token operator">=</span><span class="token punctuation">[</span><span class="token string">"POST"</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_shift_start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""
    Start a new shift.
    Prevents multiple active shifts at once.

    Optional JSON body:
      {"note": "optional note here"}
    """</span>
    note <span class="token operator">=</span> <span class="token string">""</span>
    <span class="token keyword">try</span><span class="token punctuation">:</span>
        payload <span class="token operator">=</span> request<span class="token punctuation">.</span>get_json<span class="token punctuation">(</span>silent<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span> <span class="token keyword">or</span> <span class="token punctuation">{</span><span class="token punctuation">}</span>
        note <span class="token operator">=</span> payload<span class="token punctuation">.</span>get<span class="token punctuation">(</span><span class="token string">"note"</span><span class="token punctuation">,</span> <span class="token string">""</span><span class="token punctuation">)</span> <span class="token keyword">or</span> <span class="token string">""</span>
    <span class="token keyword">except</span> Exception<span class="token punctuation">:</span>
        note <span class="token operator">=</span> <span class="token string">""</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>

    <span class="token comment"># Block starting a shift if one is already active</span>
    cur<span class="token punctuation">.</span>execute<span class="token punctuation">(</span><span class="token string">"SELECT id FROM shifts WHERE end_ts IS NULL LIMIT 1"</span><span class="token punctuation">)</span>
    <span class="token keyword">if</span> cur<span class="token punctuation">.</span>fetchone<span class="token punctuation">(</span><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">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">"A shift is already active."</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">409</span>

    start_ts <span class="token operator">=</span> utc_now_iso<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">"INSERT INTO shifts (start_ts, note) VALUES (?, ?)"</span><span class="token punctuation">,</span>
        <span class="token punctuation">(</span>start_ts<span class="token punctuation">,</span> note<span class="token punctuation">)</span>
    <span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>commit<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 shifts WHERE id = last_insert_rowid()"</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">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/shift/end"</span><span class="token punctuation">,</span> methods<span class="token operator">=</span><span class="token punctuation">[</span><span class="token string">"POST"</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_shift_end</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""
    End the currently active shift by setting end_ts.
    """</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 triple-quoted-string string">"""
        SELECT *
        FROM shifts
        WHERE end_ts IS NULL
        ORDER BY id DESC
        LIMIT 1
    """</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>

    <span class="token keyword">if</span> <span class="token keyword">not</span> row<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">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 active shift."</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">404</span>

    end_ts <span class="token operator">=</span> utc_now_iso<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">"UPDATE shifts SET end_ts = ? WHERE id = ?"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>end_ts<span class="token punctuation">,</span> row<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>commit<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 shifts WHERE id = ?"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>row<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    updated <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">return</span> jsonify<span class="token punctuation">(</span><span class="token builtin">dict</span><span class="token punctuation">(</span>updated<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/shift/summary"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_shift_summary</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token triple-quoted-string string">"""
    Returns a lightweight summary for the ACTIVE shift window.

    Current output:
      - shift window (start -> now)
      - number of gps points in that window (mode=3)
      - stop count + stopped seconds for stop_events inside window

    NOTE:
      This does not compute miles yet. That comes next.
    """</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>

    <span class="token comment"># Find active shift</span>
    cur<span class="token punctuation">.</span>execute<span class="token punctuation">(</span><span class="token triple-quoted-string string">"""
        SELECT *
        FROM shifts
        WHERE end_ts IS NULL
        ORDER BY id DESC
        LIMIT 1
    """</span><span class="token punctuation">)</span>
    shift <span class="token operator">=</span> cur<span class="token punctuation">.</span>fetchone<span class="token punctuation">(</span><span class="token punctuation">)</span>

    <span class="token keyword">if</span> <span class="token keyword">not</span> shift<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">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 active shift."</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">404</span>

    start_ts <span class="token operator">=</span> shift<span class="token punctuation">[</span><span class="token string">"start_ts"</span><span class="token punctuation">]</span>
    end_ts <span class="token operator">=</span> utc_now_iso<span class="token punctuation">(</span><span class="token punctuation">)</span>  <span class="token comment"># "now" for active shift</span>

    <span class="token comment"># gps_points stores timestamps with trailing "Z"</span>
    <span class="token comment"># shifts stores timestamps with "+00:00"</span>
    <span class="token comment"># Convert bounds for gps_points query</span>
    start_bound <span class="token operator">=</span> start_ts<span class="token punctuation">.</span>replace<span class="token punctuation">(</span><span class="token string">"+00:00"</span><span class="token punctuation">,</span> <span class="token string">"Z"</span><span class="token punctuation">)</span>
    end_bound <span class="token operator">=</span> end_ts<span class="token punctuation">.</span>replace<span class="token punctuation">(</span><span class="token string">"+00:00"</span><span class="token punctuation">,</span> <span class="token string">"Z"</span><span class="token punctuation">)</span>

    cur<span class="token punctuation">.</span>execute<span class="token punctuation">(</span><span class="token triple-quoted-string string">"""
        SELECT COUNT(*) as point_count
        FROM gps_points
        WHERE ts >= ? AND ts <= ?
          AND mode = 3
          AND lat IS NOT NULL
          AND lon IS NOT NULL
    """</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>start_bound<span class="token punctuation">,</span> end_bound<span class="token punctuation">)</span><span class="token punctuation">)</span>
    point_row <span class="token operator">=</span> cur<span class="token punctuation">.</span>fetchone<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 triple-quoted-string string">"""
        SELECT COUNT(*) as stop_count,
               COALESCE(SUM(duration_seconds), 0) as stopped_s
        FROM stop_events
        WHERE start_ts >= ? AND end_ts <= ?
    """</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>start_ts<span class="token punctuation">,</span> end_ts<span class="token punctuation">)</span><span class="token punctuation">)</span>
    stop_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">return</span> jsonify<span class="token punctuation">(</span><span class="token punctuation">{</span>
        <span class="token string">"shift_id"</span><span class="token punctuation">:</span> shift<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string">"start_ts"</span><span class="token punctuation">:</span> start_ts<span class="token punctuation">,</span>
        <span class="token string">"end_ts"</span><span class="token punctuation">:</span> end_ts<span class="token punctuation">,</span>
        <span class="token string">"points"</span><span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">(</span>point_row<span class="token punctuation">[</span><span class="token string">"point_count"</span><span class="token punctuation">]</span> <span class="token keyword">or</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token string">"stop_count"</span><span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">(</span>stop_row<span class="token punctuation">[</span><span class="token string">"stop_count"</span><span class="token punctuation">]</span> <span class="token keyword">or</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token string">"stopped_time_seconds"</span><span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">(</span>stop_row<span class="token punctuation">[</span><span class="token string">"stopped_s"</span><span class="token punctuation">]</span> <span class="token keyword">or</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token string">"note"</span><span class="token punctuation">:</span> shift<span class="token punctuation">[</span><span class="token string">"note"</span><span class="token punctuation">]</span> <span class="token keyword">or</span> <span class="token string">""</span>
    <span class="token punctuation">}</span><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 (manual)</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

```

Restart dashboard service:

```bash
sudo systemctl restart routetrack-dashboard.service

```

---

## Dashboard UI Enhancements (Full `index.html`)

The dashboard top bar now includes Shift controls:

- Start Shift
- End Shift
- Refresh Shift
- Shift status pill

A new **Active Shift** section shows live stats and refreshes every 30 seconds.

### Replace `/opt/routetrack/web/templates/index.html`

Edit:

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

```

Paste the full file:

```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 property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span> <span class="token property">gap</span><span class="token punctuation">:</span> 10px<span class="token punctuation">;</span> <span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span> <span class="token property">flex-wrap</span><span class="token punctuation">:</span> wrap<span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">#topbar button</span> <span class="token punctuation">{</span> <span class="token property">cursor</span><span class="token punctuation">:</span> pointer<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 class="token selector">.pill</span> <span class="token punctuation">{</span> <span class="token property">display</span><span class="token punctuation">:</span> inline-block<span class="token punctuation">;</span> <span class="token property">padding</span><span class="token punctuation">:</span> 2px 8px<span class="token punctuation">;</span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 999px<span class="token punctuation">;</span> <span class="token property">font-size</span><span class="token punctuation">:</span> 12px<span class="token punctuation">;</span> <span class="token property">background</span><span class="token punctuation">:</span> #333<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></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 tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span>|<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>

    <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span><span class="token punctuation">></span></span>Date:<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
    <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 Day<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>span</span><span class="token punctuation">></span></span>|<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>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>startShift()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Start Shift<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>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>endShift()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>End Shift<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>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>loadShift()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Refresh Shift<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>span</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>shiftStatus<span class="token punctuation">"</span></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>pill<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Shift: Unknown<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>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>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>Active Shift<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>shift<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>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 shiftDiv = document.getElementById("shift");
  const shiftStatus = document.getElementById("shiftStatus");

  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 = [];

  // -----------------------------
  // Day-Based Views
  // -----------------------------
  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) {
    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>
    `;
  }

  // -----------------------------
  // Shift Mode
  // -----------------------------
  async function startShift() {
    const res = await fetch("/api/shift/start", {
      method: "POST",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify({ note: "" })
    });

    const data = await res.json();
    if (!res.ok) {
      alert(data.error || "Failed to start shift");
      return;
    }
    await loadShift();
  }

  async function endShift() {
    const res = await fetch("/api/shift/end", { method: "POST" });
    const data = await res.json();
    if (!res.ok) {
      alert(data.error || "Failed to end shift");
      return;
    }
    await loadShift();
  }

  async function loadShift() {
    shiftDiv.innerHTML = "";

    const activeRes = await fetch("/api/shift/active");
    const active = await activeRes.json();

    if (!active || active.active === false || !active.id) {
      shiftStatus.textContent = "Shift: Inactive";
      shiftDiv.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 active shift. Click <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>Start Shift<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> to begin.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>";
      return;
    }

    shiftStatus.textContent = `Shift: ACTIVE (#${active.id})`;

    const sumRes = await fetch("/api/shift/summary");
    const s = await sumRes.json();

    if (s.error) {
      shiftDiv.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>${s.error}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>`;
      return;
    }

    shiftDiv.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><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>Shift ID:<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> ${s.shift_id}<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><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>Start (UTC):<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>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>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><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>Now (UTC):<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>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><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><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>Stops (inside window):<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> ${s.stop_count}<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><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>Stopped Minutes:<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> ${Math.round(s.stopped_time_seconds / 60)}<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><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>GPS Points (mode=3):<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> ${s.points}<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();
  loadShift();

  // Auto-refresh active shift every 30s (handy for truck use)
  setInterval(loadShift, 30000);
<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>

```

Restart dashboard:

```bash
sudo systemctl restart routetrack-dashboard.service

```

---

## Validation &amp; Testing

Before starting a shift:

```bash
curl http://localhost:5000/api/shift/active

```

Expected:

```json
{"active":false}

```

```bash
curl http://localhost:5000/api/shift/summary

```

Expected:

```json
{"error":"No active shift."}

```

Dashboard validation:

- Start Shift creates an active session
- Refresh Shift updates live metrics
- End Shift closes session cleanly
- Status pill returns to “Shift: Inactive”

---

## Result

RouteTrack now supports:

- Continuous GPS logging
- Stop detection + daily summaries
- Local dashboard (Flask + Leaflet)
- **Shift Mode with user-controlled Start/End**
- Live shift summary panel in the UI

This moves RouteTrack closer to a true **truck-ready route tracking + session logging system**.

---

## Next Steps

Now that Shift Mode works end-to-end, next upgrades will add:

1. **Shift mileage + moving time**
    
    
    - Apply Haversine logic inside shift window
2. **Persist final shift totals**
    
    
    - Save a shift summary row when ending a shift
3. **Shift history**
    
    
    - List past shifts and export (CSV/GeoJSON)
4. Optional: offline map tiles

# 11 - RouteTrack Pi — Logger Service Cleanup & Boot Reliability

#### **Date:** December 25, 2025  
**Category:** Raspberry Pi / systemd / GPS / Reliability  
**Backlink:** [10 – RouteTrack Pi — Shift Mode (SQLite + Flask API + Dashboard Controls)](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/10-routetrack-pi-shift-mode-sqlite-flask-api-dashboard-controls)

---

## Project Goal

Before taking RouteTrack mobile (car/truck use), I wanted to ensure the system behaves like an appliance:

- Power on → services come up automatically
- No manual “status checks” required every boot
- Logging survives reboots and unexpected shutdowns
- Logging is independent of network availability (portable use)

This entry documents the final cleanup to the `routetrack-logger.service` unit so it is:

- Dependency-safe (waits for GPSD)
- Restart-safe (always recovers)
- Logging-clean (journald only, no conflicting directives)

---

## Problem Identified

The logger service originally contained **conflicting output directives**, which can create confusion about where logs are actually going.

Example conflict pattern:

- `StandardOutput=append:/path/to/file.log`
- followed later by `StandardOutput=journal`

In systemd, the **last directive wins**, meaning file append logging may silently stop even though it appears configured.

To keep RouteTrack stable and simple, the logger service was standardized to **journald-only** logging.

---

## Final Logger Service Configuration (Clean + Portable)

### View the service

```bash
sudo systemctl cat routetrack-logger.service

```

### Final contents used

```ini
# /etc/systemd/system/routetrack-logger.service

[Unit]
Description=RouteTrack GPS Logger

# Do not start logger until gpsd is up
After=gpsd-standalone.service network.target
Wants=gpsd-standalone.service
Requires=gpsd-standalone.service

[Service]
Type=simple

# Run from project root
WorkingDirectory=/opt/routetrack

# Use the Python venv so RouteTrack is isolated from system packages
ExecStart=/opt/routetrack/venv/bin/python /opt/routetrack/bin/routetrack-logger.py

# Restart forever on crash/disconnect
Restart=always
RestartSec=3

# Send stdout/stderr into journald
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

```

---

## Why This Unit File Is “Truck Safe”

### ✅ GPS Dependency Enforcement

Because of:

```ini
Requires=gpsd-standalone.service
After=gpsd-standalone.service

```

systemd will not attempt to start RouteTrack logging until GPSD is up.

---

### ✅ Crash Recovery

Because of:

```ini
Restart=always
RestartSec=3

```

if the logger crashes or the GPS temporarily drops, RouteTrack automatically restarts.

---

### ✅ Logging That Doesn’t Break

Because of:

```ini
StandardOutput=journal
StandardError=journal

```

all logs are always available via systemd journald, which is reliable even with power cycling.

---

## Reload &amp; Restart Procedure

After any systemd file changes:

```bash
sudo systemctl daemon-reload
sudo systemctl restart routetrack-logger.service

```

---

## Confirm Boot Enablement

The portable goal is to skip status checks entirely, so the services must auto-start on boot.

Verify enablement:

```bash
systemctl is-enabled gpsd-standalone.service routetrack-logger.service routetrack-dashboard.service

```

If any return `disabled`, enable them:

```bash
sudo systemctl enable gpsd-standalone.service routetrack-logger.service routetrack-dashboard.service

```

---

## “No Status Checks” Workflow

Once enabled:

- If the dashboard loads from my phone → dashboard service is running
- If I can press **Start Shift** → API is live
- If route points update while driving → GPSD + logger are working

This allows RouteTrack to behave like a real vehicle appliance system.

---

## Quick Troubleshooting (Single Command)

If anything seems off after boot, the first and best check is:

```bash
journalctl -u routetrack-logger -n 50 --no-pager

```

---

## Result

RouteTrack is now ready for mobile testing:

GPS logger waits for GPSD  
Logger restarts automatically if anything fails  
Journald logging is clean and consistent  
Boot enablement supports “power on and go” use

---

## Next Steps

The next phase is real-world validation:

- Start a shift before leaving
- Drive a short route (gas station test)
- End shift on return
- Run processor and confirm stop events + summaries update

# 12 - RouteTrack Pi — Mobile UX Upgrade (Shift Controls + Status)

#### **Date:** December 25th, 2025  
**Category:** Raspberry Pi / GPS / Web UI / UX  
**Backlink:** [08 — RouteTrack Pi — Local Dashboard (Leaflet + Flask)](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/08-routetrack-pi-local-web-dashboard-flask-api-leaflet-map)

---

# Project Goal

This update makes the RouteTrack dashboard **easy and safe to use from a phone**, especially since this Pi will be:

- powered down frequently
- used on the go
- accessed quickly before/after driving

The dashboard becomes a **real operator UI**:

- you can glance and instantly know whether a shift is active
- you can start/stop with big buttons (no scrolling)
- the UI recovers cleanly after reboot

---

# What We Added (UX Features)

## 1) Shift Status Badge (Top Bar)

Shows:

- 🟢 **ACTIVE** (with start time)
- 🔴 **STOPPED**

## 2) Sticky Shift Controls (Bottom Bar)

Large buttons designed for mobile thumbs:

- Start Shift (green)
- Stop Shift (red)

## 3) Button Safety Rules

- If shift is ACTIVE → Start disabled
- If shift is STOPPED → Stop disabled

## 4) Inline Toast Messages

Non-blocking confirmations like:

- “Shift started”
- “Shift ended”
- “API not reachable yet” (during boot)

## 5) Auto Refresh After Start/Stop

After changing shift state, the UI automatically refreshes:

- route line
- stops
- daily summary
- shift card

---

# File Updates

## Update `app.py` (Shift endpoints + existing APIs)

## Replace the current `/opt/routetrack/web/app.py` with this full version.

```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 (read-only route data):
  - /api/summary/<date>
  - /api/points/<date>
  - /api/stops/<date>

Shift Control Endpoints (write minimal shift state only):
  - GET  /api/shift/active
  - POST /api/shift/start
  - POST /api/shift/stop
  - GET  /api/shift/summary

Notes:
- Route endpoints are READ-ONLY from gps_points/stop_events/daily_summary.
- Shift endpoints write only to the shifts table.
"""</span>

<span class="token keyword">import</span> sqlite3
<span class="token keyword">from</span> datetime <span class="token keyword">import</span> datetime<span class="token punctuation">,</span> timezone
<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<span class="token punctuation">,</span> request

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> timeout<span class="token operator">=</span><span class="token number">10</span><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 keyword">def</span> <span class="token function">utc_now_iso</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token comment"># ISO-8601 with Z suffix (matches gps_points ts style)</span>
    <span class="token keyword">return</span> datetime<span class="token punctuation">.</span>now<span class="token punctuation">(</span>timezone<span class="token punctuation">.</span>utc<span class="token punctuation">)</span><span class="token punctuation">.</span>replace<span class="token punctuation">(</span>microsecond<span class="token operator">=</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">.</span>isoformat<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span>replace<span class="token punctuation">(</span><span class="token string">"+00:00"</span><span class="token punctuation">,</span> <span class="token string">"Z"</span><span class="token punctuation">)</span>

<span class="token comment"># -------------------------</span>
<span class="token comment"># UI</span>
<span class="token comment"># -------------------------</span>
<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 comment"># -------------------------</span>
<span class="token comment"># Health (optional)</span>
<span class="token comment"># -------------------------</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/health"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_health</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token keyword">try</span><span class="token punctuation">:</span>
        conn <span class="token operator">=</span> db<span class="token punctuation">(</span><span class="token punctuation">)</span>
        conn<span class="token punctuation">.</span>execute<span class="token punctuation">(</span><span class="token string">"SELECT 1;"</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">return</span> jsonify<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string">"ok"</span><span class="token punctuation">:</span> <span class="token boolean">True</span><span class="token punctuation">}</span><span class="token punctuation">)</span>
    <span class="token keyword">except</span> Exception <span class="token keyword">as</span> e<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">"ok"</span><span class="token punctuation">:</span> <span class="token boolean">False</span><span class="token punctuation">,</span> <span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">(</span>e<span class="token punctuation">)</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">500</span>

<span class="token comment"># -------------------------</span>
<span class="token comment"># Route data endpoints</span>
<span class="token comment"># -------------------------</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>

    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>

    <span class="token keyword">return</span> jsonify<span class="token punctuation">(</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 punctuation">)</span>

<span class="token comment"># -------------------------</span>
<span class="token comment"># Shift endpoints</span>
<span class="token comment"># -------------------------</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/shift/active"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_shift_active</span><span class="token punctuation">(</span><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 triple-quoted-string string">"""
        SELECT id, start_ts, end_ts
        FROM shifts
        WHERE end_ts IS NULL
        ORDER BY id DESC
        LIMIT 1
    """</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">"active"</span><span class="token punctuation">:</span> <span class="token boolean">False</span><span class="token punctuation">}</span><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">"active"</span><span class="token punctuation">:</span> <span class="token boolean">True</span><span class="token punctuation">,</span>
        <span class="token string">"id"</span><span class="token punctuation">:</span> row<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string">"start_ts"</span><span class="token punctuation">:</span> row<span class="token punctuation">[</span><span class="token string">"start_ts"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string">"end_ts"</span><span class="token punctuation">:</span> row<span class="token punctuation">[</span><span class="token string">"end_ts"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
    <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/shift/start"</span><span class="token punctuation">,</span> methods<span class="token operator">=</span><span class="token punctuation">[</span><span class="token string">"POST"</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_shift_start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token comment"># If already active, do nothing (idempotent-ish)</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 id, start_ts FROM shifts WHERE end_ts IS NULL ORDER BY id DESC LIMIT 1;"</span><span class="token punctuation">)</span>
    existing <span class="token operator">=</span> cur<span class="token punctuation">.</span>fetchone<span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token keyword">if</span> existing<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">return</span> jsonify<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string">"ok"</span><span class="token punctuation">:</span> <span class="token boolean">True</span><span class="token punctuation">,</span> <span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"Shift already active"</span><span class="token punctuation">,</span> <span class="token string">"id"</span><span class="token punctuation">:</span> existing<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string">"start_ts"</span><span class="token punctuation">:</span> existing<span class="token punctuation">[</span><span class="token string">"start_ts"</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token punctuation">)</span>

    start_ts <span class="token operator">=</span> utc_now_iso<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">"INSERT INTO shifts (start_ts) VALUES (?);"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>start_ts<span class="token punctuation">,</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>commit<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 id, start_ts FROM shifts WHERE end_ts IS NULL ORDER BY id DESC LIMIT 1;"</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">return</span> jsonify<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string">"ok"</span><span class="token punctuation">:</span> <span class="token boolean">True</span><span class="token punctuation">,</span> <span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"Shift started"</span><span class="token punctuation">,</span> <span class="token string">"id"</span><span class="token punctuation">:</span> row<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string">"start_ts"</span><span class="token punctuation">:</span> row<span class="token punctuation">[</span><span class="token string">"start_ts"</span><span class="token punctuation">]</span><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/shift/stop"</span><span class="token punctuation">,</span> methods<span class="token operator">=</span><span class="token punctuation">[</span><span class="token string">"POST"</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_shift_stop</span><span class="token punctuation">(</span><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 id, start_ts FROM shifts WHERE end_ts IS NULL ORDER BY id DESC LIMIT 1;"</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>
    <span class="token keyword">if</span> <span class="token keyword">not</span> row<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">return</span> jsonify<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string">"ok"</span><span class="token punctuation">:</span> <span class="token boolean">False</span><span class="token punctuation">,</span> <span class="token string">"error"</span><span class="token punctuation">:</span> <span class="token string">"No active shift."</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token number">400</span>

    end_ts <span class="token operator">=</span> utc_now_iso<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">"UPDATE shifts SET end_ts = ? WHERE id = ?;"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>end_ts<span class="token punctuation">,</span> row<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    conn<span class="token punctuation">.</span>commit<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">return</span> jsonify<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string">"ok"</span><span class="token punctuation">:</span> <span class="token boolean">True</span><span class="token punctuation">,</span> <span class="token string">"message"</span><span class="token punctuation">:</span> <span class="token string">"Shift ended"</span><span class="token punctuation">,</span> <span class="token string">"id"</span><span class="token punctuation">:</span> row<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string">"start_ts"</span><span class="token punctuation">:</span> row<span class="token punctuation">[</span><span class="token string">"start_ts"</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string">"end_ts"</span><span class="token punctuation">:</span> end_ts<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/shift/summary"</span><span class="token punctuation">)</span>
<span class="token keyword">def</span> <span class="token function">api_shift_summary</span><span class="token punctuation">(</span><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 triple-quoted-string string">"""
        SELECT id, start_ts, end_ts
        FROM shifts
        WHERE end_ts IS NULL
        ORDER BY id DESC
        LIMIT 1
    """</span><span class="token punctuation">)</span>
    shift <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> shift<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 active shift."</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 comment"># Simple summary for the UI (elapsed seconds)</span>
    start_dt <span class="token operator">=</span> datetime<span class="token punctuation">.</span>fromisoformat<span class="token punctuation">(</span>shift<span class="token punctuation">[</span><span class="token string">"start_ts"</span><span class="token punctuation">]</span><span class="token punctuation">.</span>replace<span class="token punctuation">(</span><span class="token string">"Z"</span><span class="token punctuation">,</span> <span class="token string">"+00:00"</span><span class="token punctuation">)</span><span class="token punctuation">)</span>
    now_dt <span class="token operator">=</span> datetime<span class="token punctuation">.</span>now<span class="token punctuation">(</span>timezone<span class="token punctuation">.</span>utc<span class="token punctuation">)</span>
    elapsed_s <span class="token operator">=</span> <span class="token builtin">int</span><span class="token punctuation">(</span><span class="token punctuation">(</span>now_dt <span class="token operator">-</span> start_dt<span class="token punctuation">)</span><span class="token punctuation">.</span>total_seconds<span class="token punctuation">(</span><span class="token punctuation">)</span><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">"active"</span><span class="token punctuation">:</span> <span class="token boolean">True</span><span class="token punctuation">,</span>
        <span class="token string">"id"</span><span class="token punctuation">:</span> shift<span class="token punctuation">[</span><span class="token string">"id"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string">"start_ts"</span><span class="token punctuation">:</span> shift<span class="token punctuation">[</span><span class="token string">"start_ts"</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string">"elapsed_seconds"</span><span class="token punctuation">:</span> elapsed_s
    <span class="token punctuation">}</span><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>
    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 (optional, harmless):

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

```

Restart dashboard:

```bash
sudo systemctl restart routetrack-dashboard.service

```

Quick verify:

```bash
curl http://localhost:5000/api/shift/active

```

---

## ✅ Update `index.html` (Mobile UI + sticky controls + status)

Edit:

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

```

Paste the full file:

```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">:root</span> <span class="token punctuation">{</span>
      <span class="token property">--bg</span><span class="token punctuation">:</span> #0f0f0f<span class="token punctuation">;</span>
      <span class="token property">--panel</span><span class="token punctuation">:</span> #151515<span class="token punctuation">;</span>
      <span class="token property">--text</span><span class="token punctuation">:</span> #f2f2f2<span class="token punctuation">;</span>
      <span class="token property">--muted</span><span class="token punctuation">:</span> #bdbdbd<span class="token punctuation">;</span>
      <span class="token property">--ok</span><span class="token punctuation">:</span> #16a34a<span class="token punctuation">;</span>
      <span class="token property">--stop</span><span class="token punctuation">:</span> #dc2626<span class="token punctuation">;</span>
      <span class="token property">--warn</span><span class="token punctuation">:</span> #f59e0b<span class="token punctuation">;</span>
      <span class="token property">--card</span><span class="token punctuation">:</span> #1c1c1c<span class="token punctuation">;</span>
      <span class="token property">--border</span><span class="token punctuation">:</span> #2b2b2b<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <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 property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--bg<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--text<span class="token punctuation">)</span><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 12px<span class="token punctuation">;</span>
      <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--panel<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--text<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span>
      <span class="token property">gap</span><span class="token punctuation">:</span> 10px<span class="token punctuation">;</span>
      <span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span>
      <span class="token property">flex-wrap</span><span class="token punctuation">:</span> wrap<span class="token punctuation">;</span>
      <span class="token property">border-bottom</span><span class="token punctuation">:</span> 1px solid <span class="token function">var</span><span class="token punctuation">(</span>--border<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token selector">#brand</span> <span class="token punctuation">{</span> <span class="token property">font-weight</span><span class="token punctuation">:</span> 700<span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">#statusBadge</span> <span class="token punctuation">{</span>
      <span class="token property">padding</span><span class="token punctuation">:</span> 4px 10px<span class="token punctuation">;</span>
      <span class="token property">border-radius</span><span class="token punctuation">:</span> 999px<span class="token punctuation">;</span>
      <span class="token property">font-size</span><span class="token punctuation">:</span> 12px<span class="token punctuation">;</span>
      <span class="token property">border</span><span class="token punctuation">:</span> 1px solid <span class="token function">var</span><span class="token punctuation">(</span>--border<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--card<span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token selector">.badge-active</span> <span class="token punctuation">{</span> <span class="token property">border-color</span><span class="token punctuation">:</span> <span class="token function">rgba</span><span class="token punctuation">(</span>22<span class="token punctuation">,</span>163<span class="token punctuation">,</span>74<span class="token punctuation">,</span>.6<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">.badge-stopped</span> <span class="token punctuation">{</span> <span class="token property">border-color</span><span class="token punctuation">:</span> <span class="token function">rgba</span><span class="token punctuation">(</span>220<span class="token punctuation">,</span>38<span class="token punctuation">,</span>38<span class="token punctuation">,</span>.6<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token selector">#topControls</span> <span class="token punctuation">{</span>
      <span class="token property">margin-left</span><span class="token punctuation">:</span> auto<span class="token punctuation">;</span>
      <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span>
      <span class="token property">gap</span><span class="token punctuation">:</span> 8px<span class="token punctuation">;</span>
      <span class="token property">align-items</span><span class="token punctuation">:</span> center<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token selector">input[type="date"]</span><span class="token punctuation">{</span>
      <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--card<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--text<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">border</span><span class="token punctuation">:</span> 1px solid <span class="token function">var</span><span class="token punctuation">(</span>--border<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">border-radius</span><span class="token punctuation">:</span> 8px<span class="token punctuation">;</span>
      <span class="token property">padding</span><span class="token punctuation">:</span> 6px 8px<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token selector">button</span> <span class="token punctuation">{</span>
      <span class="token property">border</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
      <span class="token property">padding</span><span class="token punctuation">:</span> 9px 12px<span class="token punctuation">;</span>
      <span class="token property">border-radius</span><span class="token punctuation">:</span> 10px<span class="token punctuation">;</span>
      <span class="token property">font-weight</span><span class="token punctuation">:</span> 700<span class="token punctuation">;</span>
      <span class="token property">cursor</span><span class="token punctuation">:</span> pointer<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token selector">button:disabled</span> <span class="token punctuation">{</span> <span class="token property">opacity</span><span class="token punctuation">:</span> 0.45<span class="token punctuation">;</span> <span class="token property">cursor</span><span class="token punctuation">:</span> not-allowed<span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token selector">.btn</span> <span class="token punctuation">{</span> <span class="token property">background</span><span class="token punctuation">:</span> #2a2a2a<span class="token punctuation">;</span> <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--text<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">border</span><span class="token punctuation">:</span> 1px solid <span class="token function">var</span><span class="token punctuation">(</span>--border<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">.btnStart</span> <span class="token punctuation">{</span> <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--ok<span class="token punctuation">)</span><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">.btnStop</span> <span class="token punctuation">{</span> <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--stop<span class="token punctuation">)</span><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> 62vh<span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token selector">#content</span> <span class="token punctuation">{</span>
      <span class="token property">padding</span><span class="token punctuation">:</span> 12px<span class="token punctuation">;</span>
      <span class="token property">display</span><span class="token punctuation">:</span> grid<span class="token punctuation">;</span>
      <span class="token property">gap</span><span class="token punctuation">:</span> 12px<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token selector">.card</span> <span class="token punctuation">{</span>
      <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--card<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">border</span><span class="token punctuation">:</span> 1px solid <span class="token function">var</span><span class="token punctuation">(</span>--border<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">border-radius</span><span class="token punctuation">:</span> 14px<span class="token punctuation">;</span>
      <span class="token property">padding</span><span class="token punctuation">:</span> 12px<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token selector">h3</span> <span class="token punctuation">{</span> <span class="token property">margin</span><span class="token punctuation">:</span> 0 0 8px 0<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 property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--muted<span class="token punctuation">)</span><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> #232323<span class="token punctuation">;</span> <span class="token property">padding</span><span class="token punctuation">:</span> 2px 6px<span class="token punctuation">;</span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 6px<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 comment">/* Sticky bottom control bar for mobile */</span>
    <span class="token selector">#shiftBar</span> <span class="token punctuation">{</span>
      <span class="token property">position</span><span class="token punctuation">:</span> sticky<span class="token punctuation">;</span>
      <span class="token property">bottom</span><span class="token punctuation">:</span> 0<span class="token punctuation">;</span>
      <span class="token property">background</span><span class="token punctuation">:</span> <span class="token function">rgba</span><span class="token punctuation">(</span>15<span class="token punctuation">,</span>15<span class="token punctuation">,</span>15<span class="token punctuation">,</span>.92<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">backdrop-filter</span><span class="token punctuation">:</span> <span class="token function">blur</span><span class="token punctuation">(</span>8px<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">border-top</span><span class="token punctuation">:</span> 1px solid <span class="token function">var</span><span class="token punctuation">(</span>--border<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">padding</span><span class="token punctuation">:</span> 10px 12px<span class="token punctuation">;</span>
      <span class="token property">display</span><span class="token punctuation">:</span> flex<span class="token punctuation">;</span>
      <span class="token property">gap</span><span class="token punctuation">:</span> 10px<span class="token punctuation">;</span>
      <span class="token property">z-index</span><span class="token punctuation">:</span> 999<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token selector">#shiftBar button</span> <span class="token punctuation">{</span>
      <span class="token property">flex</span><span class="token punctuation">:</span> 1<span class="token punctuation">;</span>
      <span class="token property">padding</span><span class="token punctuation">:</span> 14px 12px<span class="token punctuation">;</span>
      <span class="token property">border-radius</span><span class="token punctuation">:</span> 14px<span class="token punctuation">;</span>
      <span class="token property">font-size</span><span class="token punctuation">:</span> 16px<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

    <span class="token comment">/* Toast */</span>
    <span class="token selector">#toast</span> <span class="token punctuation">{</span>
      <span class="token property">position</span><span class="token punctuation">:</span> fixed<span class="token punctuation">;</span>
      <span class="token property">left</span><span class="token punctuation">:</span> 50%<span class="token punctuation">;</span>
      <span class="token property">transform</span><span class="token punctuation">:</span> <span class="token function">translateX</span><span class="token punctuation">(</span>-50%<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">bottom</span><span class="token punctuation">:</span> 86px<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">border</span><span class="token punctuation">:</span> 1px solid <span class="token function">var</span><span class="token punctuation">(</span>--border<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--text<span class="token punctuation">)</span><span class="token punctuation">;</span>
      <span class="token property">padding</span><span class="token punctuation">:</span> 10px 12px<span class="token punctuation">;</span>
      <span class="token property">border-radius</span><span class="token punctuation">:</span> 12px<span class="token punctuation">;</span>
      <span class="token property">display</span><span class="token punctuation">:</span> none<span class="token punctuation">;</span>
      <span class="token property">z-index</span><span class="token punctuation">:</span> 1000<span class="token punctuation">;</span>
      <span class="token property">max-width</span><span class="token punctuation">:</span> 92vw<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token selector">#toast.ok</span> <span class="token punctuation">{</span> <span class="token property">border-color</span><span class="token punctuation">:</span> <span class="token function">rgba</span><span class="token punctuation">(</span>22<span class="token punctuation">,</span>163<span class="token punctuation">,</span>74<span class="token punctuation">,</span>.7<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">#toast.err</span> <span class="token punctuation">{</span> <span class="token property">border-color</span><span class="token punctuation">:</span> <span class="token function">rgba</span><span class="token punctuation">(</span>220<span class="token punctuation">,</span>38<span class="token punctuation">,</span>38<span class="token punctuation">,</span>.7<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
    <span class="token selector">#toast.warn</span> <span class="token punctuation">{</span> <span class="token property">border-color</span><span class="token punctuation">:</span> <span class="token function">rgba</span><span class="token punctuation">(</span>245<span class="token punctuation">,</span>158<span class="token punctuation">,</span>11<span class="token punctuation">,</span>.7<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>

    <span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">min-width</span><span class="token punctuation">:</span> 900px<span class="token punctuation">)</span></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">#shiftBar</span> <span class="token punctuation">{</span> <span class="token property">width</span><span class="token punctuation">:</span> 520px<span class="token punctuation">;</span> <span class="token property">margin</span><span class="token punctuation">:</span> 0 auto 12px auto<span class="token punctuation">;</span> <span class="token property">border-radius</span><span class="token punctuation">:</span> 14px<span class="token punctuation">;</span> <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>span</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>brand<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>RouteTrack<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</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>statusBadge<span class="token punctuation">"</span></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>badge-stopped<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>🔴 SHIFT STOPPED<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>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 attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>topControls<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>span</span> <span class="token special-attr"><span class="token attr-name">style</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span><span class="token value css language-css"><span class="token property">color</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--muted<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token property">font-size</span><span class="token punctuation">:</span> 12px<span class="token punctuation">;</span></span><span class="token punctuation">"</span></span></span><span class="token punctuation">></span></span>Date<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span><span class="token punctuation">></span></span>
      <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">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btn<span class="token punctuation">"</span></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>Reload<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 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>content<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 attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>card<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>Active Shift<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>shiftCard<span class="token punctuation">"</span></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>Checking shift status…<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>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>card<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 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>Loading…<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>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>card<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>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 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>Loading…<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>div</span><span class="token punctuation">></span></span>

  <span class="token comment"><!-- Sticky mobile shift controls --></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>shiftBar<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">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>btnStart<span class="token punctuation">"</span></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>btnStart<span class="token punctuation">"</span></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>startShift()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Start Shift<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>button</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>btnStop<span class="token punctuation">"</span></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>btnStop<span class="token punctuation">"</span></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>stopShift()<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Stop Shift<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>toast<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>script</span><span class="token punctuation">></span></span>
  // Default date = today (browser local time)
  const dayInput = document.getElementById("day");
  dayInput.valueAsDate = new Date();

  const statusBadge = document.getElementById("statusBadge");
  const btnStart = document.getElementById("btnStart");
  const btnStop = document.getElementById("btnStop");
  const toastEl = document.getElementById("toast");

  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 = [];

  function toast(msg, type="ok") {
    toastEl.className = "";
    toastEl.classList.add(type);
    toastEl.textContent = msg;
    toastEl.style.display = "block";
    setTimeout(() => toastEl.style.display = "none", 3200);
  }

  function setShiftUI(active, start_ts=null) {
    if (active) {
      statusBadge.textContent = `🟢 SHIFT ACTIVE${start_ts ? " — " + start_ts : ""}`;
      statusBadge.classList.remove("badge-stopped");
      statusBadge.classList.add("badge-active");
      btnStart.disabled = true;
      btnStop.disabled = false;
    } else {
      statusBadge.textContent = "🔴 SHIFT STOPPED";
      statusBadge.classList.remove("badge-active");
      statusBadge.classList.add("badge-stopped");
      btnStart.disabled = false;
      btnStop.disabled = true;
    }
  }

  async function apiJSON(url, opts={}) {
    const res = await fetch(url, opts);
    let data = {};
    try { data = await res.json(); } catch(e) {}
    return { res, data };
  }

  async function refreshShiftState() {
    const { res, data } = await apiJSON("/api/shift/active");
    if (!res.ok) {
      setShiftUI(false);
      document.getElementById("shiftCard").textContent = "Shift API not reachable yet.";
      return;
    }

    setShiftUI(!!data.active, data.start_ts || null);

    if (data.active) {
      // Show elapsed time quickly
      const { data: sum } = await apiJSON("/api/shift/summary");
      if (!sum || sum.error) {
        document.getElementById("shiftCard").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>Active shift detected. 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>`;
        return;
      }
      const mins = Math.floor((sum.elapsed_seconds || 0) / 60);
      document.getElementById("shiftCard").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>Started: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>code</span><span class="token punctuation">></span></span>${sum.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>Elapsed: <span class="token tag"><span class="token tag"><span class="token punctuation"><</span>strong</span><span class="token punctuation">></span></span>${mins}<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>strong</span><span class="token punctuation">></span></span> min<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>`;
    } else {
      document.getElementById("shiftCard").textContent = "No active shift.";
    }
  }

  async function startShift() {
    btnStart.disabled = true;
    const { res, data } = await apiJSON("/api/shift/start", { method: "POST" });
    if (!res.ok || data.ok === false) {
      toast(data.error || "Failed to start shift.", "err");
      await refreshShiftState();
      return;
    }
    toast(data.message || "Shift started.", "ok");
    await refreshShiftState();
    await loadAll(); // refresh route/stops/summary
  }

  async function stopShift() {
    // One simple safety check (no popup spam):
    if (!confirm("End shift now?")) return;

    btnStop.disabled = true;
    const { res, data } = await apiJSON("/api/shift/stop", { method: "POST" });
    if (!res.ok || data.ok === false) {
      toast(data.error || "Failed to stop shift.", "err");
      await refreshShiftState();
      return;
    }
    toast(data.message || "Shift ended.", "ok");
    await refreshShiftState();
    await loadAll();
  }

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

  async function loadRoute(day) {
    const { res, data } = await apiJSON(`/api/points/${day}`);
    const pts = Array.isArray(data) ? data : [];

    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) {
    stopMarkers.forEach(m => map.removeLayer(m));
    stopMarkers = [];

    const { data } = await apiJSON(`/api/stops/${day}`);
    const stops = Array.isArray(data) ? data : [];

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

    if (!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 || 0) / 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 { data } = await apiJSON(`/api/summary/${day}`);

    if (!data || 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>
    `;
  }

  // Boot behavior: shift state first, then route data
  (async () => {
    try {
      await refreshShiftState();
      await loadAll();
    } catch (e) {
      toast("Dashboard loading… waiting on services.", "warn");
    }

    // Light auto-refresh of shift status every 15s
    setInterval(refreshShiftState, 15000);
  })();
<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>

```

Restart dashboard:

```bash
sudo systemctl restart routetrack-dashboard.service

```

---

# Verification

## Confirm shift API works

```bash
curl http://localhost:5000/api/shift/active

```

Start shift:

```bash
curl -X POST http://localhost:5000/api/shift/start

```

Stop shift:

```bash
curl -X POST http://localhost:5000/api/shift/stop

```

Then load the dashboard from your phone and confirm:

- status badge flips correctly
- buttons enable/disable properly
- map + stats refresh after shift actions

---

# Why This UX Matters (for a portable device)

This dashboard is now resilient for:

- frequent power-off/on cycles
- quick “start shift / drive / stop shift” workflows
- using the UI one-handed on a phone

It reduces mistakes and removes uncertainty — which is exactly what you want when this becomes a daily tool.

---

## Next Steps

1. Add **“Shift view” mode** (show only points within the active shift window)
2. Add **start/stop shift button inside the map** (floating control)
3. Improve stop detection with:
    
    
    - ignition off detection (optional)
    - drift suppression using epx/epy thresholds

# 13 - RouteTrack Pi — Mobile Dashboard Overhaul + BIG LIVE MODE (Stop Point)

#### **Date:** December 25, 2025  
**Category:** UI/UX / Mobile Optimization / Dashboard  
**Backlink:** [Update #12 – RouteTrack Pi — Mobile UX Upgrade (Shift Controls + Status)](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/12-routetrack-pi-mobile-ux-upgrade-shift-controls-status)

---

## Goal of This Update

This update was focused on making the RouteTrack dashboard **actually usable on mobile** (Firefox on Pixel 10 Pro XL), without sacrificing any of the power/features that were added during earlier iterations.

The main objective was:

- **Mobile-first layout**
- **Shift controls always easy to tap**
- A single, one-press **BIG LIVE MODE** for “driving glance” usage
- Keep the map **contained inside its UI box**
- Preserve all existing RouteTrack dashboard functionality (shift summary, daily summary, stops, filtering, speed-colored route, live polling, etc.)

This is a **good stopping point** for the project until real-world usage exposes what should be improved next.

---

## What Was Added / Improved

### Mobile UX + Layout Improvements

- Rebuilt the layout to be **mobile-first** (not desktop-first).
- The top UI was redesigned into a **compact stacked header** with:
    
    
    - Date picker + Load
    - Shift controls block
    - Collapsible controls drawer

### BIG LIVE MODE (One-Press “Driving View”)

- Added a dedicated **BIG LIVE MODE** button that:
    
    
    - Hides all nonessential UI
    - Expands the map to **full screen**
    - Keeps **LIVE MPH** visible at all times
    - Ensures Live Mode is ON (auto-enables if needed)
    - Provides an always-available **Exit** button

### Live MPH + Live Status HUD

- A floating HUD on the map displays:
    
    
    - **LIVE MPH**
    - Live status (ON / OFF / NO FIX / ERR)
- The HUD is set to `pointer-events: none` so it **won’t block map gestures** on mobile.

### Controls Drawer (Mobile Only)

- A **Controls ▾** drawer was added to reduce clutter on mobile.
- This keeps the map usable without constantly fighting UI height.

### Preserved Features (No Regressions)

This update explicitly **kept all previously implemented functionality**:

- Speed-colored route rendering (blue/amber/red)
- Daily Summary (with filtered stop totals + raw DB totals)
- Stops list and map markers
- Hide individual stops (saved in browser localStorage)
- Filter stops above X minutes + Clear Hidden Stops
- Live mode polling from `/api/live/latest`
- Shift start/end controls
- Shift status + shift summary refresh loop

---

## Files Modified

### Dashboard HTML Template

- **File:** `/opt/routetrack/web/templates/index.html`
- **Purpose:** Main mobile-first UI + map + controls + live mode + summaries

---

## Service Restart (Apply Changes)

After updating the template:

```bash
sudo systemctl restart routetrack-dashboard.service
```

Optional: If the browser still shows the old UI on mobile, fully close the tab and reopen (mobile Firefox can be aggressive with caching).

---

## Current Project Status

This update marks a clean **pause point** for the RouteTrack Pi project.

The dashboard is now:

- Useable on mobile
- Stable enough to run day-to-day
- Ready for real-world testing (actual driving + shift usage)

Future improvements will be based on real usage (accuracy, stop detection, distance consistency, etc.).

---

## Full Script: `index.html` (Final Version for Update #13)

## Location: `/opt/routetrack/web/templates/index.html`

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>RouteTrack Dashboard</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />

  <!-- Leaflet -->
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
  <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>

  <style>
    :root{
      --bg:#0b0b0b;
      --panel:#121212;
      --panel2:#0f0f0f;
      --border:#222;
      --muted: rgba(255,255,255,0.78);
      --text:#eaeaea;
      --btn:#1a1a1a;
      --btnHover:#222;
      --ok:#86efac;
      --warn:#fca5a5;
      --blue:#3b82f6;
      --amber:#f59e0b;
      --red:#ef4444;
      --chip:#171717;
    }

    * { box-sizing: border-box; }
    body { margin:0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background:var(--bg); color:var(--text); }

    .muted { color: var(--muted); }
    .ok { color: var(--ok); }
    .warn { color: var(--warn); }
    .tiny { font-size: 0.9em; }
    .brand { font-weight: 900; letter-spacing: 0.4px; user-select:none; }
    .hr { height:1px; background: var(--border); margin: 10px 0; }

    button {
      cursor:pointer;
      border:1px solid var(--border);
      background: var(--btn);
      color:#fff;
      padding: 10px 12px;
      border-radius: 12px;
      font-weight: 900;
      line-height: 1;
      white-space: nowrap;
    }
    button:hover { background: var(--btnHover); }
    button:disabled { opacity:0.5; cursor:not-allowed; }

    input[type="date"],
    input[type="number"]{
      background: var(--btn);
      color:#fff;
      border:1px solid var(--border);
      border-radius: 12px;
      padding: 9px 10px;
      font-weight: 900;
      max-width: 100%;
    }

    .toggle {
      display:inline-flex; gap:8px; align-items:center;
      padding: 9px 10px;
      border: 1px solid var(--border);
      border-radius: 999px;
      background: var(--btn);
      font-weight: 900;
    }
    .toggle input { transform: scale(1.15); }

    .chip{
      display:inline-flex;
      align-items:center;
      gap: 10px;
      padding: 10px 10px;
      border-radius: 14px;
      background: var(--chip);
      border: 1px solid var(--border);
      flex-wrap: wrap;
      max-width: 100%;
    }

    .legendRow{
      display:flex;
      gap: 12px;
      flex-wrap: wrap;
      align-items:center;
      font-size: 0.95em;
    }
    .swatch { font-weight: 900; width: 14px; display:inline-block; text-align:center; }

    /* ====== APP SHELL ====== */
    #shell{
      display:flex;
      flex-direction: column;
      min-height: 100vh;
      background: var(--bg);
    }

    /* ====== TOPBAR (Mobile-first, compact) ====== */
    #topbar{
      background: var(--panel);
      border-bottom: 1px solid var(--border);
      padding: 10px 12px;
      display:flex;
      flex-direction: column;
      gap: 10px;
      position: static; /* mobile: NOT sticky */
      z-index: 1000;
    }

    .topRow{
      display:flex;
      align-items:center;
      gap: 10px;
      flex-wrap: wrap;
    }

    .grow { flex: 1 1 auto; }

    /* Shift badge (mobile template vibe) */
    .shiftBadge{
      padding: 10px 12px;
      border-radius: 14px;
      border: 1px solid var(--border);
      background: #101010;
      font-weight: 900;
      display:flex;
      gap: 10px;
      align-items:center;
      flex-wrap: wrap;
      justify-content: space-between;
      width: 100%;
    }

    .shiftBadge .left{
      display:flex;
      gap: 10px;
      align-items:center;
      flex-wrap: wrap;
    }

    .bigBtn{
      padding: 14px 14px;
      border-radius: 16px;
      font-weight: 950;
      font-size: 1rem;
    }

    .btnDanger { border-color: #3b1b1b; background: #1a1010; }
    .btnDanger:hover { background: #241010; }
    .btnAccent { border-color:#12305f; background:#0f1a2a; }
    .btnAccent:hover{ background:#13213a; }

    /* Controls drawer (collapsed on mobile, open by choice) */
    #controlsDrawer{
      display:none;
      flex-direction: column;
      gap: 10px;
      padding: 10px;
      border: 1px solid var(--border);
      border-radius: 14px;
      background: #101010;
    }
    #controlsDrawer.open{ display:flex; }

    /* ====== MAP ====== */
    #mapWrap{
      position: relative;
      background: var(--bg);
      border-bottom: 1px solid var(--border);
      overflow: hidden; /* keep map + controls contained */
    }

    /* dvh fixes mobile toolbar height weirdness */
    #map{ width:100%; height: 62dvh; }

    /* Floating MPH + Live status */
    #hud{
      position: absolute;
      right: 12px;
      top: 12px;
      z-index: 650;
      display:flex;
      flex-direction: column;
      gap: 8px;
      align-items: flex-end;
      pointer-events: none; /* do not block map gestures */
    }

    .hudBox{
      padding: 10px 12px;
      border-radius: 14px;
      border: 1px solid var(--border);
      background: rgba(16,16,16,0.92);
      backdrop-filter: blur(4px);
      font-weight: 950;
      display:flex;
      gap: 10px;
      align-items: baseline;
      min-width: 165px;
      justify-content: center;
    }
    .hudBox small{ font-weight: 800; font-size: 0.8rem; color: var(--muted); }

    /* ====== CONTENT BELOW MAP ====== */
    #content{
      padding: 12px;
      display:grid;
      grid-template-columns: 1fr;
      gap: 12px;
    }

    .card{
      background: var(--panel2);
      border: 1px solid var(--border);
      border-radius: 14px;
      padding: 12px;
      box-shadow: 0 10px 24px rgba(0,0,0,0.25);
    }

    h3{ margin:0 0 10px; font-size: 1.05rem; }

    .grid2 { display:grid; grid-template-columns: 1fr 1fr; gap: 10px; }
    @media (max-width: 420px) { .grid2 { grid-template-columns: 1fr; } }

    code {
      background: #1f1f1f;
      padding: 2px 6px;
      border-radius: 6px;
      color: #d7d7d7;
      word-break: break-word;
    }

    .stopItem {
      padding: 10px;
      border-radius: 12px;
      border: 1px solid var(--border);
      background: #101010;
      display:flex;
      flex-direction: column;
      gap: 8px;
    }
    .stopActions { display:flex; gap: 8px; flex-wrap: wrap; }
    .btnSmall { padding: 10px 10px; border-radius: 12px; font-weight: 950; }

    /* ====== BIG LIVE MODE ====== */
    body.bigLive #topbar,
    body.bigLive #content{
      display:none;
    }
    body.bigLive #map{
      height: 100dvh;
    }
    body.bigLive #mapWrap{
      border-bottom: none;
    }
    #exitBigLive{
      position:absolute;
      left: 12px;
      top: 12px;
      z-index: 700;
      pointer-events: auto;
      display:none;
    }
    body.bigLive #exitBigLive{ display:inline-flex; }

    /* ====== DESKTOP ENHANCEMENTS ====== */
    @media (min-width: 900px){
      #topbar{
        position: sticky;
        top: 0;
      }
      #map{ height: 68vh; }
      #content{
        grid-template-columns: 1fr 1fr;
        align-items:start;
      }
      #controlsDrawer{
        display:flex;
        flex-direction: row;
        align-items:center;
        flex-wrap: wrap;
        gap: 10px;
        padding: 0;
        border: none;
        background: transparent;
      }
      #btnControlsToggle{ display:none; }
      .shiftBadge{ width:auto; }
    }
  </style>
</head>

<body>
<div id="shell">

  <div id="topbar">
    <div class="topRow">
      <span class="brand">RouteTrack</span>
      <span class="muted tiny">Local Dashboard</span>

      <span class="chip">
        <span class="muted">Date</span>
        <input id="day" type="date" />
        <button onclick="loadAll()">Load</button>
      </span>

      <button id="btnControlsToggle" class="btnAccent" onclick="toggleControls()">Controls ▾</button>
    </div>

    <div class="shiftBadge">
      <div class="left">
        <span id="shiftStatus" class="muted">Shift: …</span>
        <span id="shiftSince" class="muted tiny"></span>
      </div>

      <div class="topRow">
        <button id="btnStartShift" class="bigBtn" onclick="startShift()">Start Shift</button>
        <button id="btnEndShift" class="bigBtn btnDanger" onclick="endShift()">End Shift</button>
        <button id="btnBigLive" class="bigBtn btnAccent" onclick="toggleBigLiveMode()">BIG LIVE MODE</button>
      </div>
    </div>

    <div id="controlsDrawer">
      <div class="chip legendRow">
        <span><span class="swatch" style="color:var(--blue);">■</span> &lt; 5 mph</span>
        <span><span class="swatch" style="color:var(--amber);">■</span> 5–25 mph</span>
        <span><span class="swatch" style="color:var(--red);">■</span> &gt; 25 mph</span>
      </div>

      <div class="chip">
        <span class="toggle">
          <input id="liveToggle" type="checkbox" onchange="toggleLive()" />
          <label for="liveToggle">Live</label>
        </span>

        <span class="toggle muted">
          <input id="liveCenter" type="checkbox" />
          <label for="liveCenter">Center</label>
        </span>

        <span id="liveStatus" class="muted">Live: off</span>
      </div>

      <div class="chip grow">
        <span class="muted">Stop Filter</span>

        <span class="toggle muted">
          <input id="ignoreLongStopsToggle" type="checkbox" checked onchange="renderStopsAndFilteredSummary()" />
          <label for="ignoreLongStopsToggle">Ignore &gt;</label>
        </span>

        <input id="ignoreLongStopsMinutes" type="number" min="0" step="10" value="240"
               title="Stops longer than this many minutes will be ignored for display + summary."
               onchange="renderStopsAndFilteredSummary()" />

        <span class="muted tiny">min</span>

        <button class="btnSmall btnDanger" onclick="clearHiddenStops()">Clear Hidden Stops</button>
      </div>
    </div>
  </div>

  <div id="mapWrap">
    <div id="map"></div>
    <button id="exitBigLive" class="btnDanger" onclick="toggleBigLiveMode()">Exit</button>

    <div id="hud">
      <div class="hudBox">
        <small>LIVE MPH</small>
        <span id="mphValue">—</span>
      </div>
      <div class="hudBox">
        <small>LIVE</small>
        <span id="hudLive">OFF</span>
      </div>
    </div>
  </div>

  <div id="content">

    <div class="card">
      <h3>Shift Summary</h3>
      <div id="shiftSummary" class="muted">Loading shift summary…</div>
      <div class="hr"></div>
      <div class="muted tiny">
        Shift Summary is computed from GPS points between <strong>Start Shift</strong> and <strong>End Shift</strong>.
        If a shift is active, this updates live.
      </div>
    </div>

    <div class="card">
      <h3>Daily Summary</h3>
      <div id="summary"></div>
      <div class="hr"></div>
      <div class="muted tiny">
        Tip: Daily Summary is based on your daily processor run. Shift Summary is the “real-world” view for work sessions.
      </div>
    </div>

    <div class="card">
      <h3>Stops</h3>
      <div class="muted tiny">
        You can hide individual stops (saved in your browser on this device). Hidden/filtered stops won’t count in the UI totals.
      </div>
      <div class="hr"></div>
      <div id="stops"></div>
    </div>

    <div class="card">
      <h3>Quick Checks</h3>
      <div class="tiny muted">
        • If distance looks wrong for the date, re-run the processor for that date.<br>
        • For “real” totals (crossing midnight), rely on Shift Summary.<br>
        • Live MPH is from latest logged point in gps_points.
      </div>
    </div>

  </div>
</div>

<script>
  function toggleControls(){
    const d = document.getElementById("controlsDrawer");
    d.classList.toggle("open");
    const btn = document.getElementById("btnControlsToggle");
    btn.textContent = d.classList.contains("open") ? "Controls ▴" : "Controls ▾";
    setTimeout(() => { map.invalidateSize(true); }, 150);
  }
  function closeControlsOnMobile(){
    if (window.matchMedia("(max-width: 899px)").matches){
      const d = document.getElementById("controlsDrawer");
      const btn = document.getElementById("btnControlsToggle");
      d.classList.remove("open");
      btn.textContent = "Controls ▾";
    }
  }

  function toggleBigLiveMode(){
    document.body.classList.toggle("bigLive");
    if (document.body.classList.contains("bigLive")){
      if (!liveToggle.checked){
        liveToggle.checked = true;
        toggleLive();
      }
    }
    setTimeout(() => map.invalidateSize(true), 200);
  }

  const dayInput = document.getElementById("day");
  dayInput.valueAsDate = new Date();

  const ignoreLongStopsToggle = document.getElementById("ignoreLongStopsToggle");
  const ignoreLongStopsMinutes = document.getElementById("ignoreLongStopsMinutes");

  const btnStartShift = document.getElementById("btnStartShift");
  const btnEndShift   = document.getElementById("btnEndShift");
  const shiftStatus   = document.getElementById("shiftStatus");
  const shiftSince    = document.getElementById("shiftSince");
  const shiftSummaryDiv = document.getElementById("shiftSummary");

  const liveToggle = document.getElementById("liveToggle");
  const liveCenter = document.getElementById("liveCenter");
  const liveStatus = document.getElementById("liveStatus");

  const mphValue = document.getElementById("mphValue");
  const hudLive = document.getElementById("hudLive");

  const map = L.map("map", { zoomControl: false }).setView([38.7153, -89.94], 13);
  L.control.zoom({ position: "bottomright" }).addTo(map);
  L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
    maxZoom: 19,
    attribution: "&copy; OpenStreetMap contributors"
  }).addTo(map);

  const MPS_TO_MPH = 2.23694;

  let routeBoundsHelper = null;
  let routeSegments = [];
  let stopMarkers = [];

  let liveMarker = null;
  let liveTimer = null;
  let shiftTimer = null;

  let stopsCache = [];
  let summaryCache = null;

  const LS_HIDDEN_STOPS_KEY = "routetrack_hidden_stops_v1";

  function getHiddenStopsSet() {
    try {
      const raw = localStorage.getItem(LS_HIDDEN_STOPS_KEY);
      if (!raw) return new Set();
      return new Set(JSON.parse(raw));
    } catch { return new Set(); }
  }

  function saveHiddenStopsSet(setObj) {
    localStorage.setItem(LS_HIDDEN_STOPS_KEY, JSON.stringify(Array.from(setObj)));
  }

  function stopKey(s) {
    return `${s.start_ts}|${s.end_ts}|${s.lat}|${s.lon}|${s.duration_seconds}`;
  }

  function clearHiddenStops() {
    localStorage.removeItem(LS_HIDDEN_STOPS_KEY);
    renderStopsAndFilteredSummary();
  }

  function fmtMph(speedMps) {
    if (speedMps === null || speedMps === undefined) return "—";
    const mph = speedMps * MPS_TO_MPH;
    return mph.toFixed(1);
  }

  function fmtLocal(ts) {
    try {
      const d = new Date(ts);
      if (isNaN(d.getTime())) return ts;
      return d.toLocaleString(undefined, {
        year: "numeric", month: "2-digit", day: "2-digit",
        hour: "2-digit", minute: "2-digit"
      });
    } catch { return ts; }
  }

  function speedStyle(speedMps) {
    if (speedMps === null || speedMps === undefined) return { color: "#777", weight: 5, opacity: 0.7 };
    const mph = speedMps * MPS_TO_MPH;
    if (mph < 5)  return { color: "var(--blue)",  weight: 5, opacity: 0.85 };
    if (mph < 25) return { color: "var(--amber)", weight: 5, opacity: 0.9 };
    return          { color: "var(--red)",   weight: 5, opacity: 0.9 };
  }

  function getPanPadding() {
    const isDesktop = window.matchMedia("(min-width: 900px)").matches;
    const topbar = document.getElementById("topbar");
    const top = (isDesktop && topbar) ? topbar.offsetHeight : 0;
    return {
      paddingTopLeft: [20, top + 20],
      paddingBottomRight: [20, 40]
    };
  }

  async function loadAll() {
    const day = dayInput.value;
    await Promise.all([
      loadRoute(day),
      loadSummary(day),
      loadStops(day),
      refreshShiftStatus(),
      refreshShiftSummary(),
    ]);
    closeControlsOnMobile();
  }

  async function loadRoute(day) {
    routeSegments.forEach(seg => map.removeLayer(seg));
    routeSegments = [];
    if (routeBoundsHelper) { map.removeLayer(routeBoundsHelper); routeBoundsHelper = null; }

    const res = await fetch(`/api/points_detailed/${day}`);
    const pts = await res.json();
    if (!Array.isArray(pts) || pts.length < 2) return;

    for (let i = 1; i < pts.length; i++) {
      const a = pts[i - 1];
      const b = pts[i];
      const seg = L.polyline([[a.lat, a.lon], [b.lat, b.lon]], speedStyle(b.speed)).addTo(map);
      routeSegments.push(seg);
    }

    const latlngs = pts.map(p => [p.lat, p.lon]);
    routeBoundsHelper = L.polyline(latlngs, { opacity: 0 }).addTo(map);
    map.fitBounds(routeBoundsHelper.getBounds(), { padding: [18, 18] });
  }

  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) {
      summaryCache = null;
      summaryDiv.innerHTML = `<div class="muted">No summary for ${day}. Re-run processor for that date.</div>`;
      return;
    }

    summaryCache = data;
    renderStopsAndFilteredSummary();
  }

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

    const res = await fetch(`/api/stops/${day}`);
    const stops = await res.json();
    stopsCache = Array.isArray(stops) ? stops : [];
    renderStopsAndFilteredSummary();
  }

  function getFilteredStops() {
    const hidden = getHiddenStopsSet();
    const ignoreLong = ignoreLongStopsToggle.checked;
    const cutoffMin = Number(ignoreLongStopsMinutes.value || 0);

    return stopsCache.filter(s => {
      const k = stopKey(s);
      if (hidden.has(k)) return false;
      if (ignoreLong && cutoffMin > 0) {
        const durMin = Number(s.duration_seconds) / 60;
        if (durMin > cutoffMin) return false;
      }
      return true;
    });
  }

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

    stopMarkers.forEach(m => map.removeLayer(m));
    stopMarkers = [];

    const filtered = getFilteredStops();

    if (!stopsCache.length) {
      stopsDiv.innerHTML = "<div class='muted'>No stops found.</div>";
    } else if (!filtered.length) {
      stopsDiv.innerHTML = "<div class='muted'>All stops are hidden/filtered.</div>";
    } else {
      filtered.forEach(s => {
        const durMin = Math.round(Number(s.duration_seconds) / 60);

        const item = document.createElement("div");
        item.className = "stopItem";
        item.innerHTML = `
          <div>
            <div><strong>Stop</strong> • ${durMin} min</div>
            <div class="tiny muted">Start: ${fmtLocal(s.start_ts)}</div>
            <div class="tiny muted">End: ${fmtLocal(s.end_ts)}</div>
            <div class="tiny muted">Coords: ${Number(s.lat).toFixed(6)}, ${Number(s.lon).toFixed(6)}</div>
          </div>
          <div class="stopActions">
            <button class="btnSmall" data-action="zoom">Zoom</button>
            <button class="btnSmall btnDanger" data-action="hide">Hide</button>
          </div>
        `;

        item.querySelector('button[data-action="zoom"]').onclick = () => {
          map.setView([s.lat, s.lon], Math.max(map.getZoom(), 16), { animate: true });
        };

        item.querySelector('button[data-action="hide"]').onclick = () => {
          const setObj = getHiddenStopsSet();
          setObj.add(stopKey(s));
          saveHiddenStopsSet(setObj);
          renderStopsAndFilteredSummary();
        };

        stopsDiv.appendChild(item);

        const popup = `
          <div style="min-width:230px">
            <div><strong>Stop</strong> (${durMin} min)</div>
            <div style="margin-top:6px"><strong>Start:</strong><br>${fmtLocal(s.start_ts)}</div>
            <div style="margin-top:6px"><strong>End:</strong><br>${fmtLocal(s.end_ts)}</div>
            <div style="margin-top:6px"><strong>Coords:</strong> ${Number(s.lat).toFixed(6)}, ${Number(s.lon).toFixed(6)}</div>
          </div>
        `;
        const m = L.marker([s.lat, s.lon]).addTo(map).bindPopup(popup);
        stopMarkers.push(m);
      });
    }

    renderDailySummaryWithFilteredStops(filtered);
  }

  function renderDailySummaryWithFilteredStops(filteredStops) {
    const summaryDiv = document.getElementById("summary");
    summaryDiv.innerHTML = "";

    if (!summaryCache) {
      summaryDiv.innerHTML = `<div class="muted">No daily summary loaded.</div>`;
      return;
    }

    const filteredStopCount = filteredStops.length;
    const filteredStoppedSeconds = filteredStops.reduce((acc, s) => acc + Number(s.duration_seconds || 0), 0);

    const movingMin = Math.round(Number(summaryCache.moving_time_seconds || 0) / 60);
    const stoppedMin = Math.round(filteredStoppedSeconds / 60);

    summaryDiv.innerHTML = `
      <div class="grid2">
        <div><span class="muted">Start</span><br>${fmtLocal(summaryCache.start_ts)}</div>
        <div><span class="muted">End</span><br>${fmtLocal(summaryCache.end_ts)}</div>
      </div>

      <div class="hr"></div>

      <div class="grid2">
        <div><span class="muted">Distance</span><br><strong>${summaryCache.total_distance_miles}</strong> miles</div>
        <div><span class="muted">Moving</span><br><strong>${movingMin}</strong> minutes</div>
      </div>

      <div class="grid2">
        <div><span class="muted">Stopped (filtered)</span><br><strong>${stoppedMin}</strong> minutes</div>
        <div><span class="muted">Stops (filtered)</span><br><strong>${filteredStopCount}</strong></div>
      </div>

      <div class="tiny muted">
        Raw DB: stopped ${Math.round(Number(summaryCache.stopped_time_seconds||0)/60)} min •
        stops ${summaryCache.stop_count}
      </div>
    `;
  }

  async function refreshShiftStatus() {
    try {
      const res = await fetch("/api/shift/status");
      const data = await res.json();

      if (data.active) {
        btnStartShift.disabled = true;
        btnEndShift.disabled = false;
        shiftStatus.innerHTML = `Shift: <span class="ok">ACTIVE</span>`;
        shiftSince.innerHTML = `since ${fmtLocal(data.shift.start_ts)}`;
      } else {
        btnStartShift.disabled = false;
        btnEndShift.disabled = true;
        shiftStatus.innerHTML = `Shift: <span class="warn">INACTIVE</span>`;
        shiftSince.textContent = "";
      }
    } catch {
      shiftStatus.textContent = "Shift: error";
      shiftSince.textContent = "";
    }
  }

  async function startShift() {
    btnStartShift.disabled = true;
    try {
      const res = await fetch("/api/shift/start", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({})
      });
      const text = await res.text();
      let data = {};
      try { data = JSON.parse(text); } catch {}
      if (!res.ok) alert((data && data.error) ? data.error : `Failed (HTTP ${res.status})`);
    } catch {
      alert("Failed to start shift (request failed)");
    }
    await refreshShiftStatus();
    await refreshShiftSummary();
  }

  async function endShift() {
    btnEndShift.disabled = true;
    try {
      const res = await fetch("/api/shift/end", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({})
      });
      const text = await res.text();
      let data = {};
      try { data = JSON.parse(text); } catch {}
      if (!res.ok) alert((data && data.error) ? data.error : `Failed (HTTP ${res.status})`);
    } catch {
      alert("Failed to end shift (request failed)");
    }
    await refreshShiftStatus();
    await refreshShiftSummary();
  }

  async function refreshShiftSummary() {
    try {
      const res = await fetch("/api/shift/summary");
      const data = await res.json();
      if (!res.ok || data.error) {
        shiftSummaryDiv.innerHTML = `<div class="muted">No shift summary available yet.</div>`;
        return;
      }

      const sum = data.summary;
      const activeLabel = data.active ? `<span class="ok">ACTIVE</span>` : `<span class="warn">COMPLETED</span>`;

      shiftSummaryDiv.innerHTML = `
        <div>Shift: ${activeLabel}</div>

        <div class="grid2" style="margin-top:8px;">
          <div><span class="muted">Start</span><br>${fmtLocal(sum.start_ts)}</div>
          <div><span class="muted">End</span><br>${fmtLocal(sum.end_ts)}</div>
        </div>

        <div class="hr"></div>

        <div class="grid2">
          <div><span class="muted">Distance</span><br><strong>${sum.distance_miles}</strong> miles</div>
          <div><span class="muted">Max Speed</span><br><strong>${sum.max_mph}</strong> mph</div>
        </div>

        <div class="grid2">
          <div><span class="muted">Moving</span><br><strong>${sum.moving_minutes}</strong> minutes</div>
          <div><span class="muted">Stopped (dwell)</span><br><strong>${sum.stopped_minutes}</strong> minutes</div>
        </div>

        <div><span class="muted">Stops (dwell)</span><br><strong>${sum.stop_count}</strong></div>

        <div class="tiny muted" style="margin-top:6px;">
          Points processed: ${sum.points}
        </div>
      `;
    } catch {
      shiftSummaryDiv.innerHTML = `<div class="muted">Shift summary error.</div>`;
    }
  }

  async function pollLiveOnce() {
    try {
      const res = await fetch("/api/live/latest");
      const data = await res.json();

      if (!res.ok || data.error) {
        liveStatus.innerHTML = `Live: <span class="warn">no fix</span>`;
        mphValue.textContent = "—";
        hudLive.textContent = "NO FIX";
        return;
      }

      const latlng = [data.lat, data.lon];
      const mph = fmtMph(data.speed);

      mphValue.textContent = mph;
      hudLive.textContent = "ON";

      const label = `${fmtLocal(data.ts)} • ${mph} mph`;

      if (!liveMarker) {
        liveMarker = L.marker(latlng).addTo(map).bindPopup(label);
      } else {
        liveMarker.setLatLng(latlng);
        liveMarker.setPopupContent(label);
      }

      liveStatus.innerHTML = `Live: <span class="ok">ON</span> • <span class="muted">${mph} mph</span>`;

      if (liveCenter.checked) {
        map.panInside(latlng, getPanPadding());
      }
    } catch {
      liveStatus.innerHTML = `Live: <span class="warn">error</span>`;
      mphValue.textContent = "—";
      hudLive.textContent = "ERR";
    }
  }

  function toggleLive() {
    if (liveToggle.checked) {
      liveStatus.innerHTML = `Live: <span class="ok">starting…</span>`;
      hudLive.textContent = "ON";
      pollLiveOnce();
      liveTimer = setInterval(pollLiveOnce, 2000);
      if (!shiftTimer) shiftTimer = setInterval(refreshShiftSummary, 5000);
    } else {
      if (liveTimer) clearInterval(liveTimer);
      liveTimer = null;
      liveStatus.textContent = "Live: off";
      mphValue.textContent = "—";
      hudLive.textContent = "OFF";
      if (shiftTimer) { clearInterval(shiftTimer); shiftTimer = null; }
    }
  }

  window.addEventListener("resize", () => setTimeout(() => map.invalidateSize(true), 150));

  loadAll();
  setInterval(refreshShiftStatus, 10000);
  setInterval(refreshShiftSummary, 15000);
</script>
</body>
</html>

```

[![image.png](https://docs.natenetworks.com/uploads/images/gallery/2025-12/scaled-1680-/Eo1SNnC1U31Sd1bl-image.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-12/Eo1SNnC1U31Sd1bl-image.png)

</body></html>

# Pi Toolkit Menu with ARP and Network Scanning

#### **Date:** June 7, 2025  
**Category:** Raspberry Pi Toolkit / Network Scanning  
**Backlink:** [Pi 5 System Summary Script with AWK](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/pi-5-system-summary-script-with-awk)

---

### 🛠️ Overview

This entry documents the setup of a modular Raspberry Pi 5 toolkit with a main menu and submenu for essential system tasks and network scanning. It enables quick access to system updates, resource summaries, and LAN device discovery using both ping-based and ARP-based tools.

---

### 📁 Script Overview

```bash
~/tskmenu.sh                    # Main toolkit launcher
~/network-tools-menu.sh         # Submenu for network-related tools
~/network-scan.sh               # Ping-based sweep with hostnames
~/pi5-system-summary.sh         # Custom AWK system overview
~/update.sh                     # Apt updater/cleaner

```

---

### 🧭 Main Menu (`tskmenu.sh`)

```bash
figlet "Nates Pi Toolkit"
❌ 0. Exit
⚒️ 1. Update, Upgrade, and Auto-Remove
📊 2. System Summary
📡 3. Enter Network Tools Menu

```

Selecting option `3` brings up the network tools submenu.

---

### 🌐 Network Tools Menu (`network-tools-menu.sh`)

```bash
figlet "Network Tools Menu"
1. 🌐 Network IP Scan (Ping + Hostnames)
2. 🧭 ARP-Scan (MAC Vendors)
0. 🔙 Back to Main Menu

```

- Option `1` runs `~/network-scan.sh`
- Option `2` runs `sudo arp-scan --localnet --interface=wlan0`

---

### 🔧 Fixing arp-scan Vendor Lookup

#### Problem:

Default arp-scan install via `apt` gave:

- Permission denied errors on `ieee-oui.txt`
- “Unknown” MAC vendors

#### Solution:

1. **Removed default install**:
    
    ```bash
    sudo apt remove arp-scan -y
    
    ```
2. **Installed from source**:
    
    ```bash
    git clone https://github.com/royhills/arp-scan.git
    cd arp-scan
    ./configure
    make
    sudo make install
    
    ```
3. **Fetched updated vendor list**:
    
    ```bash
    sudo get-oui
    
    ```
    
    This downloads a clean vendor database to:
    
    ```
    /usr/local/share/arp-scan/ieee-oui.txt
    
    ```

---

### ✅ Example Output

#### `network-scan.sh` (ping + hostname):

```powershell
✅ 192.168.1.1 (router.local)
✅ 192.168.1.101 (pi.hole)
✅ 192.168.1.185 (octopi)
```

#### `arp-scan`:

```powershell
192.168.1.1     c8:7f:54:b5:b1:a8     Cisco Systems
192.168.1.99    b8:27:eb:79:c1:de     Raspberry Pi Foundation
```

---

### Next Steps

- Auto-detect between `wlan0` and `eth0` interfaces
- Log outputs to `~/Logs/`
- Create a simple curses-style dashboard for top tools
- Add dynamic vendor filtering (e.g. filter by manufacturer name)

# Running ntopng from Source on Raspberry Pi 5 (ARM64)

#### **Date:** June 4th, 2025  
**Category:** Network/Security  
**Backlink:** [LibreNMS Docker Deployment on Raspberry Pi 5](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/librenms-docker-deployment-on-raspberry-pi-5)

---

#### **Overview**

This guide details how I installed and configured the open-source network traffic monitor **ntopng** on my **Raspberry Pi 5 (ARM64)**. The goal was to gain full LAN visibility using packet inspection via the Pi’s wireless interface. LibreNMS is already in place for SNMP-based metrics, and ntopng complements it by showing real-time traffic flows and bandwidth usage.

---

#### **Why Build from Source?**

The official Docker images for `ntop/ntopng` were built for `amd64`, which is incompatible with the Pi 5's `arm64` architecture. Since no prebuilt stable ARM image was available, I opted to build ntopng from source.

---

#### **Install Prerequisites**

```bash
sudo apt update && sudo apt upgrade -y
sudo apt install -y \
  build-essential cmake libtool autoconf automake pkg-config \
  libzmq3-dev libsqlite3-dev libhiredis-dev libmaxminddb-dev \
  libpcap-dev libcurl4-openssl-dev libssl-dev libnghttp2-dev \
  libmariadb-dev-compat libmariadb-dev libnats-dev libcap-dev \
  redis git
```

---

#### **Clone and Build nDPI**

```bash
cd ~
git clone https://github.com/ntop/nDPI.git
cd nDPI
./autogen.sh
make
```

---

#### **Clone and Build ntopng**

```bash
cd ~
git clone https://github.com/ntop/ntopng.git
cd ntopng
./configure
make
sudo make install
```

---

#### **Create Systemd Service File**

```ini
# /etc/systemd/system/ntopng.service

[Unit]
Description=NtopNG Community Edition (custom build)
After=network.target

[Service]
ExecStart=/usr/local/bin/ntopng --dont-change-user --interface=wlan0 --http-port=3000
WorkingDirectory=/var/lib/ntopng
User=root
Restart=on-failure

[Install]
WantedBy=multi-user.target
```

---

#### **Enable and Start Service**

```bash
sudo mkdir -p /var/lib/ntopng
sudo systemctl daemon-reload
sudo systemctl enable ntopng
sudo systemctl start ntopng
```

---

#### **Web Access**

- Open your browser to:  
    `http://<Pi5-IP>:3000`  
    In my case: `http://192.168.1.174:3000`

If the page doesn’t load, check:

```bash
sudo systemctl status ntopng
sudo journalctl -u ntopng --no-pager

```

---

[![2025-06-04 16_40_18-ntopng - Live — Mozilla Firefox.png](https://docs.natenetworks.com/uploads/images/gallery/2025-06/scaled-1680-/NnoIDUjBZ08Ow3LH-2025-06-04-16-40-18-ntopng-live-mozilla-firefox.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-06/NnoIDUjBZ08Ow3LH-2025-06-04-16-40-18-ntopng-live-mozilla-firefox.png)

[![2025-06-04 16_40_05-ntopng - Traffic — Mozilla Firefox.png](https://docs.natenetworks.com/uploads/images/gallery/2025-06/scaled-1680-/mO6qF1VvLNFiADZj-2025-06-04-16-40-05-ntopng-traffic-mozilla-firefox.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-06/mO6qF1VvLNFiADZj-2025-06-04-16-40-05-ntopng-traffic-mozilla-firefox.png)

# Pi 5 System Summary Script with AWK

#### **Date:** May 25th, 2025  
**Category:** Linux Scripting / System Monitoring

#### I wrote this script after studying `awk`. This command is really powerful in scripting. I have a good understanding of using the command to extract data fields from CSV files, and live system data. Here is my project I made using `awk`.

### Script Overview

- Gathers:
    
    
    - Timestamp
    - IP Address
    - Hostname
    - Uptime (human-readable)
    - Disk usage, total, free
    - Memory usage, total, free
- Outputs to both screen and log file
- Uses `awk`, `df`, `free`, `uptime`, and `tee`

---

### Script: `<strong>pi5-system-summary.sh</strong>`

```bash
#!/bin/bash

LOGFILE=~/pi5-summary-$(date +%Y-%m-%d).log
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
HOSTNAME=$(hostname)
IP=$(hostname -I | awk '{ print $1 }')
UPTIME=$(uptime -p)
DISK_TOTAL=$(df -h / | awk 'NR > 1 { print $2 }')
DISK_USAGE=$(df -h / | awk 'NR > 1 { print $5 }' | tr -d '%')
DISK_FREE=$(df -h / | awk 'NR > 1 { print $4 }')
MEM_TOTAL=$(free -h | awk 'NR == 2 { print $2 }')
MEM_USEAGE=$(free -h | awk 'NR == 2 { print $3 }')
MEM_FREE=$(free -h | awk 'NR == 2 { print $4 }')

{
echo "===== Pi 5 System Summary ====="
echo "$TIMESTAMP"
echo "IP ADDRESS: $IP"
echo "HOSTNAME: $HOSTNAME"
echo "UPTIME: $UPTIME"
echo "DISK TOTAL: $DISK_TOTAL"
echo "DISK USAGE: $DISK_USAGE%"
echo "DISK FREE: $DISK_FREE"
echo "MEM TOTAL: $MEM_TOTAL"
echo "MEM USAGE: $MEM_USEAGE"
echo "MEM FREE: $MEM_FREE"
} | tee -a "$LOGFILE"

```

---

### Sample Output

[![2025-05-26 07_46_07-PI-DT-01 (WayVNC) - RealVNC Viewer.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/osmXsYV8BHu8VC8M-2025-05-26-07-46-07-pi-dt-01-wayvnc-realvnc-viewer.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/osmXsYV8BHu8VC8M-2025-05-26-07-46-07-pi-dt-01-wayvnc-realvnc-viewer.png)

### Log Output with `tee` and wrapped in {}  
[![2025-05-26 07_45_49-PI-DT-01 (WayVNC) - RealVNC Viewer.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/OF2XjzmapDwyPZL7-2025-05-26-07-45-49-pi-dt-01-wayvnc-realvnc-viewer.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/OF2XjzmapDwyPZL7-2025-05-26-07-45-49-pi-dt-01-wayvnc-realvnc-viewer.png)

# LibreNMS Docker Deployment on Raspberry Pi 5

**Date:** May 13, 2025  
**Category:** Monitoring / Raspberry Pi Projects  
**Backlink:** *You can add this to a “Pi5 Monitoring Projects” page or your “Homelab Projects” index*

---

## Project Overview

This guide walks through setting up **LibreNMS** on a **Raspberry Pi 5 (64-bit)** using **Docker Compose**, then configuring SNMP on the Pi to allow it to be monitored—including fixing the common **“No Processors”** graph error by enabling `extend` support in SNMP for `/proc/stat`.

---

## Prerequisites

- Raspberry Pi 5 running 64-bit Raspberry Pi OS (Bookworm or similar)
- Docker and Docker Compose installed
- Static IP (e.g. `192.168.1.174`)
- Basic familiarity with SSH and Linux commands

---

## Step 1: Docker Setup

Install Docker (skip if already installed):

```bash
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

```

---

## Step 2: Clean Up Old LibreNMS Docker Setup (if any)

```bash
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)
docker volume prune -f
rm -rf ~/librenms

```

---

## Step 3: Set Up LibreNMS Docker Files

Create a new folder:

```bash
mkdir ~/librenms && cd ~/librenms

```

Download example files from the official repo:

```bash
wget https://raw.githubusercontent.com/librenms/docker/master/examples/compose/compose.yml
wget https://raw.githubusercontent.com/librenms/docker/master/examples/compose/.env
wget https://raw.githubusercontent.com/librenms/docker/master/examples/compose/librenms.env
wget https://raw.githubusercontent.com/librenms/docker/master/examples/compose/msmtpd.env

```

---

## Step 4: Edit `.env` File

```bash
nano .env

```

Set your timezone and user/group IDs:

```env
TZ=America/Chicago
PUID=1000
PGID=1000

```

---

## Step 5: Start the Docker Stack

```bash
docker compose -f compose.yml up -d

```

Once all containers start, LibreNMS will be accessible at:  
📍 `http://<PI-IP>:8000` (e.g., `http://192.168.1.174:8000`)

---

## Step 6: Create LibreNMS User

Once web UI is accessible, create your first admin user through the setup wizard.

---

## Step 7: Install and Configure SNMP on the Pi

Install SNMP daemon:

```bash
sudo apt update
sudo apt install snmpd snmp -y

```

Edit `/etc/snmp/snmpd.conf`:

```bash
sudo nano /etc/snmp/snmpd.conf

```

Modify/add the following:

```conf
agentAddress udp:161
rocommunity public
sysLocation Sitting on a Dusty Shelf
sysContact Me <zippybytes@protonmail.com>
extend cpuinfo /bin/cat /proc/stat

```

Restart the SNMP service:

```bash
sudo systemctl restart snmpd

```

---

## Step 8: Verify SNMP Works

```bash
snmpwalk -v2c -c public localhost 1.3.6.1.2.1.1
snmpwalk -v2c -c public localhost system

```

Expected result includes system description, contact, location, and uptime.

---

## Step 9: Fix “No Processors” Graph Issue

If LibreNMS shows “Error Drawing Graph: No Processors”:

Ensure `extend cpuinfo /bin/cat /proc/stat` is set in `/etc/snmp/snmpd.conf`, then test it:

```bash
snmpwalk -v2c -c public localhost NET-SNMP-EXTEND-MIB::nsExtendOutputFull.\"cpuinfo\"

```

You should see actual `/proc/stat` data returned.

---

## Final Test

1. Go to **Devices** &gt; **Add Device** in LibreNMS.
2. Enter:
    
    
    - Hostname/IP: `192.168.1.174`
    - SNMP Version: `v2c`
    - Community: `public`
3. Click **Add Device**.

After polling completes (5–10 mins), graphs will populate correctly, including CPU usage.  
  
[![2025-05-13 20_25_15-192.168.1.174 Overview _ LibreNMS — Mozilla Firefox.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/tAFaDyC94hjdqSBg-2025-05-13-20-25-15-192-168-1-174-overview-librenms-mozilla-firefox.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/tAFaDyC94hjdqSBg-2025-05-13-20-25-15-192-168-1-174-overview-librenms-mozilla-firefox.png)

---

## 📚 Notes

- If SNMP errors appear during walk attempts, ensure MIBs are installed:
    
    ```bash
    sudo apt install snmp-mibs-downloader -y
    sudo sed -i 's/^mibs :/#mibs :/' /etc/snmp/snmp.conf
    ```
- Reload or restart SNMP again:
    
    ```bash
    sudo systemctl restart snmpd
    ```

---

## 📦 Services Started

- `librenms` (Main Web Interface)
- `librenms_db` (MariaDB)
- `librenms_redis`
- `librenms_syslogng`
- `librenms_snmptrapd`
- `librenms_msmtpd`

---

## 🧭 Next Steps

- Enable polling other devices (routers, switches, VPS)
- Add disk, network, and service monitors
- Create alerts for offline devices
- Enable email notifications via `msmtpd.env`

# Sourced from raspberrytips.com

##### Raspberry Pi Glossary \\\\ [Raspberry-Pi-Glossary-526063852942.pdf](https://docs.natenetworks.com/attachments/7)

##### Raspberry Pi Commands \\\\ [Raspberry-Pi-74-Commands-4239584953.pdf](https://docs.natenetworks.com/attachments/5)

##### Raspberry Pi 24 Best Games \\\\ [Raspberry-Pi-24-Best-Games-492385539.pdf](https://docs.natenetworks.com/attachments/6)

##### Linux Command Cheat Sheet \\\\ [linux-commands-cheat-sheet-2234.pdf](https://docs.natenetworks.com/attachments/8)

##### Python Cheat Sheet \\\\ [Raspberry-Pi-Python-Cheat-Sheet-45325.pdf](https://docs.natenetworks.com/attachments/4)

# Full SNMP Monitoring on Raspberry Pi 5 for LibreNMS (UCD-SNMP + Extend Workaround)

#### **Date:** May 14, 2025  
**Category:** Monitoring / SNMP  
**Backlink:** [LibreNMS Docker Deployment on Raspberry Pi 5](https://docs.natenetworks.com/books/06-raspberry-pi-python-linux-tips/page/librenms-docker-deployment-on-raspberry-pi-5)

---

### 🧩 Overview

This update documents how I configured full-featured SNMP monitoring on a Raspberry Pi 5 running LibreNMS inside Docker. Since `hrProcessorLoad` and traditional Host Resources MIB features are often unavailable or broken on ARM-based systems, I used `UCD-SNMP` and `NET-SNMP-EXTEND-MIB` to monitor:

- ✅ CPU usage
- ✅ Memory usage (real, buffers, cache, swap)
- ✅ Disk space
- ✅ Temperature via custom script
- ✅ Graphs and health sensors in LibreNMS dashboard

---

## 🔧 Step-by-Step Setup

### 1. **Edit SNMP Configuration**

Update `/etc/snmp/snmpd.conf` with:

```ini
rocommunity public
disk / 10000

view   systemonly  included   .1.3.6.1.2.1.1
view   systemonly  included   .1.3.6.1.2.1.25
view   systemonly  included   .1.3.6.1.4.1.2021
view   systemonly  included   .1.3.6.1.4.1.8072.1

extend temp /bin/bash /usr/local/bin/snmp-temperature.sh

```

> The `disk` line enables `/` monitoring. The `extend` line provides temperature monitoring.

---

### 2. **Create Extend Script for CPU Temp**

```bash
sudo nano /usr/local/bin/snmp-temperature.sh

```

Paste:

```bash
#!/bin/bash
vcgencmd measure_temp | sed "s/temp=//;s/'C//"

```

Make it executable:

```bash
sudo chmod +x /usr/local/bin/snmp-temperature.sh

```

---

### 3. **Give SNMP Access to Pi Temperature Sensor**

```bash
sudo usermod -aG video Debian-snmp
sudo reboot

```

---

### 4. **Verify SNMP Outputs**

```bash
snmpwalk -v2c -c public localhost .1.3.6.1.4.1.2021.4     # Memory
snmpwalk -v2c -c public localhost .1.3.6.1.4.1.2021.9     # Disk
snmpwalk -v2c -c public localhost hrProcessorLoad        # CPU per core
snmpwalk -v2c -c public localhost .1.3.6.1.4.1.2021.10    # Load avg
snmpwalk -v2c -c public localhost NET-SNMP-EXTEND-MIB::nsExtendOutput1Line.\"temp\"   # Temperature

```

---

### 5. **Ensure Pi is Added in LibreNMS**

In the web UI:

- Add device: `192.168.1.174` (not `localhost`)
- SNMP v2c, community `public`
- Confirm SNMP test passes

---

## 📦 Inside the Docker Container

### Enter container:

```bash
docker exec -it librenms bash
cd /opt/librenms

```

### Run poller and rediscovery:

```bash
php artisan config:clear
./lnms poller:discovery 1
./lnms device:poll 1

```

---

## 📊 Final Results in LibreNMS

From the Pi's page:

- **Graphs → CPU, Memory, Storage, Temperature** are all active
- **Health tab** shows temperature sensor: `temp1`
- **Storage tab** shows `/` and `/boot/firmware`
- **Dashboard Device Graphs widget** now shows mini graphs for each metric

---

## 🩺 Troubleshooting Addendum

### ❌ Memory Graphs Not Appearing?

Make sure:

- SNMP returns `.1.3.6.1.4.1.2021.4` correctly
- `ucd-mib`, `mempools`, and `storage` modules are enabled in UI
- You’ve updated the device hostname to use the Pi’s **IP**, not `localhost`
- Use:
    
    ```bash
    ./lnms poller:discovery 1
    ./lnms device:poll 1
    
    ```

---

## ✅ Wrap-Up

With this configuration, the Pi 5 running LibreNMS inside Docker is now monitoring itself via SNMP, including:

- UCD-based metrics
- Custom `extend` temperature sensor
- Full graph integration

This setup is now replicable for other ARM-based Linux systems with similar SNMP limitations!

---

# Changing the Hostname on Linux and Removing VPN Server.

#### I want to change the hostname on my pivpn because I have moved that to a different system and want to use this for something else but don't need to reinstall linux:

##### use hostname to see current hostname or it is after your profile name right there in the terminal:  


```bash
hostname
```

[![Pasted image 20250517145932.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/3EQfaJHeviMkFzFD-pasted-image-20250517145932.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/3EQfaJHeviMkFzFD-pasted-image-20250517145932.png)

##### We can edit the hostname file with this command:  


```bash
sudo nano /etc/hostname
```

##### Output:

[![Pasted image 20250517150053.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/AijIO8kW6PvCwYoi-pasted-image-20250517150053.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/AijIO8kW6PvCwYoi-pasted-image-20250517150053.png)

##### I'm going to change the name to PI-DT-02 because I already have another pi I named PI-DT-01.

##### [![Pasted image 20250517150142.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/v7nWNAYfR5QBFCsa-pasted-image-20250517150142.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/v7nWNAYfR5QBFCsa-pasted-image-20250517150142.png)  
  
CTRL+S to save and CTRL+X to exit.

##### Now we need to edit the hosts file to update the name to match the new one with this command:  


```bash
sudo nano /etc/hosts
```

##### I'm going to change the box highlighted in green to my new hostname:

##### [![Pasted image 20250517150355.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/JhcAoWM0tFs36kGW-pasted-image-20250517150355.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/JhcAoWM0tFs36kGW-pasted-image-20250517150355.png)  
  
Save and exit that file.

##### To apply those changes we will have to reboot.  


```bash
sudo reboot
```

##### That disconnects the SSH session and I'll wait for it to come up.

##### If you want to know when it is reachable you can use the powershell command:  


```powershell
ping 192.168.1.16 -t
```

##### Be sure to replace this with the address of your pi.

##### It will ping that IP address until you cancel it with CTRL+C

##### [![Pasted image 20250517150842.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/PDalQ9Xw4LhCZdwu-pasted-image-20250517150842.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/PDalQ9Xw4LhCZdwu-pasted-image-20250517150842.png)  
  
We see the IP reply back and we know that it is back online.

##### Lets take it a step further and see if we can ping the new hostname.

```powershell
ping PI-DT-02
```

[![Pasted image 20250517151000.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/SM6oYZPc2Nj9rsLZ-pasted-image-20250517151000.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/SM6oYZPc2Nj9rsLZ-pasted-image-20250517151000.png)

##### We see that worked as well.

##### Lets ssh back in and see it confirmed:

##### [![Pasted image 20250517151234.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/Ax5Uindef6p1qKcR-pasted-image-20250517151234.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/Ax5Uindef6p1qKcR-pasted-image-20250517151234.png)  
  
Good to Go!

##### The rest is specific to the old setup of my pivpn. I was using wireguard so lets remove these packages:

```bash
sudo pivpn uninstall
```

##### [![Pasted image 20250517151906.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/a8sNXC1EbBmZTgPE-pasted-image-20250517151906.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/a8sNXC1EbBmZTgPE-pasted-image-20250517151906.png)  
  
I'm removing the full stack so I'm using y for the answer to all the questions.  
I'll go ahead and reboot now.

##### Lets strip the pi down to a Clean Base:  


```bash
sudo rm -rf /etc/pivpn
```

##### Removes leftovers. 

##### Final Touch:  
If you want a clean slate without re-imaging:  


```bash
sudo apt install deborphan
sudo deborphan | xargs sudo apt-get -y remove --purge

```

##### This removes orphaned libraries left behind by uninstalled apps.

##### Notice it didn't remove some of my home directory contents which I wish to keep:

[![Pasted image 20250517152555.png](https://docs.natenetworks.com/uploads/images/gallery/2025-05/scaled-1680-/zsKr6ttugfiOSqLN-pasted-image-20250517152555.png)](https://docs.natenetworks.com/uploads/images/gallery/2025-05/zsKr6ttugfiOSqLN-pasted-image-20250517152555.png)