Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ DATABASE_URL=postgresql+asyncpg://polar:polar@postgres:5432/polar
# JWT_SECRET=<secret-key>

# Sync settings
SYNC_INTERVAL_HOURS=1
SYNC_ON_STARTUP=true
SYNC_DAYS_LOOKBACK=30
SYNC_ENABLED=true # Enable/disable automatic background syncing
SYNC_INTERVAL_MINUTES=60 # Minutes between sync cycles (default: 60)
SYNC_ON_STARTUP=false # Run sync immediately on startup (default: false)
SYNC_MAX_USERS_PER_RUN=10 # Max users per sync cycle (default: 10)
SYNC_STAGGER_SECONDS=5 # Delay between user syncs (default: 5)
SYNC_DAYS_LOOKBACK=30 # Days of history to sync

# Logging
LOG_LEVEL=INFO
Expand Down
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,53 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

---

## [1.1.0] - 2026-01-13

### Added

**Automatic Background Sync**
- Smart sync scheduler with APScheduler for automatic background syncing
- Rate-limit-aware orchestration respecting Polar API limits (15-min and 24-hour windows)
- Priority queue system for efficient multi-user sync:
- CRITICAL: Users who haven't synced in 48h+ or have expiring tokens
- HIGH: Active users, hasn't synced in 12h+
- NORMAL: Regular users, hasn't synced in 24h+
- LOW: Dormant users, hasn't synced in 7d+
- Comprehensive `SyncLog` model for complete audit trail of every sync operation
- Consistent error classification with `SyncErrorType` enum covering:
- Authentication errors (TOKEN_EXPIRED, TOKEN_INVALID, TOKEN_REVOKED)
- Rate limiting (RATE_LIMITED_15M, RATE_LIMITED_24H)
- API errors (API_UNAVAILABLE, API_TIMEOUT, API_ERROR)
- Data errors (INVALID_RESPONSE, TRANSFORM_ERROR)
- Internal errors (DATABASE_ERROR, INTERNAL_ERROR)
- Post-sync analytics: Automatic baseline recalculation and pattern detection

**Admin Dashboard**
- Sync scheduler status section (running state, next run, 24h stats)
- Recent sync history table with status, duration, records synced
- Biosensing data counts (SpO2, ECG, Temperature)
- Analytics counts (Baselines, Patterns)

**Configuration**
- `SYNC_ENABLED`: Enable/disable automatic syncing (default: true)
- `SYNC_INTERVAL_MINUTES`: Sync cycle interval (default: 60)
- `SYNC_ON_STARTUP`: Run sync immediately on startup (default: false)
- `SYNC_MAX_USERS_PER_RUN`: Maximum users per sync cycle (default: 10)
- `SYNC_STAGGER_SECONDS`: Delay between user syncs (default: 5)

**Database**
- New `sync_logs` table with comprehensive fields for audit and debugging
- Composite indexes for efficient querying by user, status, and error type

### Fixed
- Alertness scale display corrected from /5 to /10 (Polar API uses 0-10 scale)

---

## [1.0.0] - 2025-01-13

First stable release of polar-flow-server - a self-hosted health analytics server for Polar devices.
Expand Down Expand Up @@ -93,6 +140,7 @@ First stable release of polar-flow-server - a self-hosted health analytics serve
- Database models for Polar data types
- Sync service foundation

[1.1.0]: https://github.com/StuMason/polar-flow-server/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/StuMason/polar-flow-server/compare/v0.2.0...v1.0.0
[0.2.0]: https://github.com/StuMason/polar-flow-server/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/StuMason/polar-flow-server/releases/tag/v0.1.0
195 changes: 195 additions & 0 deletions alembic/versions/f7g8h9i0j1k2_add_sync_logs_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"""Add sync_logs table for comprehensive sync audit trail.

Revision ID: f7g8h9i0j1k2
Revises: e6f7g8h9i0j1
Create Date: 2026-01-13 15:00:00.000000

"""

from collections.abc import Sequence

import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSON

from alembic import op

# revision identifiers, used by Alembic.
revision: str = "f7g8h9i0j1k2"
down_revision: str | None = "e6f7g8h9i0j1"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Create sync_logs table for complete sync audit trail."""
op.create_table(
"sync_logs",
# Primary key
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
# User identification
sa.Column(
"user_id",
sa.String(255),
nullable=False,
index=True,
comment="User being synced (Polar user ID or Laravel UUID)",
),
sa.Column(
"job_id",
sa.String(36),
nullable=False,
index=True,
comment="UUID for correlating logs across services",
),
# Timing
sa.Column(
"started_at",
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.func.now(),
index=True,
comment="When sync began",
),
sa.Column(
"completed_at",
sa.DateTime(timezone=True),
nullable=True,
comment="When sync finished (null if still running)",
),
sa.Column(
"duration_ms",
sa.Integer(),
nullable=True,
comment="Total sync duration in milliseconds",
),
# Status
sa.Column(
"status",
sa.String(20),
nullable=False,
server_default="started",
index=True,
comment="Status: started, success, partial, failed, skipped",
),
# Error tracking
sa.Column(
"error_type",
sa.String(50),
nullable=True,
index=True,
comment="Categorized error type (token_expired, rate_limited_15m, etc.)",
),
sa.Column(
"error_message",
sa.Text(),
nullable=True,
comment="Human-readable error description",
),
sa.Column(
"error_details",
JSON(),
nullable=True,
comment="Full error context as JSON",
),
# Results
sa.Column(
"records_synced",
JSON(),
nullable=True,
comment="Count of records synced per data type",
),
sa.Column(
"api_calls_made",
sa.Integer(),
nullable=False,
server_default="0",
comment="Total API calls made during sync",
),
# Rate limit tracking
sa.Column(
"rate_limit_remaining_15m",
sa.Integer(),
nullable=True,
comment="Remaining 15-min quota after sync",
),
sa.Column(
"rate_limit_remaining_24h",
sa.Integer(),
nullable=True,
comment="Remaining 24-hour quota after sync",
),
sa.Column(
"rate_limit_limit_15m",
sa.Integer(),
nullable=True,
comment="Max requests in 15-min window",
),
sa.Column(
"rate_limit_limit_24h",
sa.Integer(),
nullable=True,
comment="Max requests in 24-hour window",
),
# Analytics follow-up
sa.Column(
"baselines_recalculated",
sa.Boolean(),
nullable=False,
server_default="false",
comment="Whether baselines were updated after sync",
),
sa.Column(
"patterns_detected",
sa.Boolean(),
nullable=False,
server_default="false",
comment="Whether pattern detection ran after sync",
),
sa.Column(
"insights_generated",
sa.Boolean(),
nullable=False,
server_default="false",
comment="Whether insights were regenerated after sync",
),
# Context
sa.Column(
"trigger",
sa.String(20),
nullable=False,
server_default="manual",
comment="What triggered sync: scheduler, manual, webhook, startup",
),
sa.Column(
"priority",
sa.String(20),
nullable=True,
comment="Sync priority: critical, high, normal, low",
),
comment="Complete audit trail of every sync attempt",
)

# Composite indexes for common queries
op.create_index(
"ix_sync_logs_user_started",
"sync_logs",
["user_id", "started_at"],
)
op.create_index(
"ix_sync_logs_status_started",
"sync_logs",
["status", "started_at"],
)
op.create_index(
"ix_sync_logs_error_type_started",
"sync_logs",
["error_type", "started_at"],
)


def downgrade() -> None:
"""Drop sync_logs table and indexes."""
op.drop_index("ix_sync_logs_error_type_started", table_name="sync_logs")
op.drop_index("ix_sync_logs_status_started", table_name="sync_logs")
op.drop_index("ix_sync_logs_user_started", table_name="sync_logs")
op.drop_table("sync_logs")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "polar-flow-server"
version = "1.0.0"
version = "1.1.0"
description = "Self-hosted health analytics server for Polar devices"
readme = "README.md"
authors = [
Expand Down
Loading