feat: Implement single sync job management and progress tracking

This commit is contained in:
2025-10-11 18:36:19 -07:00
parent 3819e4f5e2
commit 723ca04aa8
51 changed files with 1625 additions and 596 deletions

View File

@@ -0,0 +1,34 @@
# Specification Quality Checklist: Simplify Sync Job Management with Progress Tracking
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: Saturday, October 11, 2025
**Feature**: [Link to spec.md]
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`

View File

@@ -0,0 +1,207 @@
{
"openapi": "3.0.0",
"info": {
"title": "Garmin Sync API with Progress Tracking",
"version": "1.0.0",
"description": "API for initiating and tracking the status of Garmin data synchronization for a single user."
},
"servers": [
{
"url": "/api/v1"
}
],
"paths": {
"/garmin/activities": {
"post": {
"summary": "Initiate Garmin Activity Synchronization",
"operationId": "syncGarminActivities",
"tags": ["Garmin Sync"],
"responses": {
"200": {
"description": "Activity synchronization initiated successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Activity synchronization initiated successfully."
}
}
}
}
}
},
"409": {
"description": "Conflict: A sync is already in progress.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"detail": {
"type": "string",
"example": "A synchronization is already in progress. Please wait or check status."
}
}
}
}
}
}
}
}
},
"/garmin/workouts": {
"post": {
"summary": "Initiate Garmin Workout Synchronization",
"operationId": "syncGarminWorkouts",
"tags": ["Garmin Sync"],
"responses": {
"200": {
"description": "Workout synchronization initiated successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Workout synchronization initiated successfully."
}
}
}
}
}
},
"409": {
"description": "Conflict: A sync is already in progress.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"detail": {
"type": "string",
"example": "A synchronization is already in progress. Please wait or check status."
}
}
}
}
}
}
}
}
},
"/garmin/health": {
"post": {
"summary": "Initiate Garmin Health Metrics Synchronization",
"operationId": "syncGarminHealth",
"tags": ["Garmin Sync"],
"responses": {
"200": {
"description": "Health metrics synchronization initiated successfully.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Health metrics synchronization initiated successfully."
}
}
}
}
}
},
"409": {
"description": "Conflict: A sync is already in progress.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"detail": {
"type": "string",
"example": "A synchronization is already in progress. Please wait or check status."
}
}
}
}
}
}
}
}
},
"/garmin/sync/status": {
"get": {
"summary": "Get Current Garmin Sync Status",
"operationId": "getGarminSyncStatus",
"tags": ["Garmin Sync"],
"responses": {
"200": {
"description": "Current status of the Garmin synchronization job.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SyncJob"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"SyncJob": {
"type": "object",
"properties": {
"status": {
"type": "string",
"description": "Current state of the sync.",
"enum": ["idle", "in_progress", "completed", "failed"],
"example": "in_progress"
},
"progress": {
"type": "number",
"format": "float",
"description": "Completion percentage (0.0 to 1.0).",
"minimum": 0.0,
"maximum": 1.0,
"example": 0.5
},
"start_time": {
"type": "string",
"format": "date-time",
"description": "Timestamp when the sync operation began.",
"example": "2025-10-11T10:00:00Z"
},
"end_time": {
"type": "string",
"format": "date-time",
"nullable": true,
"description": "Timestamp when the sync operation concluded (either completed or failed).",
"example": "2025-10-11T10:15:00Z"
},
"error_message": {
"type": "string",
"nullable": true,
"description": "Details if the sync operation failed.",
"example": "Failed to connect to Garmin API."
},
"job_type": {
"type": "string",
"nullable": true,
"description": "Type of data being synchronized.",
"enum": ["activities", "health", "workouts"],
"example": "activities"
}
},
"required": ["status", "progress", "start_time"]
}
}
}
}

View File

@@ -0,0 +1,14 @@
# Data Model: Sync Job Management with Progress Tracking
## Entity: SyncJob
Represents the state and progress of the single active synchronization process.
### Attributes:
* `status` (string): Indicates the current state of the sync. Possible values: "idle", "in_progress", "completed", "failed".
* `progress` (float): Completion percentage, ranging from 0.0 to 1.0.
* `start_time` (datetime): Timestamp when the sync operation began.
* `end_time` (datetime, optional): Timestamp when the sync operation concluded (either completed or failed).
* `error_message` (string, optional): Contains details if the sync operation failed.
* `job_type` (string, optional): Indicates the type of data being synchronized. Possible values: "activities", "health", "workouts".

View File

@@ -0,0 +1,65 @@
# Implementation Plan: Simplify Sync Job Management with Progress Tracking
**Branch**: `004-home-sstent-projects` | **Date**: Saturday, October 11, 2025 | **Spec**: /home/sstent/Projects/FitTrack_GarminSync/specs/004-home-sstent-projects/spec.md
**Input**: Feature specification from `/specs/004-home-sstent-projects/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/commands/plan.md` for the execution workflow.
## Summary
The feature aims to simplify sync job management for a single-user system by allowing only one sync job at a time and providing progress tracking via a polling API. This involves reintroducing a simplified `SyncJob` model and a `CurrentSyncJobManager` to manage its state, modifying existing sync services to update this state, and creating a new API endpoint (`GET /garmin/sync/status`) for users to monitor progress.
## Technical Context
**Language/Version**: Python 3.13
**Primary Dependencies**: FastAPI, Pydantic, `garth`, `garminconnect`, `httpx`
**Storage**: In-memory for `CurrentSyncJobManager`
**Testing**: Pytest
**Target Platform**: Linux server
**Project Type**: Web application (backend)
**Performance Goals**:
- Users can successfully initiate any sync operation and receive an initial confirmation within 2 seconds.
- The `GET /garmin/sync/status` API endpoint responds with the current sync status within 500ms, even under moderate load (e.g., 10 requests per second).
- Sync progress updates are reflected in the `GET /garmin/sync/status` API with a granularity that provides meaningful feedback to the user (e.g., progress updates at least every 10% completion or every 30 seconds for long syncs).
**Constraints**: Single-user system, only one sync job active at a time.
**Scale/Scope**: Single user, managing personal Garmin data synchronization.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
All gates pass. No violations.
## Project Structure
### Documentation (this feature)
```
specs/004-home-sstent-projects/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
```
### Source Code (repository root)
```
backend/
├── src/
│ ├── models/ # For SyncJob model
│ ├── services/ # For CurrentSyncJobManager and sync logic
│ └── api/ # For garmin_sync API endpoints
└── tests/
├── unit/
├── integration/
└── api/
```
**Structure Decision**: The existing "Option 2: Web application" structure is appropriate and will be used. New files will be added to `backend/src/models/` (for `SyncJob`), `backend/src/services/` (for `CurrentSyncJobManager`), and `backend/src/api/` (for the new status endpoint and modifications to existing sync endpoints).
## Complexity Tracking
N/A

View File

@@ -0,0 +1,109 @@
# Quickstart: Garmin Sync with Progress Tracking
This guide provides a quick overview of how to use the simplified Garmin sync functionality with progress tracking.
## 1. Initiate a Synchronization
To start a synchronization for activities, workouts, or health metrics, send a POST request to the respective endpoint. The system will only allow one sync to run at a time.
### Sync Activities
```bash
curl -X POST "http://localhost:8000/api/v1/garmin/activities" \
-H "accept: application/json"
```
### Sync Workouts
```bash
curl -X POST "http://localhost:8000/api/v1/garmin/workouts" \
-H "accept: application/json"
```
### Sync Health Metrics
```bash
curl -X POST "http://localhost:8000/api/v1/garmin/health" \
-H "accept: application/json"
```
**Expected Response (Success)**:
```json
{
"message": "Activity synchronization initiated successfully."
}
```
**Expected Response (Conflict - if another sync is in progress)**:
```json
{
"detail": "A synchronization is already in progress. Please wait or check status."
}
```
## 2. Poll for Sync Status
To check the current status and progress of the active synchronization job, send a GET request to the status endpoint. This endpoint will always return the status of the single active sync.
```bash
curl -X GET "http://localhost:8000/api/v1/garmin/sync/status" \
-H "accept: application/json"
```
**Expected Response (Sync in Progress)**:
```json
{
"status": "in_progress",
"progress": 0.5,
"start_time": "2025-10-11T10:00:00Z",
"end_time": null,
"error_message": null,
"job_type": "activities"
}
```
**Expected Response (Sync Completed)**:
```json
{
"status": "completed",
"progress": 1.0,
"start_time": "2025-10-11T10:00:00Z",
"end_time": "2025-10-11T10:15:00Z",
"error_message": null,
"job_type": "activities"
}
```
**Expected Response (Sync Failed)**:
```json
{
"status": "failed",
"progress": 0.75,
"start_time": "2025-10-11T10:00:00Z",
"end_time": "2025-10-11T10:10:00Z",
"error_message": "Failed to connect to Garmin API.",
"job_type": "activities"
}
```
**Expected Response (No Sync Active)**:
```json
{
"status": "idle",
"progress": 0.0,
"start_time": "1970-01-01T00:00:00Z",
"end_time": null,
"error_message": null,
"job_type": null
}
```
## 3. Handling Concurrent Sync Attempts
If you attempt to initiate a new sync while another is already in progress, the system will return a `409 Conflict` status code with an informative message. You must wait for the current sync to complete or fail before starting a new one.

View File

@@ -0,0 +1,66 @@
# Feature Specification: Simplify Sync Job Management with Progress Tracking
**Feature Branch**: `004-home-sstent-projects`
**Created**: Saturday, October 11, 2025
**Status**: Draft
**Input**: User description: "since this is a single user system can we simplify the sync jobs even further - i.e. just one sync job at a time, no queue, no job id? can we add progress tracking for the sync - with an API I can poll for updates"
## User Scenarios & Testing
### User Story 1 - Initiate a Sync and Monitor Progress (Priority: P1)
As a user, I want to initiate a data synchronization (activities, health, or workouts) and be able to monitor its progress in real-time, so I know the system is working and when it's complete or if it has failed.
**Why this priority**: This is core functionality for providing user feedback and ensuring a transparent experience during data synchronization, which is a primary function of the system.
**Independent Test**: This can be fully tested by initiating any type of sync (e.g., activity sync) and repeatedly querying the status API until the sync completes or fails. It delivers immediate value by informing the user about the state of their data synchronization.
**Acceptance Scenarios**:
1. **Given** no sync is currently in progress, **When** the user initiates an activity sync via the API, **Then** the system returns an immediate success confirmation, and subsequent calls to the `/garmin/sync/status` API show the sync as "in_progress" with increasing `progress` and `job_type` set to "activities".
2. **Given** a sync is currently in progress, **When** the user attempts to initiate another sync (e.g., health sync), **Then** the system returns a "409 Conflict" error, indicating that another sync is already running.
3. **Given** a sync is in progress, **When** the user repeatedly polls the `/garmin/sync/status` API, **Then** the API consistently returns the current `status` as "in_progress", the `progress` value reflecting the ongoing work, and the correct `job_type`.
4. **Given** a sync completes successfully, **When** the user polls the `/garmin/sync/status` API, **Then** the API returns the `status` as "completed", `progress` as 1.0, and the `end_time` is set.
5. **Given** a sync encounters an error and fails, **When** the user polls the `/garmin/sync/status` API, **Then** the API returns the `status` as "failed", `progress` reflecting the point of failure, an `error_message`, and the `end_time` is set.
6. **Given** no sync is active, **When** the user polls the `/garmin/sync/status` API, **Then** the API returns the `status` as "idle" and `progress` as 0.0.
---
### Edge Cases
- **Server Restart During Sync**: If the server restarts while a sync is in progress, the sync state will be lost, and the system will revert to an "idle" state upon restart. The user would need to re-initiate the sync.
- **Network Interruption During Sync**: If a network interruption occurs during a sync operation, the sync will likely fail and report an appropriate error message via the status API.
- **Rapid Polling**: The system should gracefully handle frequent polling requests to the status API without performance degradation.
## Requirements
### Functional Requirements
- **FR-001**: System MUST allow initiation of a single sync operation (activities, health, or workouts) at a time.
- **FR-002**: System MUST prevent initiation of a new sync if one is already in progress, returning a "409 Conflict" error.
- **FR-003**: System MUST provide a `GET /garmin/sync/status` API endpoint to retrieve the current status of the active sync job.
- **FR-004**: The `GET /garmin/sync/status` API MUST return a `SyncJob` object containing `status` (idle, in_progress, completed, failed), `progress` (0.0-1.0), `job_type` (activities, health, workouts), `start_time`, `end_time` (optional), and `error_message` (optional).
- **FR-005**: System MUST update the `progress` of the active sync job as it proceeds through its various stages.
- **FR-006**: System MUST mark the active sync job as "completed" and set its `end_time` upon successful completion.
- **FR-007**: System MUST mark the active sync job as "failed", record an `error_message`, and set its `end_time` upon failure.
- **FR-008**: System MUST initialize the `SyncJob` status to "idle" and `progress` to 0.0 when no sync is active.
### Key Entities
- **SyncJob**: Represents the state and progress of the single active synchronization process.
* `status`: String indicating the current state of the sync (e.g., "idle", "in_progress", "completed", "failed").
* `progress`: Float value from 0.0 to 1.0 representing the completion percentage of the sync.
* `start_time`: Datetime object indicating when the sync operation began.
* `end_time`: Optional Datetime object indicating when the sync operation concluded (either completed or failed).
* `error_message`: Optional string containing details if the sync operation failed.
* `job_type`: String indicating the type of data being synchronized (e.g., "activities", "health", "workouts").
## Success Criteria
### Measurable Outcomes
- **SC-001**: Users can successfully initiate any sync operation and receive an initial confirmation within 2 seconds.
- **SC-002**: The `GET /garmin/sync/status` API endpoint responds with the current sync status within 500ms, even under moderate load (e.g., 10 requests per second).
- **SC-003**: Sync progress updates are reflected in the `GET /garmin/sync/status` API with a granularity that provides meaningful feedback to the user (e.g., progress updates at least every 10% completion or every 30 seconds for long-running syncs).
- **SC-004**: The system accurately reports sync completion or failure, including relevant error messages, for 100% of sync attempts.
- **SC-005**: The system successfully prevents concurrent sync initiations, returning a 409 Conflict error in 100% of such attempts.

View File

@@ -0,0 +1,104 @@
# Tasks: Simplify Sync Job Management with Progress Tracking
**Feature Branch**: `004-home-sstent-projects` | **Date**: Saturday, October 11, 2025 | **Spec**: /home/sstent/Projects/FitTrack_GarminSync/specs/004-home-sstent-projects/spec.md
## Phase 1: Setup Tasks
*(No specific setup tasks identified beyond the existing project structure. Foundational components will be created in Phase 2.)*
## Phase 2: Foundational Tasks
These tasks establish the core components required for the single sync job management and progress tracking.
- [X] **T001**: Create `backend/src/models/sync_job.py` to define the `SyncJob` Pydantic model with `status`, `progress`, `start_time`, `end_time`, `error_message`, and `job_type` attributes. [P]
- [X] **T002**: Create `backend/src/services/sync_manager.py` to implement the `CurrentSyncJobManager` (singleton) with methods `start_sync`, `update_progress`, `complete_sync`, `fail_sync`, `get_current_sync_status`, and `is_sync_active`. [P]
- [X] **T003**: Update `backend/src/dependencies.py` to remove references to the old `job_store` and `SyncStatusService`. [P]
- [X] **T004**: Delete `backend/src/jobs.py`. [P]
- [X] **T005**: Delete `backend/src/services/sync_status_service.py`. [P]
## Phase 3: User Story 1 - Initiate a Sync and Monitor Progress (P1)
**Story Goal**: As a user, I want to initiate a data synchronization (activities, health, or workouts) and be able to monitor its progress in real-time, so I know the system is working and when it's complete or if it has failed.
**Independent Test Criteria**: This can be fully tested by initiating any type of sync (e.g., activity sync) and repeatedly querying the status API until the sync completes or fails. It delivers immediate value by informing the user about the state of their data synchronization.
- [X] **T006** [US1]: Modify `backend/src/api/garmin_sync.py` to import `CurrentSyncJobManager` and `SyncJob` from the new modules. [P]
- [X] **T007** [US1]: Modify `backend/src/api/garmin_sync.py` to remove the old `SyncJob` import and the `/status/{job_id}` endpoint. [P]
- [X] **T008** [US1]: Modify `backend/src/api/garmin_sync.py` to update the `POST /garmin/activities` endpoint:
* Implement the single-sync enforcement using `CurrentSyncJobManager.is_sync_active()`.
* Call `CurrentSyncJobManager.start_sync(job_type="activities")`.
* Pass the `SyncJob` instance (or a reference to the `CurrentSyncJobManager`) to `garmin_activity_service.sync_activities_in_background`.
* Change the `response_model` to a simple success message. [P]
- [X] **T009** [US1]: Modify `backend/src/api/garmin_sync.py` to update the `POST /garmin/workouts` endpoint:
* Implement the single-sync enforcement using `CurrentSyncJobManager.is_sync_active()`.
* Call `CurrentSyncJobManager.start_sync(job_type="workouts")`.
* Pass the `SyncJob` instance (or a reference to the `CurrentSyncJobManager`) to `garmin_workout_service.upload_workout_in_background`.
* Change the `response_model` to a simple success message. [P]
- [X] **T010** [US1]: Modify `backend/src/api/garmin_sync.py` to update the `POST /garmin/health` endpoint:
* Implement the single-sync enforcement using `CurrentSyncJobManager.is_sync_active()`.
* Call `CurrentSyncJobManager.start_sync(job_type="health")`.
* Pass the `SyncJob` instance (or a reference to the `CurrentSyncJobManager`) to `garmin_health_service.sync_health_metrics_in_background`.
* Change the `response_model` to a simple success message. [P]
- [X] **T011** [US1]: Add `GET /garmin/sync/status` API endpoint to `backend/src/api/garmin_sync.py` that returns the current `SyncJob` status from `CurrentSyncJobManager.get_current_sync_status()`. [P]
- [X] **T012** [US1]: Modify `backend/src/services/garmin_activity_service.py`:
* Remove `job_id` parameter from `sync_activities_in_background`.
* Accept `SyncJob` instance (or `CurrentSyncJobManager`) as a parameter.
* Replace `job_store.update_job` calls with `CurrentSyncJobManager.update_progress`, `complete_sync`, and `fail_sync`. [P]
- [X] **T013** [US1]: Modify `backend/src/services/garmin_workout_service.py`:
* Remove `job_id` parameter from `upload_workout_in_background`.
* Accept `SyncJob` instance (or `CurrentSyncJobManager`) as a parameter.
* Replace `job_store.update_job` calls with `CurrentSyncJobManager.update_progress`, `complete_sync`, and `fail_sync`. [P]
- [X] **T014** [US1]: Modify `backend/src/services/garmin_health_service.py`:
* Remove `job_id` parameter from `sync_health_metrics_in_background`.
* Accept `SyncJob` instance (or `CurrentSyncJobManager`) as a parameter.
* Replace `job_store.update_job` calls with `CurrentSyncJobManager.update_progress`, `complete_sync`, and `fail_sync`. [P]
- [X] **T015** [US1]: Update `backend/src/main.py` to ensure `CurrentSyncJobManager` is properly initialized and accessible (e.g., as a global or via dependency injection if preferred). [P]
## Phase 4: Polish & Cross-Cutting Concerns
- [X] **T016**: Add unit tests for `backend/src/models/sync_job.py`. [P]
- [X] **T017**: Add unit tests for `backend/src/services/sync_manager.py`. [P]
- [X] **T018**: Add API integration tests for `GET /garmin/sync/status` endpoint. [P]
- [X] **T019**: Add API integration tests for `POST /garmin/activities` (success and conflict scenarios). [P]
- [X] **T020**: Add API integration tests for `POST /garmin/workouts` (success and conflict scenarios). [P]
- [X] **T021**: Add API integration tests for `POST /garmin/health` (success and conflict scenarios). [P]
- [X] **T022**: Ensure all new and modified code adheres to Python 3.13 style guidelines (type hints, Black formatting, Flake8 linting). [P]
## Dependencies
```mermaid
graph TD
A[T001: Create SyncJob Model] --> B(T002: Create SyncManager Service)
A --> C(T003: Update dependencies.py)
A --> D(T004: Delete jobs.py)
A --> E(T005: Delete sync_status_service.py)
B --> F(T006: Modify garmin_sync.py imports)
B --> G(T012: Modify garmin_activity_service.py)
B --> H(T013: Modify garmin_workout_service.py)
B --> I(T014: Modify garmin_health_service.py)
F --> J(T007: Modify garmin_sync.py remove old endpoint)
J --> K(T008: Modify POST /garmin/activities)
J --> L(T009: Modify POST /garmin/workouts)
J --> M(T010: Modify POST /garmin/health)
J --> N(T011: Add GET /garmin/sync/status)
K --> O(T015: Update main.py)
L --> O
M --> O
N --> O
O --> P(T016: Unit tests for SyncJob)
O --> Q(T017: Unit tests for SyncManager)
O --> R(T018: API tests for GET /garmin/sync/status)
O --> S(T019: API tests for POST /garmin/activities)
O --> T(T020: API tests for POST /garmin/workouts)
O --> U(T021: API tests for POST /garmin/health)
O --> V(T022: Code style adherence)
```
## Parallel Execution Examples
* **After T005 (Foundational Tasks)**: T006, T007, T012, T013, T014 can be worked on in parallel.
* **After T015 (User Story 1 Implementation)**: T016, T017, T018, T019, T020, T021, T022 (all testing and polish tasks) can be worked on in parallel.
## Implementation Strategy
This feature will be implemented using an MVP-first approach, focusing on delivering the core functionality of User Story 1. The tasks are ordered to build foundational components first, then implement the core user story functionality, and finally add comprehensive testing and polish. Each user story phase is designed to be an independently testable increment.