Skip to content

integration with hevy#670

Merged
CodeWithCJ merged 9 commits intomainfrom
dev
Feb 15, 2026
Merged

integration with hevy#670
CodeWithCJ merged 9 commits intomainfrom
dev

Conversation

@CodeWithCJ
Copy link
Owner

No description provided.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @CodeWithCJ, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the application's external integration capabilities by adding support for Hevy, a popular workout tracking app. Users can now seamlessly bring their workout data, including detailed set information and RPE, into Sparky Fitness. This integration provides a more comprehensive view of a user's fitness journey by consolidating data from multiple sources, improving the overall utility and data richness of the platform.

Highlights

  • Hevy Integration: Introduced a new integration with the Hevy workout tracking application, allowing users to connect their Hevy accounts and synchronize workout data.
  • RPE Support: Added support for Rate of Perceived Exertion (RPE) to exercise entries, including database schema updates, API changes, and frontend display.
  • API Endpoints & Services: Developed new backend API endpoints and services for connecting, syncing (full and incremental), and disconnecting Hevy accounts, along with data processing logic.
  • User Interface Updates: Implemented frontend UI components for managing Hevy integration settings, including API key input, full sync options, and manual sync/disconnect actions.
Changelog
  • SparkyFitnessFrontend/src/api/Integrations/integrations.ts
    • Added API functions for connecting, syncing, and disconnecting Hevy accounts.
  • SparkyFitnessFrontend/src/pages/Diary/ExerciseEntryDisplay.tsx
    • Implemented display of RPE (Rate of Perceived Exertion) for exercise sets.
  • SparkyFitnessFrontend/src/pages/Settings/AddExternalProviderForm.tsx
    • Introduced UI and logic for adding Hevy as an external provider, including API key input and full sync option.
    • Added validation for Hevy API key presence.
    • Integrated logic to trigger an initial Hevy data sync upon connection.
  • SparkyFitnessFrontend/src/pages/Settings/ExternalProviderList.tsx
    • Updated component to include UI for managing Hevy integration, such as manual sync (recent or full history) and disconnect actions.
    • Added display for the Hevy API key in edit mode.
  • SparkyFitnessFrontend/src/pages/Settings/ExternalProviderSettings.tsx
    • Integrated Hevy service functions for sync and disconnect.
    • Updated ExternalDataProvider interface to include hevy_last_sync_at and hevy_connect_status fields.
    • Added logic to fetch and display Hevy connection status.
    • Included 'Hevy' in the list of available external providers and passed necessary handlers to ExternalProviderList.
  • SparkyFitnessFrontend/src/types/workout.ts
    • Extended WorkoutPresetSet interface to include an optional rpe property.
  • SparkyFitnessFrontend/tsconfig.app.tsbuildinfo
    • Updated TypeScript build information file paths, likely due to a build system change.
  • SparkyFitnessServer/SparkyFitnessServer.js
    • Registered new API routes for Hevy integration.
  • SparkyFitnessServer/db/migrations/20260215144500_add_hevy_provider_type.sql
    • Added a new migration to insert 'hevy' into the external_provider_types table.
    • Added an rpe column (numeric(3,1)) to the exercise_entry_sets table.
  • SparkyFitnessServer/integrations/hevy/hevyDataProcessor.js
    • Implemented data processing logic for Hevy user information and workouts.
    • Handled the creation of new exercises and exercise entries from synced Hevy data.
    • Mapped Hevy set types to internal Sparky Fitness set types.
  • SparkyFitnessServer/integrations/hevy/hevyService.js
    • Developed the Hevy service to handle API calls to Hevy for user info, workouts, and exercise templates.
    • Implemented syncHevyData for both full and incremental synchronization, with support for local mock data.
    • Provided functionality to get Hevy connection status and disconnect the integration.
  • SparkyFitnessServer/models/exerciseEntry.js
    • Modified exercise entry model and related database functions to store and retrieve RPE (Rate of Perceived Exertion) for exercise sets.
  • SparkyFitnessServer/routes/hevyRoutes.js
    • Created API routes for connecting, syncing, disconnecting, and checking the status of Hevy integration.
    • Implemented API key encryption and storage for Hevy credentials.
  • docker/.env.example
    • Updated the example environment file to include a setting for Hevy local data source.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces Hevy integration, allowing users to sync workouts and body measurements via API keys. It also adds RPE (Rate of Perceived Exertion) tracking to exercise sets. While the integration is comprehensive, there are several critical issues regarding API endpoint paths in the frontend, duration unit mismatches (seconds vs. minutes), and logic that causes exercise duration inflation in reports. These should be addressed to ensure data accuracy and functional connectivity.

export const connectHevyAccount = async (data: {
apiKey: string;
}): Promise<void> => {
return apiCall('/hevy/connect', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The API path is missing the /integrations prefix. The backend routes for Hevy are mounted at /api/integrations/hevy in SparkyFitnessServer.js. Calling /hevy/connect will result in a 404 error.

Suggested change
return apiCall('/hevy/connect', {
return apiCall('/integrations/hevy/connect', {

export const syncHevyData = async (
fullSync: boolean = false
): Promise<void> => {
return apiCall(`/hevy/sync${fullSync ? '?fullSync=true' : ''}`, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The API path is missing the /integrations prefix. This request will fail with a 404.

Suggested change
return apiCall(`/hevy/sync${fullSync ? '?fullSync=true' : ''}`, {
return apiCall(`/integrations/hevy/sync${fullSync ? '?fullSync=true' : ''}`, {

};

export const disconnectHevyAccount = async (): Promise<void> => {
return apiCall('/hevy/disconnect', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The API path is missing the /integrations prefix. This request will fail with a 404.

Suggested change
return apiCall('/hevy/disconnect', {
return apiCall('/integrations/hevy/disconnect', {

if (hevyProviders.length > 0) {
for (const provider of hevyProviders) {
try {
const hevyStatus = await apiCall(`/hevy/status`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The API path is missing the /integrations prefix. This will cause the status check to fail with a 404.

Suggested change
const hevyStatus = await apiCall(`/hevy/status`);
const hevyStatus = await apiCall(`/integrations/hevy/status`);

set_type: mapSetType(set.type),
weight: set.weight_kg,
reps: set.reps,
duration: set.duration_seconds,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Hevy provides duration_seconds, but the Sparky Fitness system (as seen in WorkoutPresetSet) expects the duration in minutes. Passing raw seconds will cause durations to be displayed 60x larger than they are (e.g., a 60-second set will appear as 60 minutes).

Suggested change
duration: set.duration_seconds,
duration: set.duration_seconds ? Math.round(set.duration_seconds / 60) : 0,

const entryData = {
exercise_id: exercise.id,
entry_date: startTime.toISOString().split('T')[0],
duration_minutes: durationMinutes, // Note: Hevy provides total workout duration, not per-exercise
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Setting duration_minutes to the total workout duration for every exercise entry will cause significant data inflation in daily reports. For example, a 60-minute workout with 5 exercises would be recorded as 300 minutes of total exercise. It is better to set this to 0 (relying on set-level duration) or calculate the sum of set durations for this specific exercise.

Suggested change
duration_minutes: durationMinutes, // Note: Hevy provides total workout duration, not per-exercise
duration_minutes: Math.round(hevyExercise.sets.reduce((sum, s) => sum + (s.duration_seconds || 0), 0) / 60),

try {
const userId = req.userId;
const createdByUserId = req.userId; // Currently same as userId
const result = await hevyService.syncHevyData(userId, createdByUserId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fullSync query parameter from the request is ignored. This prevents the 'Sync All History' functionality from working as intended.

Suggested change
const result = await hevyService.syncHevyData(userId, createdByUserId);
const { fullSync } = req.query;
const result = await hevyService.syncHevyData(userId, createdByUserId, fullSync === 'true');

@Sim-sat
Copy link
Contributor

Sim-sat commented Feb 15, 2026

@CodeWithCJ the connectHeavyAccount function is not used

@CodeWithCJ
Copy link
Owner Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant new feature: integration with the Hevy workout tracking app. The changes are comprehensive, spanning the frontend for configuration and display, the backend for API communication and data processing, and the database with necessary schema updates. The implementation is well-structured, including new API routes, services, and data processors for Hevy. Additionally, the concept of Rate of Perceived Exertion (RPE) has been added to exercise sets throughout the application, which is a valuable enhancement for strength training tracking. Overall, this is a solid contribution. I've included a few suggestions to improve robustness and code clarity.

Comment on lines 259 to 262
if (oldestTime < sevenDaysAgo) {
log('debug', `Stopping incremental sync: workout date ${oldestTime.toISOString()} is older than 7 days.`);
hasMore = false;
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The incremental sync logic assumes that workouts from the Hevy API are always sorted in descending chronological order. The comment on line 255, "Hevy workouts are usually sorted by date desc," suggests this order is not guaranteed. If the API response is not strictly sorted, this logic could cause the sync to terminate prematurely, potentially missing recent workouts that appear on later pages.

To improve robustness, consider fetching pages until a workout's start_time is older than the sevenDaysAgo threshold, but ensure you still process all workouts on that final page to catch any out-of-order entries. An even safer, though less efficient, approach would be to continue paginating as long as any workout on the page is within the 7-day window.

reps: set.reps,
duration: set.duration_seconds ? set.duration_seconds / 60 : null,
rpe: set.rpe, // This requires the migration to be applied
notes: set.rpe ? `RPE: ${set.rpe}` : null, // Fallback: store RPE in notes if column doesn't exist yet
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The rpe value is being stored in its dedicated column, but this line also adds it to the notes field as a fallback. Since the database migration adds the rpe column and the model has been updated to use it, storing it in the notes as well creates redundant data. It's better to rely on the rpe column exclusively to avoid data duplication and potential inconsistencies. The deployment process should ensure migrations are run before the new code is active.

Suggested change
notes: set.rpe ? `RPE: ${set.rpe}` : null, // Fallback: store RPE in notes if column doesn't exist yet
notes: null,

}
};

const handleDisconnectHevy = async (providerId: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The handleDisconnectHevy function is defined to accept a providerId, but this parameter is not used within the function body. The disconnectHevyAccount API call it invokes operates on the current user's context without needing an ID.

To improve code clarity and remove the unused variable, you could update the function signature here and in the corresponding prop types. You would also need to adjust the call site in ExternalProviderList.tsx.

For example:

  1. Here: Change to const handleDisconnectHevy = async () => { ... }
  2. In ExternalProviderList.tsx props: Change to handleDisconnectHevy: () => Promise<void>;
  3. In ExternalProviderList.tsx call site: Change onClick={() => handleDisconnectHevy(provider.id)} to onClick={handleDisconnectHevy}
Suggested change
const handleDisconnectHevy = async (providerId: string) => {
const handleDisconnectHevy = async () => {

@CodeWithCJ
Copy link
Owner Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new integration with Hevy, a workout tracking app. The changes are extensive, covering the backend service, API endpoints, database schema, and frontend UI for configuration and data display. The implementation is solid, adding RPE tracking and sync capabilities. I've identified a few areas for improvement regarding code duplication, efficiency, and a potential bug in the mock data handling logic. Overall, great work on adding this significant feature.

Comment on lines 217 to 223
await apiCall(
`/integrations/hevy/sync${fullSyncOnConnect ? '?fullSync=true' : ''}`,
{
method: 'POST',
body: JSON.stringify({ providerId: data.id }),
}
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block for triggering the initial Hevy sync duplicates the logic from the syncHevyData helper function. To follow the DRY (Don't Repeat Yourself) principle and improve maintainability, you should reuse the existing syncHevyData function here.

          await syncHevyData(fullSyncOnConnect, data.id);

Comment on lines 227 to 251
if (hevyProviders.length > 0) {
for (const provider of hevyProviders) {
try {
const hevyStatus = await apiCall(`/integrations/hevy/status`);
setProviders((prev) =>
prev.map((p) =>
p.id === provider.id
? {
...p,
hevy_last_sync_at: hevyStatus.lastSyncAt,
hevy_connect_status: hevyStatus.connected
? 'connected'
: 'disconnected',
}
: p
)
);
} catch (hevyError) {
console.error(
'Failed to fetch Hevy specific status for provider:',
provider.id,
hevyError
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop makes a redundant API call to /integrations/hevy/status for each Hevy provider. Since the backend endpoint is not provider-specific and returns a general status for the user, you should fetch the status only once before mapping over the providers. This will improve performance and simplify the code.

      if (hevyProviders.length > 0) {
        try {
          const hevyStatus = await apiCall(`/integrations/hevy/status`);
          setProviders((prev) =>
            prev.map((p) =>
              p.provider_type === 'hevy'
                ? {
                    ...p,
                    hevy_last_sync_at: hevyStatus.lastSyncAt,
                    hevy_connect_status: hevyStatus.connected
                      ? 'connected'
                      : 'disconnected',
                  }
                : p
            )
          );
        } catch (hevyError) {
          console.error(
            'Failed to fetch Hevy status for providers:',
            hevyProviders.map((p) => p.id),
            hevyError
          );
        }
      }

Comment on lines +782 to +819
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() => handleManualSyncHevy(provider.id)}
disabled={loading}
className="ml-2 text-blue-500"
>
<RefreshCw className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Sync Recently (7 Days)</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
onClick={() =>
handleManualSyncHevy(provider.id, true)
}
disabled={loading}
className="ml-2 text-cyan-500"
>
<RefreshCw className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Sync All History</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

You are using two separate TooltipProvider components for the two sync buttons. A single TooltipProvider can wrap multiple Tooltip components. Consolidating this into one provider would make the code slightly cleaner and more efficient.

                      <TooltipProvider>
                        <Tooltip>
                          <TooltipTrigger asChild>
                            <Button
                              variant="ghost"
                              size="icon"
                              onClick={() => handleManualSyncHevy(provider.id)}
                              disabled={loading}
                              className="ml-2 text-blue-500"
                            >
                              <RefreshCw className="h-4 w-4" />
                            </Button>
                          </TooltipTrigger>
                          <TooltipContent>
                            <p>Sync Recently (7 Days)</p>
                          </TooltipContent>
                        </Tooltip>
                        <Tooltip>
                          <TooltipTrigger asChild>
                            <Button
                              variant="ghost"
                              size="icon"
                              onClick={() =>
                                handleManualSyncHevy(provider.id, true)
                              }
                              disabled={loading}
                              className="ml-2 text-cyan-500"
                            >
                              <RefreshCw className="h-4 w-4" />
                            </Button>
                          </TooltipTrigger>
                          <TooltipContent>
                            <p>Sync All History</p>
                          </TooltipContent>
                        </Tooltip>
                      </TooltipProvider>

Comment on lines +281 to +286
await client.query(
`UPDATE external_data_providers
SET last_sync_at = NOW(), updated_at = NOW()
WHERE user_id = $1 AND provider_type = 'hevy'`,
[userId],
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The query to update last_sync_at for local mock data sources targets all 'hevy' providers for a user, which could lead to incorrect data if a user has multiple Hevy providers configured. This should be scoped to a specific providerId to be consistent with the live API sync logic and prevent potential bugs.

        const targetProviderId = providerId || (await getHevyProviderId(userId));
        if (targetProviderId) {
          await client.query(
            `UPDATE external_data_providers
                       SET last_sync_at = NOW(), updated_at = NOW()
                       WHERE id = $1`,
            [targetProviderId],
          );
        } else {
          log('warn', `[hevyService] Could not determine a provider ID to update sync time for user ${userId}.`);
        }

@CodeWithCJ CodeWithCJ merged commit eadd7c7 into main Feb 15, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants