diff --git a/next.config.ts b/next.config.ts index 68a6c64..bee5911 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,22 @@ +// ABOUTME: Next.js configuration for PhaseFlow application. +// ABOUTME: Configures standalone output and injects git commit hash for build verification. +import { execSync } from "node:child_process"; import type { NextConfig } from "next"; +// Get git commit hash at build time for deployment verification +function getGitCommit(): string { + try { + return execSync("git rev-parse --short HEAD").toString().trim(); + } catch { + return "unknown"; + } +} + const nextConfig: NextConfig = { output: "standalone", + env: { + GIT_COMMIT: process.env.GIT_COMMIT || getGitCommit(), + }, }; export default nextConfig; diff --git a/src/lib/garmin.ts b/src/lib/garmin.ts index e779f5c..b7b2ab2 100644 --- a/src/lib/garmin.ts +++ b/src/lib/garmin.ts @@ -1,5 +1,7 @@ // ABOUTME: Garmin Connect API client using stored OAuth tokens. // ABOUTME: Fetches body battery, HRV, and intensity minutes from Garmin. + +import { logger } from "@/lib/logger"; import type { GarminTokens, HrvStatus } from "@/types"; const GARMIN_BASE_URL = "https://connect.garmin.com/modern/proxy"; @@ -63,6 +65,10 @@ export async function fetchHrvStatus( }); if (!response.ok) { + logger.warn( + { status: response.status, endpoint: "hrv-service" }, + "Garmin HRV API error", + ); return "Unknown"; } @@ -70,13 +76,23 @@ export async function fetchHrvStatus( const status = data?.hrvSummary?.status; if (status === "BALANCED") { + logger.info({ status: "BALANCED" }, "Garmin HRV data received"); return "Balanced"; } if (status === "UNBALANCED") { + logger.info({ status: "UNBALANCED" }, "Garmin HRV data received"); return "Unbalanced"; } + logger.info( + { rawStatus: status, hasData: !!data?.hrvSummary }, + "Garmin HRV returned unknown status", + ); return "Unknown"; - } catch { + } catch (error) { + logger.error( + { err: error, endpoint: "hrv-service" }, + "Garmin HRV fetch failed", + ); return "Unknown"; } } @@ -97,6 +113,10 @@ export async function fetchBodyBattery( ); if (!response.ok) { + logger.warn( + { status: response.status, endpoint: "bodyBattery" }, + "Garmin body battery API error", + ); return { current: null, yesterdayLow: null }; } @@ -108,8 +128,22 @@ export async function fetchBodyBattery( const yesterdayStats = data?.bodyBatteryStatList?.[0]; const yesterdayLow = yesterdayStats?.min ?? null; + logger.info( + { + current, + yesterdayLow, + hasCurrentData: !!currentData, + hasYesterdayData: !!yesterdayStats, + }, + "Garmin body battery data received", + ); + return { current, yesterdayLow }; - } catch { + } catch (error) { + logger.error( + { err: error, endpoint: "bodyBattery" }, + "Garmin body battery fetch failed", + ); return { current: null, yesterdayLow: null }; } } @@ -129,6 +163,10 @@ export async function fetchIntensityMinutes( ); if (!response.ok) { + logger.warn( + { status: response.status, endpoint: "fitnessstats" }, + "Garmin intensity minutes API error", + ); return 0; } @@ -136,14 +174,28 @@ export async function fetchIntensityMinutes( const weeklyTotal = data?.weeklyTotal; if (!weeklyTotal) { + logger.info( + { hasWeeklyTotal: false }, + "Garmin intensity minutes: no weekly data", + ); return 0; } const moderate = weeklyTotal.moderateIntensityMinutes ?? 0; const vigorous = weeklyTotal.vigorousIntensityMinutes ?? 0; + const total = moderate + vigorous; - return moderate + vigorous; - } catch { + logger.info( + { moderate, vigorous, total }, + "Garmin intensity minutes data received", + ); + + return total; + } catch (error) { + logger.error( + { err: error, endpoint: "fitnessstats" }, + "Garmin intensity minutes fetch failed", + ); return 0; } } diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index d1a5f4d..aec4b68 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -47,3 +47,19 @@ export const activeUsersGauge = new promClient.Gauge({ help: "Number of users with activity in the last 24 hours", registers: [metricsRegistry], }); + +// Build info metric: exposes version and git commit for deployment verification +const buildInfo = new promClient.Gauge({ + name: "phaseflow_build_info", + help: "Build information with version and git commit", + labelNames: ["version", "commit"] as const, + registers: [metricsRegistry], +}); + +// Set build info at module load (value is always 1, labels contain the info) +buildInfo + .labels({ + version: process.env.npm_package_version || "unknown", + commit: process.env.GIT_COMMIT || "unknown", + }) + .set(1);