function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}

/**
 * Removes excluded text from a Node.
 *
 * @param {Node} target Node to filter.
 * @param {string} exclude CSS selector of nodes to exclude.
 * @returns {DOMString} Text from `target` with text removed.
 */
function filterText(target, exclude) {
    const clone = target.cloneNode(true);  // clone as to not modify the live DOM
    if (exclude) {
        // remove excluded nodes
        clone.querySelectorAll(exclude).forEach(node => node.remove());
    }
    return clone.innerText;
}

// Callback when a copy button is clicked. Will be passed the node that was clicked
// should then grab the text and replace pieces of text that shouldn't be used in output
function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
    var regexp;
    var match;

    // Do we check for line continuation characters and "HERE-documents"?
    var useLineCont = !!lineContinuationChar
    var useHereDoc = !!hereDocDelim

    // create regexp to capture prompt and remaining line
    if (isRegexp) {
        regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)')
    } else {
        regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)')
    }

    const outputLines = [];
    var promptFound = false;
    var gotLineCont = false;
    var gotHereDoc = false;
    const lineGotPrompt = [];
    for (const line of textContent.split('\n')) {
        match = line.match(regexp)
        if (match || gotLineCont || gotHereDoc) {
            promptFound = regexp.test(line)
            lineGotPrompt.push(promptFound)
            if (removePrompts && promptFound) {
                outputLines.push(match[2])
            } else {
                outputLines.push(line)
            }
            gotLineCont = line.endsWith(lineContinuationChar) & useLineCont
            if (line.includes(hereDocDelim) & useHereDoc)
                gotHereDoc = !gotHereDoc
        } else if (!onlyCopyPromptLines) {
            outputLines.push(line)
        } else if (copyEmptyLines && line.trim() === '') {
        }
    }

    // If no lines with the prompt were found then just use original lines
    if (lineGotPrompt.some(v => v === true)) {
        textContent = outputLines.join('\n');
    }

    // Remove a trailing newline to avoid auto-running when pasting
    if (textContent.endsWith("\n")) {
        textContent = textContent.slice(0, -1)
    }
    return textContent
}

The newly created account is inactive. The user must confirm his email
to activate it.

HTTP/1.1 200 SUCCESS
Content-Type: application/json

{
  "status": "success"
}

HTTP/1.1 400 BAD REQUEST

{
  "message": "Errors: email: valid email must be provided\n",
  "status": "error"
}

lang (string) -- user language preferences (if not provided or invalid,
fallback to 'en' (english))

user language preferences (if not provided or invalid,

fallback to ‘en’ (english))

accepted_policy (boolean) -- true if user accepted privacy policy

400 Bad Request -- invalid payload

sorry, that username is already taken

sorry, you must agree privacy policy to register

username: 3 to 30 characters required

username: only alphanumeric characters and the underscore
character "_" allowed

email: valid email must be provided

password: 8 characters required

- ``invalid payload``
- ``sorry, that username is already taken``
- ``sorry, you must agree privacy policy to register``
- ``username: 3 to 30 characters required``
- ``username: only alphanumeric characters and the underscore
  character "_" allowed``
- ``email: valid email must be provided``
- ``password: 8 characters required``

``username: only alphanumeric characters and the underscore
character "_" allowed``

500 Internal Server Error -- error, please try again or contact the administrator

POST /api/auth/account/confirm HTTP/1.1

HTTP/1.1 200 OK

{
  "auth_token": "JSON Web Token",
  "message": "account confirmation successful",
}

POST /api/auth/account/resend-confirmation HTTP/1.1

{
  "message": "confirmation email resent",
}

{
  "message": "successfully logged in",
}

HTTP/1.1 401 UNAUTHORIZED

{
  "message": "invalid credentials",
}

{
  "data": {
    "accepted_privacy_policy": true,
    "admin": false,
    "bio": null,
    "birth_date": null,
    "created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
    "date_format": "dd/MM/yyyy",
    "display_ascent": true,
    "email": "sam@example.com",
    "email_to_confirm": null,
    "first_name": null,
    "imperial_units": false,
    "is_active": true,
    "language": "en",
    "last_name": null,
    "location": null,
    "nb_sports": 3,
    "nb_workouts": 6,
    "picture": false,
    "records": [
      {
        "id": 9,
        "record_type": "AS",
        "sport_id": 1,
        "user": "sam",
        "value": 18,
        "workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
        "workout_id": "hvYBqYBRa7wwXpaStWR4V2"
      },
      {
        "id": 10,
        "record_type": "FD",
      },
      {
        "id": 13,
        "record_type": "HA",
        "user": "Sam",
        "value": 43.97,
      },
      {
        "id": 11,
        "record_type": "LD",
        "value": "1:01:00",
      },
      {
        "id": 12,
        "record_type": "MS",
      }
    ],
    "sports_list": [
        1,
        4,
        6
    ],
    "start_elevation_at_zero": false,
    "timezone": "Europe/Paris",
    "total_ascent": 720.35,
    "total_distance": 67.895,
    "total_duration": "6:50:27",
    "use_raw_gpx_speed": false,
    "username": "sam",
    "weekm": false
  },
}

401 Unauthorized -- provide a valid auth token

signature expired, please log in again

invalid token, please log in again

- ``provide a valid auth token``
- ``signature expired, please log in again``
- ``invalid token, please log in again``

POST /api/auth/profile/edit HTTP/1.1

{
  "data": {
      {
        "id": 9,
      },
      {
        "id": 10,
      },
      {
        "id": 13,
      },
      {
        "id": 11,
      },
      {
        "id": 12,
      }
    ],
        1,
        4,
        6
    ],
    "username": "sam"
    "weekm": true,
  },
  "message": "user profile updated",
}

``date_string``, corresponding on client to:

- ``MMM. do, yyyy`` for ``en`` locale
- ``d MMM yyyy`` for ``es``, ``fr``, ``gl``, ``it`` and ``nl`` locales
- ``do MMM yyyy`` for ``de`` and ``nb`` locales

``d MMM yyyy`` for ``es``, ``fr``, ``gl``, ``it`` and ``nl`` locales

POST /api/auth/profile/edit/preferences HTTP/1.1

{
  "data": {
    "date_format": "MM/dd/yyyy",
      {
        "id": 9,
      },
      {
        "id": 10,
      },
      {
        "id": 13,
      },
      {
        "id": 11,
      },
      {
        "id": 12,
      }
    ],
        1,
        4,
        6
    ],
    "start_elevation_at_zero": true,
    "use_raw_gpx_speed": true,
  },
  "message": "user preferences updated",
}

date_format (string) -- the format used to display dates in the app

display_ascent (boolean) -- display highest ascent records and total

start_elevation_at_zero (boolean) -- do elevation plots start at zero?

use_raw_gpx_speed (boolean) -- Use unfiltered gpx to calculate speeds

password: password and password confirmation don't match

- ``password: password and password confirmation don't match``

POST /api/auth/profile/edit/sports HTTP/1.1

{
  "data": {
    "color": "#000000",
    "sport_id": 1,
    "stopped_speed_threshold": 1,
    "user_id": 1
  },
  "message": "user sport preferences updated",
}

stopped_speed_threshold (float) -- stopped speed threshold used by gpxpy

DELETE /api/auth/profile/reset/sports/1 HTTP/1.1

POST /api/auth/picture HTTP/1.1
Content-Type: multipart/form-data

{
  "message": "user picture updated",
}

no file part

no selected file

file extension not allowed

- ``no file part``
- ``no selected file``
- ``file extension not allowed``

413 Request Entity Too Large -- error during picture update: file size exceeds 1.0MB

DELETE /api/auth/picture HTTP/1.1

POST /api/auth/password/reset-request HTTP/1.1

{
  "message": "password reset request processed",
}

Email change:

- one to the current address to inform user
- another one to the new address to confirm it.

PATCH /api/auth/profile/edit/account HTTP/1.1

{
  "data": {
      {
        "id": 9,
      },
      {
        "id": 10,
      },
      {
        "id": 13,
      },
      {
        "id": 11,
      },
      {
        "id": 12,
      }
    ],
        1,
        4,
        6
    ],
  },
  "message": "user account updated",
}

email is missing

current password is missing

- ``email is missing``
- ``current password is missing``

invalid credentials

- ``invalid credentials``

POST /api/auth/password/update HTTP/1.1

{
  "message": "password updated",
}

POST /api/auth/email/update HTTP/1.1

{
  "message": "email updated",
}

User logout.
If a valid token is provided, it will be blacklisted.

{
  "message": "successfully logged out",
}

{
  "message": "provide a valid auth token",
}

The access token provided is expired, revoked, malformed, or invalid
for other reasons.

- ``The access token provided is expired, revoked, malformed, or invalid
  for other reasons.``

``The access token provided is expired, revoked, malformed, or invalid
for other reasons.``

POST /auth/account/privacy-policy HTTP/1.1

{
}

Get a data export info for authenticated user if a request exists.

GET /auth/account/export HTTP/1.1

{
  "status": "success",
  "request": {
      "created_at": "Wed, 01 Mar 2023 12:31:17 GMT",
      "status": "successful",
      "file_name": "archive_rgjsR3fHt295ywNQr5Yp.zip",
      "file_size": 924
  }
}

{
  "request": null
}

POST /auth/account/export/request HTTP/1.1

{
      "status": "in_progress",
      "file_name": null,
      "file_size": null
  }
}

400 Bad Request -- ongoing request exists

completed request already exists

- ``ongoing request exists``
- ``completed request already exists``

GET /auth/account/export/download/archive_rgjsR3fHr5Yp.zip HTTP/1.1

{
  "data": {
    "about": null,
    "admin_contact": "admin@example.com",
    "gpx_limit_import": 10,
    "is_email_sending_enabled": true,
    "is_registration_enabled": false,
    "max_single_file_size": 1048576,
    "max_users": 0,
    "max_zip_file_size": 10485760,
    "map_attribution": "&copy; <a href=http://www.openstreetmap.org/copyright>OpenStreetMap</a> contributors",
    "privacy_policy": null,
    "privacy_policy_date": null,
    "version": "0.7.26",
    "weather_provider": null
  },
}

{
  "data": {
    "max_users": 10,
  },
}

gpx_limit_import (integer) -- max number of files in zip archive

max_users (integer) -- max users allowed to register on instance

valid email must be provided for admin contact

- ``valid email must be provided for admin contact``

{
  "message": "pong!",
}

Get OAuth2 clients (apps) for authenticated user with pagination
(5 clients/page).

This endpoint is only accessible by FitTrackee client (first-party
application).

GET /api/oauth/apps?page=2 HTTP/1.1

{
  "data": {
    "clients": [
      {
        "client_description": "",
        "client_id": "o22a27s2aBPUoxJbxV3UjDOx",
        "id": 1,
        "issued_at": "Thu, 14 July 2022 06:27:53 GMT",
        "name": "GPX Importer",
        "redirect_uris": [
          " https://example.com/callback"
        ],
        "scope": "profile:read workouts:write",
        "website": "https://example.com"
      }
    ]
  },
  "pagination": {
    "has_next": false,
    "has_prev": false,
    "page": 1,
    "pages": 1,
    "total": 1
  },
}

{
  "data": {
    "client": {
      "client_description": "",
      "client_id": "o22a27s2aBPUoxJbxV3UjDOx",
      "client_secret": "<CLIENT SECRET>",
      "id": 1,
      "issued_at": "Thu, 14 July 2022 06:27:53 GMT",
      "name": "GPX Importer",
      "redirect_uris": [
        "https://example.com/callback"
      ],
      "scope": "profile:read workouts:write",
      "website": "https://example.com"
    }
  },
  "status": "created"
}

GET /api/oauth/apps/o22a27s2aBPUoxJbxV3UjDOx HTTP/1.1

{
  "data": {
      "id": 1,
      ],
    }
  },
}

HTTP/1.1 404 NOT FOUND

{
  "status": "not found",
  "message": "OAuth2 client not found"
}

GET /api/oauth/apps/1/by_id HTTP/1.1

DELETE /api/oauth/apps/1 HTTP/1.1

POST /api/oauth/apps/1/revoke HTTP/1.1

Authorize an OAuth2 client (app).
If successful, it redirects to the client callback URL with the code to
issue a token.

POST /api/oauth/authorize HTTP/1.1

string  response_type -- client response type (only 'code' is supported
by FitTrackee)

string  state -- unique value to prevent cross-site request forgery
(not mandatory but recommended)

unique value to prevent cross-site request forgery

string  code_challenge -- string generated from a code verifier
(for PKCE, not mandatory but recommended)

string generated from a code verifier

string  code_challenge_method -- method used to create challenge,
for instance "S256" (mandatory if code_challenge
provided)

method used to create challenge,
for instance "S256" (mandatory if `code_challenge`
provided)

for instance “S256” (mandatory if 

errors returned by Authlib library

POST /api/oauth/token HTTP/1.1

{
  "access_token": "rOEHv64THCG28WcewZHRnVLUsOdUvw8NVnHKCmL57e",
  "expires_in": 864000,
  "refresh_token": "NuV9cY8VQOnrQKHTZ5pQAq2Zw7mSH0MorNPJr14AmSwD6f6I",
  "scope": ["profile:read", "workouts:write"],
  "token_type": "Bearer",
  "expires_at": 1658660147.0667062
}

string  grant_type -- OAuth2 client grant type
(only 'authorization_code' (for token issue)
and 'refresh_token' (for token refresh)
are supported by FitTrackee)

OAuth2 client grant type

(only ‘authorization_code’ (for token issue)
and ‘refresh_token’ (for token refresh)

string  code -- code generated after authorizing the client
(for token issue)

string  code_verifier -- code verifier
(for token issue with PKCE, not mandatory)

POST /api/oauth/revoke HTTP/1.1

Following types of records are available:
- average speed (record_type: ``AS``)
- farthest distance (record_type: ``FD``)
- highest ascent (record_type: ``HA``)
- longest duration (record_type: ``LD``)
- maximum speed (record_type: ``MS``)

{
  "data": {
      {
        "id": 9,
        "user": "admin",
      },
      {
        "id": 10,
      },
      {
        "id": 13,
      },
      {
        "id": 11,
      },
      {
        "id": 12,
      }
    ]
  },
}

{
  "data": {
    "records": []
  },
}

{
  "data": {
    "sports": [
      {
        "color": null,
        "id": 1,
        "is_active": true,
        "is_active_for_user": true,
        "label": "Cycling (Sport)",
        "stopped_speed_threshold": 1
      },
      {
        "id": 2,
        "label": "Cycling (Transport)",
      },
      {
        "id": 3,
        "label": "Hiking",
        "stopped_speed_threshold": 0.1
      },
      {
        "id": 4,
        "label": "Mountain Biking",
      },
      {
        "id": 5,
        "label": "Running",
      },
      {
        "id": 6,
        "label": "Walking",
      }
    ]
  },
}

{
  "data": {
      {
        "has_workouts": true,
        "id": 1,
      },
      {
        "has_workouts": false,
        "id": 2,
      },
      {
        "id": 3,
      },
      {
        "id": 4,
      },
      {
        "id": 5,
      },
      {
        "id": 6,
      }
    ]
  },
}

{
  "data": {
      {
        "id": 1,
      }
    ]
  },
}

{
  "data": {
      {
        "id": 1,
      }
    ]
  },
}

{
  "data": {
    "sports": []
  },
  "status": "not found"
}

{
  "data": {
      {
        "id": 1,
        "is_active": false,
        "is_active_for_user": false,
      }
    ]
  },
}

GET /api/stats/admin/by_time?from=2018-01-01&to=2018-06-30&time=week
  HTTP/1.1

{
  "data": {
    "statistics": {
      "2017": {
        "3": {
          "average_speed": 4.48,
          "nb_workouts": 2,
          "total_ascent": 203.0,
          "total_ascent": 156.0,
          "total_distance": 15.282,
          "total_duration": 12341
        }
      },
      "2019": {
        "1": {
          "average_speed": 16.99,
          "nb_workouts": 3,
          "total_ascent": 150.0,
          "total_ascent": 178.0,
          "total_distance": 47,
          "total_duration": 9960
        },
        "2": {
          "average_speed": 15.95,
          "nb_workouts": 1,
          "total_ascent": 46.0,
          "total_ascent": 78.0,
          "total_distance": 5.613,
          "total_duration": 1267
        }
      }
    }
  },
}

{
  "data": {
      "statistics": {}
  },
}

time (string) -- time frame:week: week starting Sunday

weekm: week starting Monday

month: month

year: year (default)

time frame:

- ``week``: week starting Sunday
- ``weekm``: week starting Monday
- ``month``: month
- ``year``: year (default)

{
  "data": {
      "1": {
        "average_speed": 16.99,
        "nb_workouts": 3,
        "total_ascent": 150.0,
        "total_ascent": 178.0,
        "total_distance": 47,
        "total_duration": 9960
      },
      "2": {
        "average_speed": 15.95,
        "nb_workouts": 1,
        "total_ascent": 46.0,
        "total_ascent": 78.0,
        "total_distance": 5.613,
        "total_duration": 1267
      },
      "3": {
        "average_speed": 4.46,
        "nb_workouts": 2,
        "total_ascent": 203.0,
        "total_ascent": 156.0,
        "total_distance": 15.282,
        "total_duration": 12341
      }
    }
  },
}

{
  "data": {
    "sports": 3,
    "uploads_dir_size": 1000,
    "users": 2,
    "workouts": 3,
  },
}

Get all users (regardless their account status), if authenticated user
has admin rights.

GET /api/users?order_by=workouts_count&par_page=5  HTTP/1.1

{
  "data": {
    "users": [
      {
        "admin": true,
        "bio": null,
        "birth_date": null,
        "created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
        "email": "admin@example.com",
        "first_name": null,
        "is_admin": true,
        "imperial_units": false,
        "language": "en",
        "last_name": null,
        "location": null,
        "nb_sports": 3,
        "nb_workouts": 6,
        "picture": false,
        "records": [
          {
            "id": 9,
            "record_type": "AS",
            "sport_id": 1,
            "user": "admin",
            "value": 18,
            "workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
            "workout_id": "hvYBqYBRa7wwXpaStWR4V2"
          },
          {
            "id": 10,
            "record_type": "FD",
          },
          {
            "id": 13,
            "record_type": "HA",
            "user": "Sam",
            "value": 43.97,
          },
          {
            "id": 11,
            "record_type": "LD",
            "value": "1:01:00",
          },
          {
            "id": 12,
            "record_type": "MS",
          }
        ],
        "sports_list": [
            1,
            4,
            6
        ],
        "timezone": "Europe/Paris",
        "total_distance": 67.895,
        "total_duration": "6:50:27",
        "username": "admin",
        "weekm": false
      },
      {
        "admin": false,
        "created_at": "Sat, 20 Jul 2019 11:27:03 GMT",
        "email": "sam@example.com",
        "is_admin": false,
        "language": "fr",
        "nb_sports": 0,
        "nb_workouts": 0,
        "records": [],
        "sports_list": [],
        "total_distance": 0,
        "total_duration": "0:00:00",
        "username": "sam"
      }
    ]
  },
}

per_page (integer) -- number of users per page (default: 10, max: 50)

order_by (string) -- sorting criteria: username, created_at,
workouts_count, admin, is_active
(default: username)

sorting criteria: ``username``, ``created_at``,
``workouts_count``, ``admin``, ``is_active``
(default: ``username``)

Get single user details. Only user with admin rights can get other users
details.

{
  "data": [
    {
      "admin": true,
      "bio": null,
      "birth_date": null,
      "created_at": "Sun, 14 Jul 2019 14:09:58 GMT",
      "email": "admin@example.com",
      "first_name": null,
      "imperial_units": false,
      "is_admin": true,
      "language": "en",
      "last_name": null,
      "location": null,
      "nb_sports": 3,
      "nb_workouts": 6,
      "picture": false,
      "records": [
        {
          "id": 9,
          "record_type": "AS",
          "sport_id": 1,
          "user": "admin",
          "value": 18,
          "workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
          "workout_id": "hvYBqYBRa7wwXpaStWR4V2"
        },
        {
          "id": 10,
          "record_type": "FD",
        },
        {
          "id": 13,
          "record_type": "HA",
          "user": "Sam",
          "value": 43.97,
        },
        {
          "id": 11,
          "record_type": "LD",
          "value": "1:01:00",
        },
        {
          "id": 12,
          "record_type": "MS",
        }
      ],
      "sports_list": [
          1,
          4,
          6
      ],
      "timezone": "Europe/Paris",
      "total_distance": 67.895,
      "total_duration": "6:50:27",
      "username": "admin"
    }
  ],
}

GET /api/users/admin/picture HTTP/1.1

reset password (and send email to update user password,
if sending enabled)

update user email (and send email to new user email, if sending enabled)

PATCH /api/users/<user_name> HTTP/1.1

{
  "data": [
    {
      "is_active": true,
        {
          "id": 9,
        },
        {
          "id": 10,
        },
        {
          "id": 13,
        },
        {
          "id": 11,
        },
        {
          "id": 12,
        }
      ],
          1,
          4,
          6
      ],
    }
  ],
}

valid email must be provided

new email must be different than current email

- ``valid email must be provided``
- ``new email must be different than current email``

An admin can delete all accounts except his account if he's the only
one admin.

An admin can delete all accounts except his account if he’s the only
one admin.

DELETE /api/users/john_doe HTTP/1.1

403 Forbidden -- you do not have permissions

you can not delete your account, no other user has admin rights

- ``you do not have permissions``
- ``you can not delete your account, no other user has admin rights``

``you can not delete your account, no other user has admin rights``

GET /api/workouts?from=2019-07-02&to=2019-07-31&sport_id=1  HTTP/1.1

  {
    "data": {
      "workouts": [
        {
          "ascent": null,
          "ave_speed": 10.0,
          "bounds": [],
          "creation_date": "Sun, 14 Jul 2019 13:51:01 GMT",
          "descent": null,
          "distance": 10.0,
          "duration": "0:17:04",
          "id": "kjxavSTUrJvoAh2wvCeGEF",
          "map": null,
          "max_alt": null,
          "max_speed": 10.0,
          "min_alt": null,
          "modification_date": null,
          "moving": "0:17:04",
          "next_workout": 3,
          "notes": null,
          "pauses": null,
          "previous_workout": null,
          "records": [
            {
              "id": 4,
              "record_type": "MS",
              "sport_id": 1,
              "user": "admin",
              "value": 10.0,
              "workout_date": "Mon, 01 Jan 2018 00:00:00 GMT",
              "workout_id": "kjxavSTUrJvoAh2wvCeGEF"
            },
            {
              "id": 13,
              "record_type": "HA",
              "user": "Sam",
              "value": 43.97,
              "workout_date": "Sun, 07 Jul 2019 08:00:00 GMT",
              "workout_id": "hvYBqYBRa7wwXpaStWR4V2"
            },
            {
              "id": 3,
              "record_type": "LD",
              "value": "0:17:04",
            },
            {
              "id": 2,
              "record_type": "FD",
            },
            {
              "id": 1,
              "record_type": "AS",
            }
          ],
          "segments": [],
          "title": null,
          "weather_end": null,
          "weather_start": null,
          "with_gpx": false,
          "workout_date": "Mon, 01 Jan 2018 00:00:00 GMT"
        }
      ]
    },
    "status": "success"
  }

  {
      "data": {
          "workouts": []
      },
      "status": "success"
  }

per_page (integer) -- number of workouts per page
(default: 5, max: 100)

title (string) -- any part (or all) of the workout title;
title matching is case-insensitive

any part (or all) of the workout title;

order_by (string) -- sorting criteria: ave_speed, distance,
duration, workout_date (default:
workout_date)

sorting criteria: ``ave_speed``, ``distance``,
``duration``, ``workout_date`` (default:
``workout_date``)

  {
    "data": {
        {
          "ave_speed": 16,
          "creation_date": "Sun, 14 Jul 2019 18:57:14 GMT",
          "distance": 12,
          "duration": "0:45:00",
          "max_speed": 16,
          "modification_date": "Sun, 14 Jul 2019 18:57:22 GMT",
          "moving": "0:45:00",
          "next_workout": 4,
          "notes": "workout without gpx",
          "previous_workout": 3,
          "records": [],
          "title": "biking on sunday morning",
          "workout_date": "Sun, 07 Jul 2019 07:00:00 GMT"
        }
      ]
    },
  }

  {
    "data": {
      "workouts": []
    },
    "status": "not found"
  }

GET /api/workouts/kjxavSTUrJvoAh2wvCeGEF/gpx HTTP/1.1

{
  "data": {
    "gpx": "gpx file content"
  },
  "message": "",
}

404 Not Found -- workout not found

no gpx file for this workout

Get chart data from a workout gpx file, to display it with Chart.js.

GET /api/workouts/kjxavSTUrJvoAh2wvCeGEF/chart HTTP/1.1

{
  "data": {
    "chart_data": [
      {
        "distance": 0,
        "duration": 0,
        "elevation": 279.4,
        "latitude": 51.5078118,
        "longitude": -0.1232004,
        "speed": 8.63,
        "time": "Fri, 14 Jul 2017 13:44:03 GMT"
      },
      {
        "distance": 7.5,
        "duration": 7380,
        "elevation": 280,
        "latitude": 51.5079733,
        "longitude": -0.1234538,
        "speed": 6.39,
        "time": "Fri, 14 Jul 2017 15:47:03 GMT"
      }
    ]
  },
}

GET /api/workouts/kjxavSTUrJvoAh2wvCeGEF/chart/segment/0 HTTP/1.1

Get gpx file for a workout segment displayed on map with Leaflet.

GET /api/workouts/kjxavSTUrJvoAh2wvCeGEF/gpx/segment/0 HTTP/1.1

GET /api/workouts/map/fa33f4d996844a5c73ecd1ae24456ab8?1563529507772
  HTTP/1.1

HTTP/1.1 201 CREATED

 {
    "data": {
        {
            {
              "id": 4,
              "value": 10.,
            },
            {
              "id": 3,
              "workout_id": "kjxavSTUrJvoAh2wvCeGEF",
            },
            {
              "id": 2,
            },
            {
              "id": 1,
            }
          ],
        }
      ]
    },
  }

data -- sport id and notes (example: {"sport_id": 1, "notes": ""}).
Double quotes in notes must be escaped.

sport id and notes (example: ``{"sport_id": 1, "notes": ""}``).

POST /api/workouts/no_gpx HTTP/1.1

 {
    "data": {
        {
            {
              "id": 4,
            },
            {
              "id": 3,
            },
            {
              "id": 2,
            },
            {
              "id": 1,
            }
          ],
          "uuid": "kjxavSTUrJvoAh2wvCeGEF"
        }
      ]
    },
  }

ascent (float) -- workout ascent (not mandatory,
must be provided with descent)

descent (float) -- workout descent (not mandatory,
must be provided with ascent)

workout_date (string) -- workout date, in user timezone
(format: %Y-%m-%d %H:%M)

 {
    "data": {
        {
            {
              "id": 4,
            },
            {
              "id": 3,
            },
            {
              "id": 2,
            },
            {
              "id": 1,
            }
          ],
        }
      ]
    },
  }

ascent (float) -- workout ascent
(only for workout without gpx, must be provided with descent)

workout ascent

descent (float) -- workout descent
(only for workout without gpx, must be provided with ascent)

workout descent

distance (float) -- workout distance in km
(only for workout without gpx)

duration (integer) -- workout duration in seconds

workout_date (string) -- workout date in user timezone

workout date in user timezone
(format: ``%Y-%m-%d %H:%M``)

DELETE /api/workouts/kjxavSTUrJvoAh2wvCeGEF HTTP/1.1

#224 - Missing elevation results in incorrect ascent/descent total

PR#421 - remove darksky from available weather providers in .env

Note: This release contains database migration (see upgrade instructions in documentation)

 This release contains database migration (see upgrade instructions in 

#384 - Inconsistent page with between workout with and without GPS data

 - Inconsistent page with between workout with and without GPS data

Polish is available in FitTrackee interface (partially translated).<br />Documentation is now translated in French (note: documentation translations are not yet available on Weblate).

Important: Python 3.7 is no longer supported, the minimum version is now Python 3.8.1.

: Python 3.7 is no longer supported, the minimum version is now Python 3.8.1.

PR#371 - Translations update (Polish, thanks to Mariusz on Weblate)

PR#366, PR#369 - Translations update from Hosted Weblate (Galician, thanks to @xmgz)

 - Translations update from Hosted Weblate (Galician, thanks to @xmgz)

Among enhancements and fixes, FitTrackee is now available in Galician, Spanish and partially in Norwegian Bokmål (see translation status below).

 is now available in Galician, Spanish and partially in Norwegian Bokmål (see translation status below).

Note: DarkSky API support is removed, since the service shut down on March 31, 2023.

 API support is removed, since the service shut down on March 31, 2023.

#338 - Display relevant error message when <time> is missing in GPX file

#328 - GPX speed and altitude track has incorrect units on x-axis when imperial

 - GPX speed and altitude track has incorrect units on x-axis when imperial

PR#315 - Translations update from Hosted Weblate (Dutch, thanks to @bjornclauw)

 - Translations update from Hosted Weblate (Dutch, thanks to @bjornclauw)

This version allows to display the instance privacy policy. A user must agree to the privacy policy to register.<br />A default policy is available and a custom policy can be defined in the administration.<br />Note: After FitTrackee upgrade, a message will be displayed to all users in order to review the policy.

This version allows to display the instance privacy policy. A user must agree to the privacy policy to register.

A default policy is available and a custom policy can be defined in the administration.

 upgrade, a message will be displayed to all users in order to review the policy.

A user can now request a data export (containing user info, workout data and uploaded gpx files).

Lastly, additional information that may be useful to users can be displayed in About page.

Lastly, additional information that may be useful to users can be displayed in 

PR#310 - Translations update from Hosted Weblate (Dutch and German)

PR#290 - Translations update from Hosted Weblate (German, thanks to @qwerty287)

 - Translations update from Hosted Weblate (German, thanks to @qwerty287)

PR#265 - Implementing alternative weather API (VisualCrossing.com)<br />Note: A new environment variable must be to set to configure the weather data provider: WEATHER_API_PROVIDER (see documentation)

: A new environment variable must be to set to configure the weather data provider: 

#92 - Add ascent and descent parameters in workout import without GPX file

 - Add ascent and descent parameters in workout import without GPX file

c88a515 - Translations update from Hosted Weblate (Dutch, thanks to @bjornclauw)

PR#278 - Translations update from Hosted Weblate (German, thanks to @qwerty287)

Note: fittrackee_worker command is disabled, please use existing flask-dramatiq CLI (see documentation)

 command is disabled, please use existing flask-dramatiq CLI (see 

#266 - Translations update from Hosted Weblate (German, thanks to @qwerty287)

#261 - The API should return an error when the number of files in an archive exceeds the limit

 - The API should return an error when the number of files in an archive exceeds the limit

PR#242 - Translations update from Weblate (German, thanks to @qwerty287)

 - Translations update from Weblate (German, thanks to @qwerty287)

PR#241 - Add missing password strength estimation package (german)

PR#239 - Translations update from Weblate (German, thanks to @qwerty287)

PR#212 - Translations update from Hosted Weblate (thanks to J. Lavoie)

 - Translations update from Hosted Weblate (thanks to J. Lavoie)

In this release 1 issue was closed.<br />Note: This release contains a fix on the last database migration that will be executed only on versions lower than v0.6.11 (no data differences with v0.6.11, the fix allows to execute the migration on Postgres<12)<br />(see upgrade instructions in documentation).

 This release contains a fix on the last database migration that will be executed only on versions lower than v0.6.11 (no data differences with v0.6.11, the fix allows to execute the migration on Postgres

#213 - Statistics - When using imperial measurements, ascent/descent should be in ft not mi

 - Statistics - When using imperial measurements, ascent/descent should be in ft not mi

In this release 1 issue was closed.<br />Note: This release contains database migration (see upgrade instructions in documentation)

#210 - ERROR - could not download 6 tiles<br />Note: for tile server requiring subdomains, see the new environment variable STATICMAP_SUBDOMAINS

: for tile server requiring subdomains, see the new environment variable 

#209 - Incorrect duration with track containing multiple segments

FitTrackee is now available in German (thanks to @gorgobacka).<br />And translations can be updated on Weblate.

#200 - Detect browser language to use matching translation if available

 - Detect browser language to use matching translation if available

#201 - html lang attribute is not updated when changing language

change gpx and map file naming (included in PR#195)<br />Note: it does not affect previously imported files

No new features in this release, only dependencies update and code refacto before introducing new features.

It is now possible to start FitTrackee without a configured SMTP provider (see documentation).
It reduces pre-requisites for single-user instances.

It is now possible to start FitTrackee without a configured SMTP provider (see 

This version introduces some changes on user registration.<br />From now on, a user needs to confirm his account after registration (an email with confirmation instructions is sent after registration).

From now on, a user needs to confirm his account after registration (an email with confirmation instructions is sent after registration).

In this release 3 issues were closed.<br />Note: This release contains database migration (see upgrade instructions in documentation)

This release contains several fixes including security fixes.<br />Thanks to @DanielSiersleben for the report.

And from now on, admin account is not created on application initialization.<br />A new command is added to set administration rights on the account created after registration
(see documentation)

And from now on, admin account is not created on application initialization.

A new command is added to set administration rights on the account created after registration

fix dramatiq warning when launching workers w/ script entrypoint

check app config before dropping database, to avoid deleting data on production

877fa0f - fix sport icon color (when changed) on calendar on small resolutions

 - fix sport icon color (when changed) on calendar on small resolutions

#127 - parse_email_url() can't validate a legitimate EMAIL_URI such as "smtp://localhost:25"

In this release 2 issues were closed.<br />Note: This release contains database migration.

#116 - Better UI for Speed and Elevation buttons in the graph of the Workout screen

 - Better UI for Speed and Elevation buttons in the graph of the Workout screen

#91 - Display elevation chart with min and max altitude of workout

#98/#109 - Added stopped_speed_threshold to support slow movement

In this release 6 issues were closed.<br />Note: This release contains database migrations.

#83 - allow using configured tile server to generate static maps<br />Note: to keep using the default tile server, set environment variable DEFAULT_STATICMAP to True

: to keep using the default tile server, set environment variable 

Update Python and Javascript dependencies<br />IMPORTANT: Due to SQLAlchemy update (1.4+), engine URLs starting with postgres:// are no longer supported. Please update DATABASE_URL with postgresql://.

#70 - max size for an archive must not be less than uploaded files max size

 - max size for an archive must not be less than uploaded files max size

#58 - Standardize terms used for workouts<br />Note: Database model, upload directory for workouts and API endpoints are also updated.

 Database model, upload directory for workouts and API endpoints are also updated.

No new features in this release, only some refactorings before introducing
new features.

Installation becomes more easy. FitTrackee can be now be installed from PyPi.<br />⚠️ Warning: please read installation documentation, some environment variables and files have been renamed.

It's now possible to change the tile provider for maps. The default tile server is now OpenStreetMap's standard tile layer (replacing ThunderForest Outdoors),
see Map tile server in documentation.

s now possible to change the tile provider for maps. The default tile server is now 

FitTrackee administration is now available (see documentation)<br />⚠️ Warning: some application parameters move from environment variables to database (see installation).

⚠️ Warning: some application parameters move from environment variables to database (see 

This version contains minor fix and improvements on client side:

b862a77,
2e1ee2c -
add URL interceptors to simplify routes definition

#46 - calendar cannot display more than 5 or 6 activities on the same day

 - calendar cannot display more than 5 or 6 activities on the same day

#43 - Display weekend days with a different background color on calendar

 - Display weekend days with a different background color on calendar

#24 - Some distances are not displayed correctly on current month statistics

 - Some distances are not displayed correctly on current month statistics

Activity creation by uploading a gpx file. An activity can even be created without gpx (the user must enter date, time, duration and distance)

An activity can be displayed with map (if with gpx), weather (if the DarkSky API key is provided) and charts (speed and elevation)

A command line interface (CLI) is available to manage database, OAuth2 tokens and users.

$ ftcli
Usage: ftcli [OPTIONS] COMMAND [ARGS]...

  FitTrackee Command Line Interface

Options:
  --help  Show this message and exit.

Commands:
  db      Manage database.
  oauth2  Manage OAuth2 tokens.
  users   Manage users.

| The following commands are now deprecated and will be removed in a next version:
| - ``fittrackee_set_admin``
| - ``fittrackee_upgrade_db``
| - ``fittrackee_worker`` (disabled)

The following commands are now deprecated and will be removed in a next version:

Empty database and delete uploaded files, only on development environments.

Delete export requests and related archives created more than provided number of days.

Remove blacklisted tokens expired for more than provided number of days.

- the newly created account is already active.
- the CLI allows to create users when registration is disabled.

User password (if not provided, a random password is generated).

Process incomplete user export requests.
Can be used if redis is not set (no dramatiq workers running).

Modify a user account (admin rights, active status, email and password).

Add/remove admin rights (when adding admin rights, it also activates user account if not active).

**FitTrackee** allows you to store and display gpx files and some statistics from your outdoor activities.

 allows you to store and display gpx files and some statistics from your outdoor activities.

For now, this app is kind of a single-user application. Even if several users can register, a user can only view his own workouts.

Gpx files are stored in an upload directory (without encryption).

With the default configuration, `Open Street Map <https://www.openstreetmap.org>`__ is used as tile server in Workout detail and for static map generation.

 is used as tile server in Workout detail and for static map generation.

14 sports are supported:
   - Cycling (Sport)
   - Cycling (Transport)
   - Cycling (Virtual)  (*new in 0.7.3*)
   - Hiking
   - Mountain Biking
   - Mountain Biking (Electric)  (*new in 0.5.0*)
   - Mountaineering  (*new in 0.7.9*)
   - Open Water Swimming  (*new in 0.7.20*)
   - Paragliding  (*new in 0.7.19*)
   - Rowing  (*new in 0.5.0*)
   - Running
   - Skiing (Alpine) (*new in 0.5.0*)
   - Skiing (Cross Country)  (*new in 0.5.0*)
   - Snowshoes (*new in 0.5.2*)
   - Trail (*new in 0.5.0*)
   - Walking

- Cycling (Sport)
- Cycling (Transport)
- Cycling (Virtual)  (*new in 0.7.3*)
- Hiking
- Mountain Biking
- Mountain Biking (Electric)  (*new in 0.5.0*)
- Mountaineering  (*new in 0.7.9*)
- Open Water Swimming  (*new in 0.7.20*)
- Paragliding  (*new in 0.7.19*)
- Rowing  (*new in 0.5.0*)
- Running
- Skiing (Alpine) (*new in 0.5.0*)
- Skiing (Cross Country)  (*new in 0.5.0*)
- Snowshoes (*new in 0.5.2*)
- Trail (*new in 0.5.0*)
- Walking

(*new in 0.5.0*) Stopped speed threshold used by `gpxpy <https://github.com/tkrajina/gpxpy>`_ is not the default one for the following sports (0.1 km/h instead of 1 km/h):
   - Hiking
   - Mountaineering
   - Open Water Swimming
   - Paragliding
   - Skiing (Cross Country)
   - Snowshoes
   - Trail
   - Walking

- Hiking
- Mountaineering
- Open Water Swimming
- Paragliding
- Skiing (Cross Country)
- Snowshoes
- Trail
- Walking

 is not the default one for the following sports (0.1 km/h instead of 1 km/h):

Except the stopped speed threshold, all sports are analyzed in the same way (no specificity taken into account for the moment).

Dashboard with month calendar displaying workouts and record. The week can start on Sunday or Monday (which can be changed in the user preferences). The calendar displays up to 100 workouts.

Workout creation by uploading a gpx file (related data are stored in database in metric system).

| Only gpx files with time are supported.
| Calculated values may differ from values calculated by the application that originally generated the gpx files, in particular the maximum speed.

Calculated values may differ from values calculated by the application that originally generated the gpx files, in particular the maximum speed.

| A workout can even be created without gpx (the user must enter date, time, duration and distance).
| Ascent and descent can also be provided (*new in 0.7.10*).

A workout can even be created without gpx (the user must enter date, time, duration and distance).

| A workout with a gpx file can be displayed with map and charts (speed and elevation (if the gpx file contains elevation data, *updated in 0.7.20*)).
| Controls allow full screen view and position reset (*new in 0.5.5*).

A workout with a gpx file can be displayed with map and charts (speed and elevation (if the gpx file contains elevation data, *updated in 0.7.20*)).

A workout with a gpx file can be displayed with map and charts (speed and elevation (if the gpx file contains elevation data, 

Controls allow full screen view and position reset (*new in 0.5.5*).

| If **Visual Crossing** (*new in 0.7.11*) API key is provided, weather is displayed in workout detail. Data source is displayed in **About** page.
| Wind is displayed, with an arrow indicating the direction (a tooltip can be displayed with the direction that the wind is coming **from**) (*new in 0.5.5*).

If **Visual Crossing** (*new in 0.7.11*) API key is provided, weather is displayed in workout detail. Data source is displayed in **About** page.

) API key is provided, weather is displayed in workout detail. Data source is displayed in 

Wind is displayed, with an arrow indicating the direction (a tooltip can be displayed with the direction that the wind is coming **from**) (*new in 0.5.5*).

Wind is displayed, with an arrow indicating the direction (a tooltip can be displayed with the direction that the wind is coming 

User statistics, by time period (week, month, year) and sport:
  - total distance
  - total duration
  - total workouts
  - total ascent  (*new in 0.5.0*)
  - total descent  (*new in 0.5.0*)
  - average speed  (*new in 0.5.1*)

- total distance
- total duration
- total workouts
- total ascent  (*new in 0.5.0*)
- total descent  (*new in 0.5.0*)
- average speed  (*new in 0.5.1*)

User records by sports:
  - average speed
  - farthest distance
  - highest ascent (*new in 0.6.11*, can be hidden, see user preferences)
  - longest duration
  - maximum speed

- average speed
- farthest distance
- highest ascent (*new in 0.6.11*, can be hidden, see user preferences)
- longest duration
- maximum speed

highest ascent (*new in 0.6.11*, can be hidden, see user preferences)

Records may differ from records displayed by the application that originally generated the gpx files.

Workouts list.
  - The user can filter workouts on:
      - date
      - sports (only sports with workouts are displayed in sport dropdown)
      - title (*new in 0.7.15*)
      - distance
      - duration
      - average speed
      - maximum speed
  - Workouts can be sorted by:
      - date
      - distance
      - duration

- The user can filter workouts on:
    - date
    - sports (only sports with workouts are displayed in sport dropdown)
    - title (*new in 0.7.15*)
    - distance
    - duration
    - average speed
    - maximum speed
- Workouts can be sorted by:
    - date
    - distance
    - duration

The user can filter workouts on:
  - date
  - sports (only sports with workouts are displayed in sport dropdown)
  - title (*new in 0.7.15*)
  - distance
  - duration

- date
- sports (only sports with workouts are displayed in sport dropdown)
- title (*new in 0.7.15*)
- distance
- duration

sports (only sports with workouts are displayed in sport dropdown)

Workouts can be sorted by:
  - date
  - distance
  - duration

- date
- distance
- duration

The user must agree to the privacy policy to register. If a more recent policy is available, a message is displayed on the dashboard to review the new version (*new in 0.7.13*).

The user must agree to the privacy policy to register. If a more recent policy is available, a message is displayed on the dashboard to review the new version (

On registration, the user account is created with selected language in dropdown as user preference (*new in 0.6.9*).

On registration, the user account is created with selected language in dropdown as user preference (

After registration, the user account is inactive and an email with confirmation instructions is sent to activate it.
A user with an inactive account cannot log in. (*new in 0.6.0*).

A user with an inactive account cannot log in. (

In case email sending is not configured, a `command line <cli.html#ftcli-users-update>`__ allows to activate users account.

A user can choose between metric system and imperial system for distance, elevation and speed display (*new in 0.5.0*)

A user can choose between metric system and imperial system for distance, elevation and speed display (

A user can choose to display or hide ascent records and total on Dashboard (*new in 0.6.11*)

A user can choose to display or hide ascent records and total on Dashboard (

A user can choose elevation chart axis start: zero or minimum altitude (*new in 0.7.15*)

A user can choose elevation chart axis start: zero or minimum altitude (

A user can choose to exclude extreme values (which may be GPS errors) when calculating the maximum speed (by default, extreme values are excluded) (*new in 0.7.16*)

A user can choose to exclude extreme values (which may be GPS errors) when calculating the maximum speed (by default, extreme values are excluded) (

A user can set sport preferences (*new in 0.5.0*):
   - change sport color (used for sport image and charts)
   - can override stopped speed threshold (for next uploaded gpx files)
   - disable/enable a sport.

- change sport color (used for sport image and charts)
- can override stopped speed threshold (for next uploaded gpx files)
- disable/enable a sport.

can override stopped speed threshold (for next uploaded gpx files)

| If a sport is disabled by an administrator, it can not be enabled by a user. In this case, it will only appear in preferences if the user has workouts and only sport color can be changed.
| A disabled sport (by admin or user) will not appear in dropdown when **adding a workout**.
| A workout with a disabled sport will still be displayed in the application.

If a sport is disabled by an administrator, it can not be enabled by a user. In this case, it will only appear in preferences if the user has workouts and only sport color can be changed.

A disabled sport (by admin or user) will not appear in dropdown when **adding a workout**.

A disabled sport (by admin or user) will not appear in dropdown when 

A workout with a disabled sport will still be displayed in the application.

A user can create `clients <apps.html>`__ for third-party applications (*new in 0.7.0*).

| A user can request a data export (*new in 0.7.13*).
| It generates a zip archive containing 2 ``json`` files (user info and workouts data) and all uploaded gpx files.

It generates a zip archive containing 2 ``json`` files (user info and workouts data) and all uploaded gpx files.

 files (user info and workouts data) and all uploaded gpx files.

For now, it's not possible to import these files into another **FitTrackee** instance.

active users limit. If 0, registration is enabled (no limit defined).

maximum size of gpx file (individually uploaded or in a zip archive) (*changed in 0.7.4*)

maximum size of gpx file (individually uploaded or in a zip archive) (

Updating server configuration may be necessary to handle large files (like `nginx <https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>`_ for instance).

Updating server configuration may be necessary to handle large files (like 

It is possible displayed additional information that may be useful to users in **About** page.

It is possible displayed additional information that may be useful to users in 

A default privacy policy is available (originally adapted from the `Discourse <https://github.com/discourse/discourse>`__ privacy policy).

A default privacy policy is available (originally adapted from the 

A custom privacy policy can set if needed (Markdown syntax can be used). A policy update will display a message on users dashboard to review it.

Only the default privacy policy is translated (if the translation is available).

edit a user to:

- add/remove administration rights
- activate his account (*new in 0.6.0*)
- update his email (in case his account is locked) (*new in 0.6.0*)
- reset his password (in case his account is locked) (*new in 0.6.0*). If email sending is disabled, it is only possible via CLI.

update his email (in case his account is locked) (*new in 0.6.0*)

reset his password (in case his account is locked) (*new in 0.6.0*). If email sending is disabled, it is only possible via CLI.

enable or disable a sport (a sport can be disabled even if workout with this sport exists)

FitTrackee is available in the following languages (which can be saved in the user preferences):

Application translations status on `Weblate <https://hosted.weblate.org/engage/fittrackee/>`__ (may differ from the released version):

.. figure:: https://hosted.weblate.org/widgets/fittrackee/-/multi-auto.svg

.. figure:: _images/fittrackee_screenshot-01.png
   :alt: FitTrackee Dashboard

.. figure:: _images/fittrackee_screenshot-02.png
   :alt: FitTrackee Workout

.. figure:: _images/fittrackee_screenshot-03.png
   :alt: FitTrackee Workouts

.. figure:: _images/fittrackee_screenshot-04.png
   :alt: FitTrackee Statistics

.. figure:: _images/fittrackee_screenshot-05.png
   :alt: FitTrackee Administration

.. figure:: _images/fittrackee_screenshot-06.png
   :alt: FitTrackee Sports Administration

FitTrackee documentation master file, created by
sphinx-quickstart on Sun Jul 14 14:43:43 2019.

This web application allows you to track your outdoor activities (workouts)
from gpx files and keep your data on your own server.

No mobile app has been developed yet, but several existing mobile apps can
store workouts data locally and export them into a gpx file.

\* `Runner Up <https://github.com/jonasoreland/runnerup>`_ (GPL v3)

\* `OpenTracks <https://github.com/OpenTracksApp/OpenTracks>`_ (Apache License)

\* `FitoTrack <https://codeberg.org/jannis/FitoTrack>`_ (GPL v3)

Maps are displayed using `Open Street
Map <https://www.openstreetmap.org>`__.

**Still under heavy development (some features may be unstable).**

(see `issues <https://github.com/SamR1/FitTrackee/issues>`__ for more information)

This application is written in Python (API) and Typescript (client):

API:
  - Flask
  - `gpxpy <https://github.com/tkrajina/gpxpy>`_ to parse gpx files
  - `staticmap <https://github.com/komoot/staticmap>`_ to generate a static map image from gpx coordinates
  - `dramatiq <https://flask-dramatiq.readthedocs.io/en/latest/>`_ for task queue
  - `Authlib <https://docs.authlib.org/en/latest/>`_ for OAuth 2.0 Authorization support
  - `Flask-Limiter <https://flask-limiter.readthedocs.io/en/stable>`_ for API rate limits

API:
- Flask
- `gpxpy <https://github.com/tkrajina/gpxpy>`_ to parse gpx files
- `staticmap <https://github.com/komoot/staticmap>`_ to generate a static map image from gpx coordinates
- `dramatiq <https://flask-dramatiq.readthedocs.io/en/latest/>`_ for task queue
- `Authlib <https://docs.authlib.org/en/latest/>`_ for OAuth 2.0 Authorization support
- `Flask-Limiter <https://flask-limiter.readthedocs.io/en/stable>`_ for API rate limits

`staticmap <https://github.com/komoot/staticmap>`_ to generate a static map image from gpx coordinates

`dramatiq <https://flask-dramatiq.readthedocs.io/en/latest/>`_ for task queue

`Authlib <https://docs.authlib.org/en/latest/>`_ for OAuth 2.0 Authorization support

`Flask-Limiter <https://flask-limiter.readthedocs.io/en/stable>`_ for API rate limits

Client:
  - Vue3/Vuex
  - `Leaflet <https://leafletjs.com/>`__ to display map
  - `Chart.js <https://www.chartjs.org/>`__ to display charts with elevation and speed

Client:
- Vue3/Vuex
- `Leaflet <https://leafletjs.com/>`__ to display map
- `Chart.js <https://www.chartjs.org/>`__ to display charts with elevation and speed

`Chart.js <https://www.chartjs.org/>`__ to display charts with elevation and speed

Logo, some sports and weather icons are made by `Freepik <https://www.freepik.com/>`__ from `www.flaticon.com <https://www.flaticon.com/>`__.

FitTrackee also uses icons from `Fork Awesome <https://forkaweso.me>`__.

optional
  - Redis for task queue (if email sending is enabled and for data export requests) and API rate limits
  - SMTP provider (if email sending is enabled)
  - API key from a `weather data provider <installation.html#weather-data>`__
  - `Poetry <https://poetry.eustace.io>`__ (for installation from sources only)
  - `Node <https://nodejs.org>`__ 18+ and `Yarn <https://yarnpkg.com>`__ (for development only)
  -  Docker and Docker Compose (for development or evaluation purposes)

optional
- Redis for task queue (if email sending is enabled and for data export requests) and API rate limits
- SMTP provider (if email sending is enabled)
- API key from a `weather data provider <installation.html#weather-data>`__
- `Poetry <https://poetry.eustace.io>`__ (for installation from sources only)
- `Node <https://nodejs.org>`__ 18+ and `Yarn <https://yarnpkg.com>`__ (for development only)
-  Docker and Docker Compose (for development or evaluation purposes)

Redis for task queue (if email sending is enabled and for data export requests) and API rate limits

API key from a `weather data provider <installation.html#weather-data>`__

`Poetry <https://poetry.eustace.io>`__ (for installation from sources only)

`Node <https://nodejs.org>`__ 18+ and `Yarn <https://yarnpkg.com>`__ (for development only)

Docker and Docker Compose (for development or evaluation purposes)

| If registration is enabled, it is recommended to set Redis and a SMTP provider for email sending and data export requests.

If registration is enabled, it is recommended to set Redis and a SMTP provider for email sending and data export requests.

| The following steps describe an installation on Linux systems (tested
  on Debian and Arch).
| On other OS, some issues can be encountered and adaptations may be
  necessary.

The following steps describe an installation on Linux systems (tested
on Debian and Arch).

On other OS, some issues can be encountered and adaptations may be
necessary.

| Since FitTrackee 0.4.0, ``Makefile.custom.config`` is replaced by ``.env``

Since FitTrackee 0.4.0, ``Makefile.custom.config`` is replaced by ``.env``

The following environment variables are used by **FitTrackee** web application
or the task processing library. They are not all mandatory depending on
deployment method.

 web application

``FLASK_APP`` should contain ``$(PWD)/fittrackee/__main__.py`` with installation from sources, else ``fittrackee``.

**FitTrackee** secret key, must be initialized in production environment.

**Absolute path** to the directory where ``uploads`` folder will be created.

| With installation from PyPI, the directory will be located in
  **virtualenv** directory if the variable is not initialized.

With installation from PyPI, the directory will be located in
**virtualenv** directory if the variable is not initialized.

Database URL with username and password, must be initialized in production environment.

For example in dev environment : ``postgresql://fittrackee:fittrackee@localhost:5432/fittrackee``

| Since `SQLAlchemy update (1.4+) <https://docs.sqlalchemy.org/en/14/changelog/changelog_14.html#change-3687655465c25a39b968b4f5f6e9170b>`__,
  engine URL should begin with ``postgresql://``.

Since `SQLAlchemy update (1.4+) <https://docs.sqlalchemy.org/en/14/changelog/changelog_14.html#change-3687655465c25a39b968b4f5f6e9170b>`__,
engine URL should begin with ``postgresql://``.

Disable pooling if needed (when starting application with **FitTrackee** entry point and not directly with **Gunicorn**),
see `SqlAlchemy documentation <https://docs.sqlalchemy.org/en/13/core/pooling.html#using-connection-pools-with-multiprocessing-or-os-fork>`__.

Email URL with credentials, see `Emails <installation.html#emails>`__.

API rate limits, see `API rate limits <installation.html#api-rate-limits>`__.

Tile server URL (with api key if needed), see `Map tile server <installation.html#map-tile-server>`__.

Since **0.4.9**, it's also used to generate static maps (to keep default server, see `DEFAULT_STATICMAP <installation.html#envvar-DEFAULT_STATICMAP>`__)

, it’s also used to generate static maps (to keep default server, see 

The default URL is updated: **OpenStreetMap**'s tile server no longer requires subdomains.

Some tile servers require a subdomain, see `Map tile server <installation.html#map-tile-server>`__.

Map attribution (if using another tile server), see `Map tile server <installation.html#map-tile-server>`__.

``&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors``

&copy; <a href="http://www.openstreetmap.org/copyright" target="_blank" rel="noopener noreferrer">OpenStreetMap</a> contributors

If ``True``, it keeps using **staticmap** default tile server to generate static maps (OSM tile server since **staticmap** 0.5.6 (Komoot.de tile server before this version)).

 default tile server to generate static maps (OSM tile server since 

Otherwise, it uses the tile server set in `TILE_SERVER_URL <installation.html#envvar-TILE_SERVER_URL>`__.

If ``False``, depending on tile server, `subdomains <installation.html#envvar-STATICMAP_SUBDOMAINS>`__ may be mandatory.

Provider for weather data (not mandatory), see `Weather data <installation.html#weather-data>`__.

with an unencrypted SMTP server: ``smtp://username:password@smtp.example.com:25``

with SSL: ``smtp://username:password@smtp.example.com:465/?ssl=True``

with STARTTLS: ``smtp://username:password@smtp.example.com:587/?tls=True``

| If the email URL is invalid, the application may not start.
| Sending emails with Office365 may not work if SMTP auth is disabled.

Sending emails with Office365 may not work if SMTP auth is disabled.

| Since 0.6.0, newly created accounts must be confirmed (an email with confirmation instructions is sent after registration).

Since 0.6.0, newly created accounts must be confirmed (an email with confirmation instructions is sent after registration).

notification when a data export archive is ready to download (*new in 0.7.13*)

For single-user instance, it is possible to disable email sending with an empty ``EMAIL_URL`` (in this case, no need to start dramatiq workers).

For single-user instance, it is possible to disable email sending with an empty 

A `CLI <cli.html#ftcli-users-update>`__ is available to activate account, modify email and password and handle data export requests.

 is available to activate account, modify email and password and handle data export requests.

Password can be encoded if it contains special characters.
For instance with password ``passwordwith@and&and?``, the encoded password will be: ``passwordwith%40and%26and%3F``.

For instance with password 

Default tile server is now **OpenStreetMap**'s standard tile layer (if environment variables are not initialized).
The tile server can be changed by updating ``TILE_SERVER_URL`` and ``MAP_ATTRIBUTION`` variables (`list of tile servers <https://wiki.openstreetmap.org/wiki/Raster_tile_providers>`__).

’s standard tile layer (if environment variables are not initialized).
The tile server can be changed by updating 

`list of tile servers <https://wiki.openstreetmap.org/wiki/Raster_tile_providers>`__

``TILE_SERVER_URL=https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey=XXXX`` where **XXXX** is **ThunderForest** API key

``MAP_ATTRIBUTION=&copy; <a href="http://www.thunderforest.com/">Thunderforest</a>, &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors``

MAP_ATTRIBUTION=&copy; <a href="http://www.thunderforest.com/">Thunderforest</a>, &copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors

| Check the terms of service of tile provider for map attribution.

Check the terms of service of tile provider for map attribution.

Since the tile server can be used for static map generation, some servers require a subdomain.

For instance, to set OSM France tile server, the expected values are:

``MAP_ATTRIBUTION='fond de carte par <a href="http://www.openstreetmap.fr/mentions-legales/" target="_blank" rel="nofollow noopener">OpenStreetMap France</a>, sous&nbsp;<a href="http://creativecommons.org/licenses/by-sa/2.0/fr/" target="_blank" rel="nofollow noopener">licence CC BY-SA</a>'``

MAP_ATTRIBUTION='fond de carte par <a href="http://www.openstreetmap.fr/mentions-legales/" target="_blank" rel="nofollow noopener">OpenStreetMap France</a>, sous&nbsp;<a href="http://creativecommons.org/licenses/by-sa/2.0/fr/" target="_blank" rel="nofollow noopener">licence CC BY-SA</a>'

API rate limits are managed by `Flask-Limiter <https://flask-limiter.readthedocs.io/en/stable>`_, based on IP with fixed window strategy.

| If no Redis instance is available for rate limits, FitTrackee can still start.

If no Redis instance is available for rate limits, FitTrackee can still start.

All endpoints are subject to rate limits, except endpoints serving assets.

Limits can be modified by setting the environment variable ``API_RATE_LIMITS`` (see `Flask-Limiter documentation for notation <https://flask-limiter.readthedocs.io/en/stable/configuration.html#rate-limit-string-notation>`_).

`Flask-Limiter documentation for notation <https://flask-limiter.readthedocs.io/en/stable/configuration.html#rate-limit-string-notation>`_

**Flask-Limiter** provides a `Command Line Interface <https://flask-limiter.readthedocs.io/en/stable/cli.html>`_ for maintenance and diagnostic purposes.

`Command Line Interface <https://flask-limiter.readthedocs.io/en/stable/cli.html>`_

$ flask limiter
Usage: flask limiter [OPTIONS] COMMAND [ARGS]...

  Flask-Limiter maintenance & utility commands

Options:

Commands:
  clear   Clear limits for a specific key
  config  View the extension configuration
  limits  Enumerate details about all routes with rate limits

The following weather data providers are supported by **FitTrackee**:

`Visual Crossing <https://www.visualcrossing.com>`__ (**note**: historical data are provided on hourly period)

To configure a weather provider, set the following environment variables:

``WEATHER_API_KEY``: the key to the corresponding weather provider

**DarkSky** support is discontinued, since the service shut down on March 31, 2023.

 support is discontinued, since the service shut down on March 31, 2023.

| Note that **FitTrackee** is under heavy development, some features may be unstable.

Note that **FitTrackee** is under heavy development, some features may be unstable.

CREATE USER fittrackee WITH PASSWORD '<PASSWORD>';
CREATE SCHEMA fittrackee AUTHORIZATION fittrackee;
CREATE DATABASE fittrackee OWNER fittrackee;

| see PostgreSQL `documentation <https://www.postgresql.org/docs/15/ddl-schemas.html>`_ for schema and privileges.

see PostgreSQL `documentation <https://www.postgresql.org/docs/15/ddl-schemas.html>`_ for schema and privileges.

Initialize environment variables, see `Environment variables <installation.html#environment-variables>`__

`Environment variables <installation.html#environment-variables>`__

For instance, copy and update ``.env`` file from ``.env.example`` and source the file.

Start task queue workers if email sending is enabled, with flask-dramatiq CLI:

| To start application and workers with **systemd** service, see `Deployment <installation.html#deployment>`__

To start application and workers with **systemd** service, see `Deployment <installation.html#deployment>`__

To set admin rights to the newly created account, use the following command line:

| Since **FitTrackee** 0.2.1, Python packages installation needs Poetry.
| For more information, see `Poetry Documentation <https://python-poetry.org/docs/#installation>`__

Since **FitTrackee** 0.2.1, Python packages installation needs Poetry.

For more information, see `Poetry Documentation <https://python-poetry.org/docs/#installation>`__

`Poetry Documentation <https://python-poetry.org/docs/#installation>`__

| To keep virtualenv in project directory, update Poetry configuration.

.. code-block:: bash

    $ poetry config virtualenvs.in-project true

To keep virtualenv in project directory, update Poetry configuration.

$ git clone https://github.com/SamR1/FitTrackee.git
$ cd FitTrackee

Create **.env** from example and update it
(see `Environment variables <installation.html#environment-variables>`__).

Install Python virtualenv, Vue and all related packages and
initialize the database:

| Note that FitTrackee is under heavy development, some features may be unstable.

Note that FitTrackee is under heavy development, some features may be unstable.

Download the last release (for now, it is the release v0.7.26):

$ wget https://github.com/SamR1/FitTrackee/archive/v0.7.26.tar.gz
$ tar -xzf v0.7.26.tar.gz
$ mv FitTrackee-0.7.26 FitTrackee

Initialize the database (**after updating** ``db/create.sql`` **to change
database credentials**):

| Before upgrading, make a backup of all data:
| - database (with `pg_dump <https://www.postgresql.org/docs/11/app-pgdump.html>`__ for instance)
| - upload directory (see `Environment variables <installation.html#environment-variables>`__)

- database (with `pg_dump <https://www.postgresql.org/docs/11/app-pgdump.html>`__ for instance)

- upload directory (see `Environment variables <installation.html#environment-variables>`__)

For now, releases do not follow `semantic versioning <https://semver.org>`__). Any version may contain backward-incompatible changes.

Update environment variables if needed and source environment variables file

Restart the application and task queue workers (if email sending is enabled).

Update **.env** if needed (see `Environment variables <installation.html#environment-variables>`__).

Download the last release (for now, it is the release v0.7.26) and overwrite existing files:

$ cp -R FitTrackee-0.7.26/* FitTrackee/

There are several ways to start **FitTrackee** web application and task queue
library.
One way is to use a **systemd** services and **Nginx** to proxy pass to **Gunicorn**.

Examples (to adapt depending on your instance configuration and operating system):

[Unit]
Description=FitTrackee service
After=network.target
After=postgresql.service
After=redis.service
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=1
User=<USER>
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=fittrackee
Environment="APP_SECRET_KEY="
Environment="APP_LOG="
Environment="UPLOAD_FOLDER="
Environment="DATABASE_URL="
Environment="UI_URL="
Environment="EMAIL_URL="
Environment="SENDER_EMAIL="
Environment="REDIS_URL="
Environment="TILE_SERVER_URL="
Environment="STATICMAP_SUBDOMAINS="
Environment="MAP_ATTRIBUTION="
Environment="WEATHER_API_KEY="
WorkingDirectory=/home/<USER>/<FITTRACKEE DIRECTORY>
ExecStart=/home/<USER>/<FITTRACKEE DIRECTORY>/.venv/bin/gunicorn -b 127.0.0.1:5000 "fittrackee:create_app()" --error-logfile /home/<USER>/<FITTRACKEE DIRECTORY>/gunicorn.log

[Install]
WantedBy=multi-user.target

To handle large files, a higher value for `timeout <https://docs.gunicorn.org/en/stable/settings.html#timeout>`__ can be set.

More information on deployment with Gunicorn in its `documentation <https://docs.gunicorn.org/en/stable/deploy.html>`__.

[Unit]
Description=FitTrackee task queue service

[Service]
SyslogIdentifier=fittrackee_workers
Environment="FLASK_APP=fittrackee"
ExecStart=/home/<USER>/<FITTRACKEE DIRECTORY>/.venv/bin/flask worker --processes <NUMBER OF PROCESSES>

[Install]

server {
    listen 443 ssl http2;
    server_name example.com;
    ssl_certificate fullchain.pem;
    ssl_certificate_key privkey.pem;

    ## this parameter controls how large of a file can be
    ## uploaded, and defaults to 1MB. If you change the FitTrackee
    ## settings to allow larger uploads, you'll need to change this
    ## setting by uncommenting the line below and setting the size limit
    ## you want. Set to "0" to prevent nginx from checking the
    ## request body size at all
    # client_max_body_size 1m;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_redirect    default;
        proxy_set_header  Host $host;
        proxy_set_header  X-Real-IP $remote_addr;
        proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Host $server_name;
        proxy_set_header  X-Forwarded-Proto $scheme;
    }
}

server {
    listen 80;
        return 301 https://example.com$request_uri;
    }
}

If needed, update configuration to handle larger files (see `client_max_body_size <https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>`_).

For **evaluation** purposes, docker files are available, installing **FitTrackee** from **sources**.

$ cp .env.docker .env
$ make docker-build

Open http://localhost:8025 to access `MailHog interface <https://github.com/mailhog/MailHog>`_ (email testing tool)

$ make docker-lint-client  # run type check and lint on javascript files
$ make docker-test-client  # run unit tests on Client
$ make docker-lint-python  # run type check and lint on python files
$ make docker-test-python  # run unit tests on API

A package is available, see https://github.com/YunoHost-Apps/fittrackee_ynh.

FitTrackee provides a REST API (see `documentation <api/index.html>`__) whose
most endpoints require authentication/authorization.

To allow a third-party application to interact with API endpoints, an
`OAuth2 <https://datatracker.ietf.org/doc/html/rfc6749>`_ client can be created
in user settings ('apps' tab).

OAuth2 endpoints requiring authentication are not accessible by third-party
applications (`documentation <api/oauth2.html>`__), only by FitTrackee
client (first-party application).

applications (

FitTrackee supports only `Authorization Code <https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.1>`_
flow (with `PKCE <https://datatracker.ietf.org/doc/html/rfc7636>`_ support).
It allows to exchange an authorization code for an access token.
The client ID and secret must be sent in the POST body.
It is recommended to use PKCE to provide a better security.

 support).

``application:write``: grants write access to application configuration (only for users with administration rights),

: grants write access to application configuration (only for users with administration rights),

``workouts:read``: grants read access to workouts-related endpoints,

``workouts:write``: grants write access to workouts-related endpoints.

The user creates an App (client) on FitTrackee for a third-party application.

.. figure:: _images/fittrackee_screenshot-07.png
 :alt: OAuth2 client creation on FitTrackee

| After registration, the client id and secret are shown.
| They must be stored in the 3rd-party application by the user.

| The 3rd-party app needs to redirect to FitTrackee, in order for the user to authorize the 3rd-party app to access user data on FitTrackee.

.. figure:: _images/fittrackee_screenshot-08.png
 :alt: App authorization on FitTrackee

| The authorization URL is ``https://<FITTRACKEE_HOST>/profile/apps/authorize``.
| The required parameters are:

- ``client_id``: the client id displayed after registration
- ``response_type``:  ``code``, since FitTrackee only supports Authorization Code flow.
- ``scope``: scopes separated with space.

| and optional parameters:

- ``state``: unique value to prevent cross-site request forgery

| For PKCE, the following parameters are mandatory:

- ``code_challenge``: string generated from a code verifier
- ``code_challenge_method``: method used to create challenge, for instance "S256"

| Example for PKCE:
| ``https://<FITTRACKEE_HOST>/profile/apps/authorize?response_type=code&client_id=<CLIENT_ID>&scope=profile%3Aread+workouts%3Awrite&state=<STATE>&code_challenge=<CODE_CHALLENGE>&code_challenge_method=S256``

The 3rd-party app needs to redirect to FitTrackee, in order for the user to authorize the 3rd-party app to access user data on FitTrackee.

The authorization URL is ``https://<FITTRACKEE_HOST>/profile/apps/authorize``.

``response_type``:  ``code``, since FitTrackee only supports Authorization Code flow.

``code_challenge_method``: method used to create challenge, for instance "S256"

| After the authorization, FitTrackee redirects to the 3rd-party app, so the 3rd-party app can get the authorization code from the redirect URL and then fetches an access token with the client id and secret (endpoint `/api/oauth/token <https://samr1.github.io/FitTrackee/api/oauth2.html#post--api-oauth-token>`_).
| Example of a redirect URL:
| ``https://example.com/callback?code=<AUTHORIZATION_CODE>&state=<STATE>``

After the authorization, FitTrackee redirects to the 3rd-party app, so the 3rd-party app can get the authorization code from the redirect URL and then fetches an access token with the client id and secret (endpoint `/api/oauth/token <https://samr1.github.io/FitTrackee/api/oauth2.html#post--api-oauth-token>`_).

After the authorization, FitTrackee redirects to the 3rd-party app, so the 3rd-party app can get the authorization code from the redirect URL and then fetches an access token with the client id and secret (endpoint 

OAuth2 support is implemented with `Authlib <https://docs.authlib.org/en/latest/>`_ library.

| If FitTrackee is running behind a proxy, the ``X-Forwarded-Proto`` header must be set.
| For instance for ``nginx``:

.. code-block::

   proxy_set_header  X-Forwarded-Proto $scheme;

If FitTrackee is running behind a proxy, the ``X-Forwarded-Proto`` header must be set.

`OAuth 2.0 Simplified <https://www.oauth.com>`_ by `Aaron Parecki <https://aaronparecki.com>`_ (example for `authorization code flow with PKCE <https://www.oauth.com/oauth2-servers/server-side-apps/example-flow/>`_)

`authorization code flow with PKCE <https://www.oauth.com/oauth2-servers/server-side-apps/example-flow/>`_

`Web App Example of OAuth 2 web application flow <https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html>`_ with Requests-OAuthlib (python)

`Web App Example of OAuth 2 web application flow <https://requests-oauthlib.readthedocs.io/en/latest/examples/real_world_example.html>`_

`OAuth 2 Session <https://docs.authlib.org/en/latest/client/oauth2.html#oauth-2-session>`_ with Authlib (python)

`OAuth 2 Session <https://docs.authlib.org/en/latest/client/oauth2.html#oauth-2-session>`_

`Minimal example of an application interacting with FitTrackee <https://codeberg.org/SamR1/ft-oauth-client>`_ (python)

`Minimal example of an application interacting with FitTrackee <https://codeberg.org/SamR1/ft-oauth-client>`_

`strava-to-fittrackee <https://github.com/jat255/strava-to-fittrackee>`_ (import workouts from a Strava account)

`FitTrackee Uploader <https://github.com/ebrithiljonas/fittrackee-uploader>`_ (upload .gpx and .fit files)

`FitTrackee Uploader <https://github.com/ebrithiljonas/fittrackee-uploader>`_

Check the database URL in `environment variables <../installation.html#envvar-DATABASE_URL>`__ if the following error is displayed in **gunicorn** logs:

.. code::

   sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres

The variable ``DATABASE_URL`` must start with ``postgresql://` (engine URLs starting with ``postgres://`` are no longer supported).

`environment variables <../installation.html#envvar-DATABASE_URL>`__

sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:postgres

Check the email URL in `environment variables <../installation.html#envvar-EMAIL_URL>`__ if the following error is displayed in **gunicorn** logs:

.. code::

   fittrackee.emails.exceptions.InvalidEmailUrlScheme

A valid ``EMAIL_URL`` must be provided (see `emails <../installation.html#emails>`__).

`environment variables <../installation.html#envvar-EMAIL_URL>`__

`Map images are not displayed but map is shown in Workout detail`

Check the path in `environment variables <../installation.html#envvar-UPLOAD_FOLDER>`__. ``UPLOAD_FOLDER`` must be set with an absolute path.

`environment variables <../installation.html#envvar-UPLOAD_FOLDER>`__

Check ``client_max_body_size`` in **nginx** config. Increase the value to handle larger files (see **nginx** `documentation <https://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size>`_).

Increase **gunicorn** `timeout <https://docs.gunicorn.org/en/stable/settings.html#timeout>`__ value if the following error is displayed in gunicorn log: ``[CRITICAL] WORKER TIMEOUT``.