Bus Tracker is a FastAPI and vanilla-JavaScript fleet-management application for school transport operations. It includes authenticated management, route/bus/driver assignments, student transport assignments, and live trip tracking.
- Backend: FastAPI (Python)
- Database: SQLite for development and PostgreSQL for production
- ORM: SQLAlchemy
- Authentication: JWT with Passlib and python-jose
- Frontend: HTML5, CSS3, and Vanilla JavaScript ES Modules
- Maps and routing: Leaflet.js, OpenStreetMap, OSRM, and Nominatim
- Bus-pass scanning: Short-lived Ed25519 QR credentials with a vendored browser scanner
- Create and activate a Python virtual environment.
- Install dependencies with
pip install -r requirements.txt. - For computer-only use, start the application with
python -m uvicorn backend.main:app --reloadfrom this project directory. - Open
http://127.0.0.1:8000/for the login shell orhttp://127.0.0.1:8000/dashboardfor the SPA host.
Connect the computer and phone to the same trusted Wi-Fi network. In PowerShell, from this project directory, run:
.\start-lan.ps1The helper starts FastAPI on every network interface and prints an address such
as http://192.168.1.25:8000/. Open that exact address in the phone browser.
Do not use localhost or 127.0.0.1 on the phone: those addresses point to
the phone itself.
If the phone times out, allow the app through Windows Firewall on Private networks (or add an inbound TCP rule for port 8000), then restart the helper. Avoid exposing this development server on public Wi-Fi; it has no HTTPS and is intended only for your private local network.
Before signing in for the first time, create an administrator without placing a password in source code or shell history:
python database/init_database.py --username your-admin-nameThe setup command securely prompts for a 12–72 character password and stores only its bcrypt hash with a unique salt. The login page receives a short-lived JWT in an HttpOnly, SameSite session cookie. The browser does not persist the JSON bearer token; that response field remains available only for non-browser API compatibility. The dashboard verifies the server session before it loads, and logout revokes the session and clears the browser cookie.
To rotate an existing administrator password (including an older development account), run:
python database/init_database.py --username admin --reset-passwordIf the development SQLite database is deleted, the server will recreate its tables and print the recovery command instead of inventing a known password. Use admin as the username or choose another username, then enter a new password at the secure prompt. For a production deployment, set BOOTSTRAP_ADMIN_PASSWORD in the server's secret store before the first start; the application will create the configured administrator once and will not print the password.
For production, set APP_ENV=production, DATABASE_URL to PostgreSQL, and a random JWT_SECRET_KEY of at least 32 characters. Configure BOOTSTRAP_ADMIN_PASSWORD only through the hosting provider's secret store if a first administrator must be created at startup. See .env.example; never commit a real secret or use a default password. For an existing SQLite installation, follow the verified SQLite-to-PostgreSQL cutover rather than manually copying database files.
The included render.yaml provisions PostgreSQL and has Render generate a persistent 256-bit JWT_SECRET_KEY for a new Blueprint deployment. For an existing Render service, set JWT_SECRET_KEY manually in Environment to a random value of at least 32 characters, then redeploy; Render does not overwrite an existing empty secret when a Blueprint is updated. Before the first successful deployment, also set BOOTSTRAP_ADMIN_PASSWORD to a unique 12–72 character password in the same Environment page. The first administrator uses username admin unless BOOTSTRAP_ADMIN_USERNAME is set. Keep both values in Render's secret store and never add them to the repository.
If the first administrator password is lost, set BOOTSTRAP_ADMIN_RESET_PASSWORD=true temporarily in Render together with the intended BOOTSTRAP_ADMIN_USERNAME and a new BOOTSTRAP_ADMIN_PASSWORD, then deploy once. This resets only that existing active administrator's password, ends its existing sessions, and clears login lockout. Remove the reset variable or set it to false immediately after the successful deployment so future deployments cannot overwrite a password changed in Settings.
The repository includes a Cloud Build image definition, guarded Cloud Run
deployment script, Cloud SQL backup/PITR script, and read-only post-deployment
verifier. Start with the GCP deployment runbook and the
readiness report. Production startup now rejects
SQLite and refuses RESET_DATABASE=true; /health checks the process and
/ready checks database availability.
- Management and identity APIs require server-side JWT authentication and role checks.
- Browser sessions use an HttpOnly, SameSite cookie with a dedicated logout endpoint; bearer headers remain supported for non-browser API clients.
- Driver trip actions are restricted to the authenticated driver's own profile and trip.
- Student APIs are restricted to the authenticated student or management staff.
- Request-size limits, per-client API/login/upload rate limits, security headers, HSTS in production, and an enforced browser Content Security Policy are applied by
backend/security.py. - Administrative deletion is limited to authenticated administrators. There is intentionally no remote database-wipe endpoint; account removal/revocation is auditable and scoped to an individual account.
- Driver safety feedback is stored in the notifications table and is visible to management with acknowledge, resolve, reopen, and refresh workflows.
- Production startup fails closed if the JWT signing secret is missing or too short. Use a reverse proxy/WAF for distributed rate limiting, TLS termination, backups, and intrusion monitoring.
BusTrack accepts GPS-company webhooks without changing the existing driver-mobile GPS contract. An administrator first maps each external provider device ID to a bus, then creates either one service token for the whole fleet or a bus-scoped token. Tokens never expire automatically and plaintext values are returned only once to the authorised API caller when they are created or rotated. They are never returned by the token-list API or persisted in readable form.
-
POST /api/integrations/gps/deviceswith an admin session:{"bus_id": 12, "external_device_id": "862567072404952", "display_name": "KL07BN4647"} -
POST /api/integrations/gps/tokenswith{"label":"GPS company"}. Store the returnedtokenwith the provider. Supplying abus_idmakes the token valid for that bus only. -
Ask the provider to
POSTits native JSON tohttps://YOUR-DOMAIN/api/integrations/gps/ingest, withX-GPS-Token: <returned token>andContent-Type: application/json. A bare object or an array of position objects is accepted. The custom key is accepted only in this header on this webhook; it is not a browser login, Bearer token, or credential for any read or management API.
The supplied Teltonika payload maps uniqueId, deviceId, name, or phone
to the configured device mapping. The latest internal position is visible to
administrators at /api/integrations/gps/status; its complete vendor payload is
available per bus at /api/integrations/gps/status/{bus_id}. The receiver
expects the provider's 20-second ignition-on updates and two-minute ignition-off
heartbeats. Fresh vehicle GPS and driver-phone GPS are both recorded for an
active trip, so either source can keep the map and route progression current.
Administrators can create a Technician account from Users. Technician accounts have a separate GPS Integration portal; they can rotate or disable provider credentials, edit external device-to-bus mappings, and update the allowed JSON field paths when a provider changes its payload layout. They do not receive general fleet, account, or assignment administration permissions. The separate Provider Health module shows provider contact time and device fix time independently, reports delayed/offline/error states per bus, and provides an exact-bus filter over the rolling raw coordinate feed.
See docs/mvd-gps-provider-handoff.md for
the exact handoff contract to send to the GPS provider, including the accepted
payload forms, expected response codes, and the final live-delivery check.
For Airotrack's vehicle-live-data API, configure AIROTRACK_API_TOKEN only
in the server/deployment secret store (or a local uncommitted .env file) and
restart the API. BusTrack then polls every bus's database registration
number every 20 seconds (set AIROTRACK_POLL_INTERVAL_SECONDS to change
this; the minimum is 20 seconds). Add all 24 buses normally in Bus Management;
their registration number is the only per-bus Airotrack lookup value. The API
returns the tracker IMEI and BusTrack stores it with the received position.
The translated positions feed the existing Admin and User live-tracking APIs
automatically. A GPS technician can run an immediate authorised refresh with
POST /api/integrations/gps/airotrack/refresh.
The vendor's source_date, not the time BusTrack polled it, determines whether
a location is fresh. An old last-known point remains visible to management but
is never presented to users as current vehicle movement. If a sleeping service
wakes to an advancing queue of older Airotrack heartbeats, BusTrack performs a
bounded catch-up read and applies only the greatest device timestamp. Set
AIROTRACK_CATCHUP_MAX_REQUESTS to tune the maximum (default 8). A production
cold start completes its first provider refresh before serving tracking pages.
Configure KINGSTRACK_ACCOUNTS in the server secret store as comma-separated
company_id:user_id pairs. BusTrack fetches each account once per polling
cycle because the Kingstrack endpoint returns its full fleet. It matches each
plate_no to the registration number saved in Bus Management and records the
returned IMEI automatically. No provider field or device ID needs to be typed
when adding the bus.
After its first matching response, a bus is persistently assigned to
Kingstrack. Airotrack then excludes that bus from its own lookups, so failures
or delayed data from one provider cannot replace the other provider's state.
Both adapters write the canonical GPS state and use the same route progression,
reset, skipped-stop and terminal-reversal safeguards. Provider Health labels
each bus and raw response as Airotrack or Kingstrack and supports exact provider
filtering. GPS_PROVIDER_POLL_INTERVAL_SECONDS controls the shared schedule;
the minimum remains 20 seconds.
A suspended free web service cannot run outbound polling code while suspended. For uninterrupted collection during that period, configure the provider to push heartbeats to the webhook endpoint or run the service on an always-on instance. The cold-start catch-up prevents the saved pre-sleep coordinate from winning once the service is awake again.
backend/contains the FastAPI application, SQLAlchemy models, authenticated API routes, GPS adapters, security middleware, and background services.frontend/contains the role-aware SPA, common layout/client utilities, maps, tracking views, management forms, and bus-pass scanner.database/holds the ignored development SQLite database and the administrator/bootstrap utilities.tests/contains Python integration/regression tests and dependency-free Node tests for browser modules.docs/contains deployment, GPS handoff, bus-document, bus-pass, and database-cutover guidance.requirements.txtlists backend dependencies.
frontend/common/router.js is the single hash-routing seam. After the server verifies the session, it selects the allowed Admin, Driver, User, or Technician module map and dynamically imports the requested ES module. Shared layout and security utilities live in frontend/common/; feature behavior and styles live in frontend/modules/.
The Python regression suite uses the standard-library test runner, so no
separate pytest installation is required after installing requirements.txt:
.\.venv\Scripts\python.exe -m unittest discover -s tests -p "test_*.py"The browser-module tests run directly with Node.js:
Get-ChildItem tests\*.mjs | ForEach-Object { node $_.FullName }