{
  "openapi": "3.1.1",
  "info": {
    "title": "Practice Log Report (Minutes, Streaks \u0026 Tempo Progress) API",
    "version": "1.0.0",
    "summary": "Turn a practice log into minutes per week, a 12-week heatmap, streaks and tempo progress per piece.",
    "description": "Every endpoint performs the same calculation as the human page and returns inputs, result, units, assumptions, sources, warnings and computedAt. MCP endpoint: /mcp."
  },
  "servers": [
    {
      "url": "https://tonegridpracticereport.usefulweb.app"
    }
  ],
  "paths": {
    "/api/v1/calculate": {
      "post": {
        "operationId": "calculate",
        "summary": "Turn a practice log into minutes per week, a 12-week heatmap, streaks and tempo progress per piece.",
        "description": "Paste a practice log as CSV (date, minutes, piece, tempo, target) or the journal file exported from the Tonegrid app, and get your total and weekly practice time, active days and streak, a calendar heatmap, and for every piece the tempo you started at, the latest and best tempo and how close you are to the target.\n\nMethodology:\n- Sessions on or before the report date within the chosen number of whole weeks (Monday to Sunday, the last week contains the report date) are counted. Minutes per day are summed; the heatmap shades a day by 1–24, 25–74, 75–149 or 150 and more minutes (the scale of Focus Room’s session log).\n- The streak counts consecutive days with practice ending on the report date, or on the day before when the report date has no session yet. Tempo progress uses the sessions with a tempo: first, latest and best tempo, and the latest tempo as a percentage of the piece’s target.\n\nYour log is only used for this report and is not stored.",
        "tags": [
          "apps"
        ],
        "x-mcp-tool": "tonegrid_practice_report",
        "x-owner-app": "tonegrid",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TonegridPracticeReportInput"
              },
              "examples": {
                "example1": {
                  "summary": "Five weeks of piano practice on two pieces",
                  "value": {
                    "journal": "date,minutes,piece,bpm,target_bpm,note\n2026-10-05,25,Bach Prelude in C,72,96,hands separately\n2026-10-06,20,Scales,100,120,\n2026-10-08,30,Bach Prelude in C,76,96,\n2026-10-11,15,Scales,104,120,\n2026-10-13,35,Bach Prelude in C,80,96,left hand cleaner\n2026-10-17,20,Scales,108,120,\n2026-10-20,40,Bach Prelude in C,84,96,\n2026-10-22,25,Scales,112,120,\n2026-10-27,30,Bach Prelude in C,88,96,\n2026-10-29,20,Scales,112,120,\n2026-11-02,45,Bach Prelude in C,90,96,run-through\n2026-11-04,25,Scales,116,120,\n2026-11-08,30,Bach Prelude in C,92,96,",
                    "weeks": 12
                  }
                },
                "example2": {
                  "summary": "Short log, report on a fixed date over 4 weeks",
                  "value": {
                    "journal": "date;minutes;piece;bpm\n2026-03-02;20;Etude 3;60\n2026-03-03;20;Etude 3;63\n2026-03-05;30;Etude 3;66\n2026-03-09;25;Etude 3;69",
                    "today": "2026-03-15",
                    "weeks": 4
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TonegridPracticeReportResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tool": {
      "get": {
        "operationId": "getToolDefinition",
        "summary": "Inputs, output schema and examples for this tool",
        "responses": {
          "200": {
            "description": "Tool definition.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 422 responses include per-field `errors`.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "TonegridPracticeReportInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "journal": {
            "title": "Practice log (CSV or Tonegrid journal JSON)",
            "description": "CSV header with at least date (YYYY-MM-DD) and minutes; optional piece, bpm, target_bpm and note. Comma, semicolon or tab separated.",
            "type": "string",
            "maxLength": 1000000,
            "default": "date,minutes,piece,bpm,target_bpm,note\n2026-10-05,25,Bach Prelude in C,72,96,hands separately\n2026-10-06,20,Scales,100,120,\n2026-10-08,30,Bach Prelude in C,76,96,\n2026-10-11,15,Scales,104,120,\n2026-10-13,35,Bach Prelude in C,80,96,left hand cleaner\n2026-10-17,20,Scales,108,120,\n2026-10-20,40,Bach Prelude in C,84,96,\n2026-10-22,25,Scales,112,120,\n2026-10-27,30,Bach Prelude in C,88,96,\n2026-10-29,20,Scales,112,120,\n2026-11-02,45,Bach Prelude in C,90,96,run-through\n2026-11-04,25,Scales,116,120,\n2026-11-08,30,Bach Prelude in C,92,96,"
          },
          "today": {
            "title": "Report date (optional)",
            "description": "Last day of the report. Empty = the date of the latest session.",
            "type": "string",
            "format": "date"
          },
          "weeks": {
            "title": "Weeks to show",
            "type": "integer",
            "minimum": 4,
            "maximum": 52,
            "default": 12
          }
        },
        "required": []
      },
      "TonegridPracticeReportResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "today",
              "weeks",
              "totalMinutes",
              "sessions",
              "activeDays",
              "streakDays",
              "thisWeekMinutes",
              "averagePerActiveDay",
              "bestDay",
              "pieces",
              "weekly"
            ],
            "properties": {
              "today": {
                "type": "string"
              },
              "weeks": {
                "type": "integer"
              },
              "totalMinutes": {
                "type": "integer"
              },
              "sessions": {
                "type": "integer"
              },
              "activeDays": {
                "type": "integer"
              },
              "streakDays": {
                "type": "integer",
                "description": "Days in a row with practice up to the report date (the report date itself may be empty)."
              },
              "thisWeekMinutes": {
                "type": "integer"
              },
              "averagePerActiveDay": {
                "type": "number"
              },
              "bestDay": {
                "type": [
                  "object",
                  "null"
                ],
                "additionalProperties": false,
                "required": [
                  "date",
                  "minutes"
                ],
                "properties": {
                  "date": {
                    "type": "string"
                  },
                  "minutes": {
                    "type": "integer"
                  }
                }
              },
              "pieces": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "name",
                    "minutes",
                    "sessions",
                    "firstDate",
                    "lastDate",
                    "firstBpm",
                    "latestBpm",
                    "bestBpm",
                    "targetBpm",
                    "progress"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "minutes": {
                      "type": "integer"
                    },
                    "sessions": {
                      "type": "integer"
                    },
                    "firstDate": {
                      "type": "string"
                    },
                    "lastDate": {
                      "type": "string"
                    },
                    "firstBpm": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "latestBpm": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "bestBpm": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "targetBpm": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "progress": {
                      "type": [
                        "number",
                        "null"
                      ],
                      "description": "Latest tempo in percent of the target."
                    }
                  }
                }
              },
              "weekly": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "weekStart",
                    "minutes"
                  ],
                  "properties": {
                    "weekStart": {
                      "type": "string",
                      "description": "Monday of the week."
                    },
                    "minutes": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}