Skip to content

Operational Map implementation walkthrough

This page explains how the operational map works and the reasoning a reviewer should assess. Release findings and verification results are recorded separately in the repository-root OPERATIONAL_MAP_REVIEW.md.

Purpose and boundaries

The map is a view of Mission Plan deployment coordinates. It overlays those positions on locally uploaded raster MBTiles archives. It does not track a device through GPS, poll for position updates, or deploy configuration. Reloading the page refreshes its mission snapshot.

“Offline” means Nautobot and its local storage remain reachable while Internet access is unavailable. It does not mean a browser can operate after disconnecting from Nautobot. Leaflet assets and basemap tiles are served locally. There is no external tile provider fallback in the map script.

Request flow

flowchart TD
    Upload[Atlas upload through UI or REST] --> Validate[Model validates MBTiles metadata]
    Validate --> Store[Private archive storage and MapTileSet metadata]
    Browser[Authenticated browser] --> Page[OperationalMapView]
    Page --> Permissions[Restrict missions, operations, and basemaps]
    Permissions --> JSON[Escaped JSON configuration in template]
    JSON --> Leaflet[Local Leaflet renderer]
    Leaflet --> Tiles[Permission-checked XYZ tile endpoint]
    Tiles --> Cache[Versioned tile-byte cache]
    Cache -->|Miss| SQLite[Read archive using TMS row coordinates]
    Browser --> Download[Object-permission-checked archive download]
    Download --> Store

Archives have no generic media URL. UI downloads use the atlas view permission; REST downloads use the viewset's restricted object lookup and support API authentication. Both return attachments with a fixed binary content type.

Files and responsibilities

File Responsibility
models/map.py Defines MapTileSet, derived metadata, and upload validation/save behavior
map_storage.py Private filesystem storage, authorized file URLs, safe attachments, and commit-aware cleanup
utils.py Reads archive metadata and retrieves one tile using SQLite
forms.py Atlas UI forms and permission-restricted operation selection
api/serializers.py, api/views.py, api/urls.py Standard Nautobot REST management of atlas records/uploads
views.py Atlas UI, filtered mission data, basemap configuration, tile authorization and caching
urls.py, navigation.py Registers routes and permission-aware navigation links
templates/nautobot_trax/operational_map.html Displays filters, markers, labels, basemap selection, and zoom behavior
static/nautobot_trax/leaflet/ Locally served Leaflet 1.9.4 assets
migrations/0009_*, 0010_*, 0011_* Creates the atlas model, records available zooms, and migrates legacy public archives to private storage
tests/test_operational_map.py Metadata, uploads, permissions, filters, and cache regressions
tests/test_map_security.py Download authorization, hostile input limits, private storage, migration, cleanup, and demo-role regressions
management/commands/cleanup_map_archives.py Age-guarded preview/removal of unreferenced private archives
tools/check_operational_map_browser.py Exercises the actual map script in local headless Chrome
tools/build_mbtiles_overviews.py Optional offline helper that creates overview imagery in a separate archive

Model and upload handling

MapTileSet stores a unique name, description, uploaded file, and activation flag. The raster format, attribution, zoom limits/list, and geographic bounds are extracted from the archive instead of accepting the browser's assertions.

clean() reads the file while it is still an upload. The helper copies the file to a temporary filesystem path because SQLite needs a path, and restores the upload stream's original position. A label/activation edit skips rescanning an unchanged committed archive. save() also validates a newly assigned upload so programmatic/API creation populates derived fields. Updating only the file extends update_fields to persist the derived fields and timestamp as well.

The parser accepts the MBTiles table/view interfaces, raster PNG/JPEG/WebP format names, and common MIME aliases. It queries actual zoom levels instead of trusting potentially stale minzoom/maxzoom metadata. Missing or invalid bounds are derived from coverage at the most detailed zoom. Vector PBF archives are rejected because Leaflet's raster layer cannot render vector data.

New uploads require an .mbtiles extension and receive UUID-based storage names. PrivateMapStorage rejects roots inside media/static storage, writes files with mode 0600, and creates directories with mode 0700. Its generic url() raises an error; the custom field supplies the authorized atlas download route instead. An administrator must also keep the directory out of proxy/static routes and mount persistent storage in every service that accesses it.

The local workspace Dockerfiles build from the workspace parent. Their adjacent Dockerfile.dockerignore files allow only the source projects they copy and exclude environment files, uploaded media, atlas files, and the MOBAC workspace. A .dockerignore inside the TRAX directory alone does not protect a parent build context; see Docker's build-context rules.

GitHub Actions uses development/Dockerfile.ci with the TRAX checkout as its build context. It installs companion apps from the constrained Poetry lockfile, so CI does not require neighboring source checkouts. Its adjacent ignore file excludes local credentials, archives, and caches. CI tests archive processing on Python 3.12 and 3.13; Python 3.10 retains core coverage and verifies that archive processing fails closed when SQLite resource limits are unavailable.

Uploaded schemas can contain executable SQLite views. _mbtiles_connection() therefore opens a read-only URI, disables trusted-schema features and memory mapping, enables query-only mode, and installs SQLite resource limits and a progress-handler deadline. Python 3.11 or later is required for setlimit(); older runtimes fail closed when processing an archive. Validation has a 30-second default query budget, tile reads 2 seconds. The progress handler interrupts SQLite VM work; this is not a hard wall-clock deadline on filesystem I/O or a process-wide memory limit. Ordinary indexed tables and views remain supported within the limits.

Validation bounds the temporary copy to 5 GiB by default, limits metadata entries and values, checks every coordinate against its zoom grid even when bounds are provided, and limits tile BLOBs to 16 MiB. SQL parameters bind requested coordinates. These checks establish structure, supported metadata, and bounded query work; they do not decode every tile to prove it is valid raster imagery. Web-server upload limits, filesystem quotas, and representative atlas timings remain deployment responsibilities.

Replacement/deletion schedules removal of unreferenced files with transaction.on_commit(). Rollback preserves the previous file; shared files remain while referenced. A new upload written before rollback can remain as a private orphan. cleanup_map_archives previews such files; --apply removes only unreferenced regular files older than the configured age guard.

Migration 0011 moves the entire legacy trax_map_tiles namespace, including old orphaned archives, out of media storage. It verifies SHA-256 copies before removing originals and preserves names so interrupted migrations can be retried. Missing referenced files or conflicting copies stop the upgrade. Run the migration with services stopped and both original media and private storage mounted. Its reverse intentionally does not restore public files. See the administrator instructions for configuration and recovery prerequisites.

Mission selection and permissions

OperationalMapView uses Nautobot's ObjectPermissionRequiredMixin. Its dispatch checks mission-view permission and replaces the view queryset with an object-restricted queryset. get_context_data() then excludes expired missions and records without both coordinates, and joins tenant/location to avoid a separate query for each marker.

The operation selector independently restricts its queryset to operations the user may view. Multiple selections use OR semantics and .distinct() to avoid duplicating a mission shared by two operations. Invalid or inaccessible selected operations produce form errors and an empty mission result. Clearing the filter includes all otherwise permitted plotted missions, including unassigned ones.

Active basemaps use their own restricted queryset. Permission to view a mission does not grant permission to view a basemap, and operation selection does not expand mission permissions. Navigation permissions hide links, while server checks remain the enforcement boundary.

Tenant/location names are included as part of an authorized mission's displayed summary. A deployment needing independent redaction of related-object names should explicitly define and test that policy.

Browser rendering and injection prevention

Django's json_script serializes the two configuration objects into inert JSON script elements. The renderer reads their text content rather than evaluating JavaScript generated from user data. Popup fields, permanent mission labels, layer names, and attribution pass through escapeHtml() before Leaflet can interpret their HTML strings.

Circle markers use a shared canvas renderer to reduce individual SVG element cost. Permanent labels still create DOM elements; the implementation is not a clustered or paginated view for arbitrarily large mission inventories. Add performance tests with representative mission counts before promising scale.

The marker colors correspond to active/planned mission status. Expiration here means the stored expired status; a past end date alone does not remove a marker. Coordinates are already normalized/stored on MissionPlan; the browser does not implement a second MGRS conversion algorithm.

Offline zoom and viewport behavior

The first permitted active atlas is selected initially. The viewport prioritizes missions inside its coverage, then atlas coverage, then a default view. Missions outside coverage remain in the data without pulling the initial view away from available imagery. Switching a basemap updates both zoom limits and viewport.

The tile layer uses actual zoom_levels. Its _clampZoom override chooses the closest available coarser native level when an intermediate level is absent. This avoids requesting missing zooms or synthesizing a broad overview from a large number of finer tiles. One zoom level finer can require four times as many tiles for the same area; four levels finer can require 256 times as many.

Two additional display zoom levels stretch existing imagery to separate nearby markers. They add no cartographic detail. Zooming out below the native minimum requires real overview tiles. The optional overview builder copies an archive, reduces each group of four children into a parent, respects TMS north/south orientation, and never overwrites the source or an existing destination.

The _clampZoom hook is a private Leaflet interface. Pinning/vendoring 1.9.4 and running the browser scenarios are part of maintaining that decision. A Leaflet upgrade requires rechecking this behavior. Browser scenarios substitute a local image for tile requests, so they test rendering/request counts rather than the HTTP authentication or binary image-serving path.

Tile endpoint and cache

MapTileView applies the atlas permission mixin before loading an object, cache entry, or conditional response. It rejects invalid XYZ values before computing large powers, then rejects unavailable zoom levels. The SQLite query uses bound parameters for coordinates. MBTiles uses bottom-origin TMS rows, so a request with top-origin XYZ row y is translated to 2**z - 1 - y.

The server caches tile bytes, or a missing-tile sentinel, for one day. Cache keys include atlas ID, last-updated timestamp, and tile coordinates. Tile URLs also include a version parameter, so a successful file replacement gets fresh URLs. A missing tile returns 404. Successful responses include an ETag and private, max-age=0, must-revalidate; conditional requests can return 304 only after object permission checks. Thus new requests after permission revocation are denied even for a cached tile. Already downloaded copies cannot be recalled. Archive downloads use private, no-store, attachment disposition, a generated .mbtiles download name, application/octet-stream, and nosniff.

Missing/corrupt storage and expected SQLite errors produce a controlled 404 and a server warning identifying the atlas. Unexpected programming errors are not silently swallowed. Large atlases still need a cache capacity policy; permissions and TTL alone do not bound Redis memory consumption.

Directly changing an archive on disk bypasses timestamp-based invalidation. Use supported model/form/API updates. File lookup currently requires local filesystem storage because the view accesses FileField.path; remote object storage requires a separate retrieval/local-cache design.

Review questions and expected evidence

Question Evidence to request
Are mission and operation constraints independent? Restricted-user and inaccessible-filter tests
Can an atlas be read through a different URL? Direct raw-media/download authorization regression tests
Can an uploaded filename become active content? Reject/safe-download tests for HTML-named SQLite files
What stops an expensive schema or huge BLOB? SQLite budget/limit tests and representative large-atlas timings
Do file changes update metadata and caches? UI/API replacement tests and new-version ETag checks
What happens after storage loss or deletion? Expected error response, cleanup/retention tests, restore procedure
Can zoom changes multiply requests unexpectedly? Browser scenarios for sparse, zero-only, and detail-only atlases
Is the installed package self-contained? Fresh-wheel install, migrations, collectstatic, and license/content inspection
Is the release safe outside the demo? Explicit opt-in for demo account creation and documented role provisioning

Claims to keep precise

The design supports local raster basemaps, stored mission positions, and server-side object-restricted map, tile, and archive-download views. Query limits reduce hostile-schema exposure; they do not establish complete sandboxing of SQLite or a hard process memory budget. A passing map suite does not establish live position tracking, recall of downloaded imagery, large-inventory performance, or compatibility with every version/backend in broad dependency ranges.

The fixed-password operator/operator login is restricted to explicit demo provisioning. enable_demo_user defaults to False; the supplied development configuration enables it. setup_operator_user refreshes the demo operator's existing mission/operation access and MapTileSet view/add/change/delete permissions without resetting an existing password unless --reset is supplied.