Requirements

Every project requirement is a sphinx-needs req object with a stable ID. Implementing code references these IDs from its docstrings with the :need: role; Traceability links each one to its verifying test(s). IDs are stable — do not renumber them.

No requirement text contains a real account value: any number below is a synthetic example.

Safety & privacy

Requirement: Package imports only the standard library REQ_ZERO_DEPS
tags: safety, packaging
is verified by: TEST_ZERO_DEPS

The installed package imports only the Python standard library; it has zero runtime dependencies. (Design spec §4.)

Requirement: Read-only by construction REQ_READONLY
tags: safety
is verified by: TEST_READONLY

Against the trackiwi API, the only state-changing request the client may issue is DELETE /api/v2/session (logout). No call may alter trackers, alarms, tours, markers or shares. (Design spec §7.5; rescoped by influx spec §3.2 — writes to the user’s own InfluxDB are bounded separately by REQ_INFLUX_WRITE_SCOPE.)

Requirement: No private data in the repository REQ_NO_PRIVATE_DATA
tags: privacy
is verified by: TEST_NO_PRIVATE_DATA

No private data — position cache, exported tracks, credentials — is ever committed; a pre-commit guard blocks it. (Design spec §7.2.)

Requirement: The token is never logged or printed REQ_TOKEN_NEVER_LOGGED
tags: privacy, security
is verified by: TEST_TOKEN_NEVER_LOGGED

The session token is never logged or printed. Server-controlled text that can reach output (an error body a proxy may echo a header into) is redacted first. (Design spec §7.3.)

Authentication & transport

Requirement: The API base comes from the login response REQ_API_BASE_FROM_LOGIN
tags: auth
is verified by: TEST_API_BASE_FROM_LOGIN

The API base is taken from the login response’s server field, never hardcoded. (Design spec §3.1, §3.2.)

Requirement: The API base must be https REQ_API_BASE_HTTPS
tags: auth, security
is verified by: TEST_API_BASE_HTTPS

The API base must start with https://, wherever it came from (login response, --api-base, or the config file); an http:// base would send the bearer token in cleartext. (Design spec §7.3.)

Requirement: Redirects are never followed REQ_NO_REDIRECTS
tags: auth, security
is verified by: TEST_NO_REDIRECTS

No request follows an HTTP redirect. urllib copies the Authorization header onto a redirected request, to any host and any scheme, so a 3xx is an error: never success, and never “no more data”.

Requirement: The server command header is ignored REQ_IGNORE_APP_COMMAND
tags: safety, security
is verified by: TEST_IGNORE_APP_COMMAND

The trackiwi-app-command response header is ignored entirely; the client never executes a server-delivered command. (Design spec §3.5.)

Sync

Requirement: Sync is resumable REQ_SYNC_RESUME
tags: sync
is verified by: TEST_SYNC_RESUME

Sync is offset-based and resumable: a failure never loses fetched rows and never advances the stored offset past an unstored row, so re-running continues from the highest id already stored (there is no retry loop). (Design spec §8.)

Requirement: The sync offset is exclusive REQ_SYNC_OFFSET_EXCLUSIVE
tags: sync

The sync offset is exclusive: a page requested at a given offset returns rows strictly greater than it, with no overlap between consecutive pages, so the loop advances by the highest id seen. Confirmed against a live account. (Design spec §3.6 item 7, §10.)

Requirement: Sync fails loudly on a stuck page REQ_SYNC_FAIL_LOUD
tags: sync
is verified by: TEST_SYNC_FAIL_LOUD

A page on which no line has a readable id, or a page whose highest id does not exceed the requested offset, raises rather than silently ending or looping forever. (Design spec §8.)

Requirement: Sync moves past malformed rows REQ_SYNC_PAST_MALFORMED
tags: sync, parsing
is verified by: TEST_SYNC_PAST_MALFORMED

The next offset is the highest id on the page, counting skipped rows whose first field is an integer. A malformed row at the newest end of the data, or a page of nothing but malformed rows, is skipped, counted and passed; it never makes sync fail. (Issue #18.)

Parsing

Requirement: A malformed row is skipped and counted REQ_MALFORMED_SKIP
tags: parsing
is verified by: TEST_MALFORMED_SKIP

A malformed field means skip-and-count the row — never crash, never store it. Non-finite coordinates and out-of-range timestamps count as malformed. (Design spec §8.)

Requirement: fix_at is normalised to epoch seconds REQ_FIX_AT_SECONDS
tags: parsing
is verified by: TEST_FIX_AT_SECONDS

fix_at is normalised to epoch seconds: the sync CSV’s integer form (seconds or milliseconds) and the tracker endpoint’s ISO 8601 string form both resolve to seconds; a zone-less ISO value is read as UTC. (Design spec §3.6 item 4.)

Local cache & config

Requirement: The token file is 0600 and self-heals REQ_CONFIG_MODE_0600
tags: security, storage
is verified by: TEST_CONFIG_MODE_0600

The credentials file and the InfluxDB target config (influx.toml) are kept at mode 0600, and a widened mode is narrowed back on load. (Design spec §7.3; influx spec §5.6.)

Requirement: The position cache is 0600 in a 0700 directory REQ_CACHE_MODE_0600
tags: security, storage
is verified by: TEST_CACHE_MODE_0600

The position cache is created at mode 0600 inside a 0700 directory, and a widened mode self-heals on open. (Design spec §7.1.)

Requirement: A fix re-sent under a new id is not cached twice REQ_CACHE_NO_RESENT_FIX
tags: storage
is verified by: TEST_CACHE_NO_RESENT_FIX

A row with the same tracker, fix_at, latitude and longitude as a cached row is ignored, whatever its id: the server once re-sent a batch under new ids, which duplicated points in exports and overwrote InfluxDB points.

Requirement: Purge deletes the cache even when corrupt REQ_PURGE_DELETES
tags: storage, privacy
is verified by: TEST_PURGE_DELETES

purge deletes the cache and its SQLite sidecar files without opening the database, so it still works when the file is corrupt. (Design spec §6, §7.1.)

Requirement: A lock is never mistaken for corruption REQ_PURGE_NOT_ON_LOCK
tags: storage
is verified by: TEST_PURGE_NOT_ON_LOCK

A locked/busy cache is distinguished from a corrupt one; the destructive purge --yes advice is never given for a mere lock. (Design spec §7.1.)

Export

Requirement: Export writes atomically REQ_EXPORT_ATOMIC
tags: export
is verified by: TEST_EXPORT_ATOMIC

export -o writes atomically (temp file plus os.replace), refuses a symlink destination, keeps only the owner bits of an existing file’s mode, defaults a new file to 0600, and removes the temp file on any failure (Ctrl-C included). (Design spec §6.)

Requirement: A multi-tracker export keeps trackers separate REQ_EXPORT_PER_TRACKER
tags: export
is verified by: TEST_EXPORT_PER_TRACKER

An export covering several trackers keeps them apart: one GPX <trk> and one GeoJSON Feature per tracker, each carrying its tracker_id. (Design spec §6.)

Requirement: GeoJSON is RFC 7946-valid REQ_GEOJSON_VALID
tags: export
is verified by: TEST_GEOJSON_VALID

GeoJSON output is RFC 7946-valid: a Point for a single fix, a LineString for two or more, and never Infinity/NaN. (Design spec §6.)

Requirement: logout revokes server-side and reports honestly REQ_LOGOUT_REVOKES
tags: auth, security
is verified by: TEST_LOGOUT_REVOKES

logout revokes the session server-side (DELETE /api/v2/session) and, when it cannot, still removes the local credentials and warns that the token may remain valid. (Design spec §7.3.)

Requirement: A stored token is always removable REQ_TOKEN_REMOVABLE
tags: auth, security
is verified by: TEST_TOKEN_REMOVABLE

logout removes the local credentials whatever the config file holds: a file that cannot be loaded (truncated JSON, a non-object document, a non-string or non-https api_base) is deleted all the same, with a warning that the token may remain valid. (Design spec §7.3.)

Requirement: Positions are written to InfluxDB in natural units REQ_LINEPROTOCOL_UNITS
tags: influx, data
is verified by: TEST_LINEPROTOCOL_UNITS

Each cached row becomes one trackiwi_position line-protocol point with tags tracker_id and tracker_name and fields in natural units (distance in metres, voltage in volts). NULL optional columns are omitted, tag values are escaped, and non-finite values are rejected. (Influx spec §4.)

Heading

Requirement: A heading is estimated from cached positions, read-only REQ_HEADING_ESTIMATE
tags: heading, safety
is verified by: TEST_HEADING_ESTIMATE

The heading helper is pure arithmetic over cached positions: no request is made and the cache is never created by it. Moving (at or above min_speed, so GPS jitter at a standstill does not count): the heading is the fix’s course. Parked: the heading is the initial great-circle bearing between the last two moving fixes of the same tracker, in [0, 360) and correct across the antimeridian; only when that bearing is undefined (one moving fix, two at the same spot, or a pair too far apart in time or space) the last moving fix’s raw course is used. No moving fix at all yields unknown, never an error, and a NULL or non-finite cached field never raises. The caveats — a vehicle that reversed into its spot points the opposite way, and GPS alone cannot sense a stationary heading — are stated in the library docstring and the command’s --help. (Issue #3.)

Requirement: The heading carries a confidence state with a configurable threshold REQ_HEADING_STATE
tags: heading
is verified by: TEST_HEADING_STATE

Every estimate carries a state: moving, freshly_parked (last movement no more than stale_after seconds ago), stale (older) or unknown. stale_after defaults to the documented DEFAULT_STALE_AFTER (3600 s) and is a parameter of the library function and a flag (--stale-after) of the command. (Issue #3.)

Mirror

Requirement: The mirror never advances past an unacknowledged row REQ_MIRROR_RESUME
tags: integrity, influx
is verified by: TEST_MIRROR_RESUME

The per-target mirror position advances only after InfluxDB acknowledges a batch. A failure leaves earlier batches recorded and the failed batch and everything after it for the next run. (Influx spec §5.2.)

Requirement: InfluxDB writes are bounded to one measurement and one target REQ_INFLUX_WRITE_SCOPE
tags: safety, influx
is verified by: TEST_INFLUX_WRITE_SCOPE

The only writes to InfluxDB are POSTs of trackiwi_position line protocol to the configured target’s write endpoint (/api/v2/write or /write). No deletes, no schema or bucket management. (Influx spec §3.2.)

Requirement: InfluxDB credentials are never shown REQ_INFLUX_TOKEN_REDACT
tags: privacy, security, influx
is verified by: TEST_INFLUX_TOKEN_REDACT

The InfluxDB token and password never appear in output: every message built from server-controlled text is redacted first. (Influx spec §5.6.)

Requirement: Re-sending positions never creates duplicates REQ_MIRROR_IDEMPOTENT
tags: integrity, influx
is verified by: TEST_MIRROR_IDEMPOTENT

A cached row always becomes the same point — same measurement, tags and timestamp — so re-sending (including a full backfill) overwrites rather than duplicates. (Influx spec §4.)

Requirement: A tracker without a name never stalls the mirror REQ_MIRROR_FALLBACK_NAME
tags: integrity, influx

After a successful tracker listing, ingest stores a name for every tracker that has cached rows: the real name, or else the stable fallback tracker <id> when the API gives none or no longer lists the tracker. A real name replaces a fallback; a fallback never replaces a stored name. (Issue #16.)

Requirement: Committed deployment templates carry no deployment's specifics REQ_PORTABLE_CONFIG
tags: privacy, portability, influx
is verified by: TEST_PORTABLE_CONFIG

No file under deploy/ or examples/ contains a hostname, IP address, credential, tracker id or coordinate from any real deployment; values are placeholders, and every variable the compose file uses is defined in deploy/example.env. (Influx spec §6.)

Requirement: The compose stack exposes as little as possible REQ_DEPLOY_LEAST_EXPOSURE
tags: security, influx

Every port deploy/docker-compose.yml publishes binds to 127.0.0.1 unless a bind-address variable is set deliberately, and the ingest container receives a token scoped to read and write on the trackiwi bucket only — never the operator token. Read is required because influx check confirms the bucket exists through GET /api/v2/buckets, which lists only the buckets the token may read. (Influx spec §7.1.)

Requirement: The dashboard is importable anywhere REQ_DASHBOARD_PORTABLE
tags: portability, influx
is verified by: TEST_DASHBOARD_PORTABLE

The committed Grafana dashboard references its datasource only through a DS_TRACKIWI variable, reads its bucket and tracker from dashboard variables, uses Flux throughout, and contains no hardcoded datasource UID or real value. (Influx spec §8.)