Fix garmin-sync upsert and add Settings UI for intensity goals
All checks were successful
Deploy / deploy (push) Successful in 1m39s

- Fix dailyLog upsert to use range query (matches today route pattern)
- Properly distinguish 404 errors from other failures in upsert logic
- Add logging for dailyLog create/update operations
- Add Settings UI section for weekly intensity goals per phase
- Add unit tests for upsert behavior and intensity goals UI
- Add E2E tests for intensity goals settings flow

This fixes the issue where Garmin sync was creating new dailyLog
records instead of updating existing ones (322 vs 222 intensity
minutes bug, Unknown HRV bug).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 20:53:43 +00:00
parent 6cd0c06396
commit 8956e04eca
5 changed files with 630 additions and 4 deletions

View File

@@ -772,4 +772,198 @@ test.describe("settings", () => {
await page.waitForTimeout(500);
});
});
test.describe("intensity goals section", () => {
test.beforeEach(async ({ page }) => {
const email = process.env.TEST_USER_EMAIL;
const password = process.env.TEST_USER_PASSWORD;
if (!email || !password) {
test.skip();
return;
}
await page.goto("/login");
await page.waitForLoadState("networkidle");
const emailInput = page.getByLabel(/email/i);
const hasEmailForm = await emailInput.isVisible().catch(() => false);
if (!hasEmailForm) {
test.skip();
return;
}
await emailInput.fill(email);
await page.getByLabel(/password/i).fill(password);
await page.getByRole("button", { name: /sign in/i }).click();
await page.waitForURL("/", { timeout: 10000 });
await page.goto("/settings");
await page.waitForLoadState("networkidle");
});
test("displays Weekly Intensity Goals section", async ({ page }) => {
const sectionHeading = page.getByRole("heading", {
name: /weekly intensity goals/i,
});
await expect(sectionHeading).toBeVisible();
});
test("displays input for menstrual phase goal", async ({ page }) => {
const menstrualInput = page.getByLabel(/menstrual/i);
await expect(menstrualInput).toBeVisible();
});
test("displays input for follicular phase goal", async ({ page }) => {
const follicularInput = page.getByLabel(/follicular/i);
await expect(follicularInput).toBeVisible();
});
test("displays input for ovulation phase goal", async ({ page }) => {
const ovulationInput = page.getByLabel(/ovulation/i);
await expect(ovulationInput).toBeVisible();
});
test("displays input for early luteal phase goal", async ({ page }) => {
const earlyLutealInput = page.getByLabel(/early luteal/i);
await expect(earlyLutealInput).toBeVisible();
});
test("displays input for late luteal phase goal", async ({ page }) => {
const lateLutealInput = page.getByLabel(/late luteal/i);
await expect(lateLutealInput).toBeVisible();
});
test("can modify menstrual phase goal and save", async ({ page }) => {
const menstrualInput = page.getByLabel(/menstrual/i);
const isVisible = await menstrualInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Get original value
const originalValue = await menstrualInput.inputValue();
// Set a different value
const newValue = originalValue === "75" ? "80" : "75";
await menstrualInput.fill(newValue);
// Save and wait for success toast
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Restore original value
await menstrualInput.fill(originalValue);
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
});
test("persists intensity goal value after page reload", async ({
page,
}) => {
const menstrualInput = page.getByLabel(/menstrual/i);
const isVisible = await menstrualInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Get original value
const originalValue = await menstrualInput.inputValue();
// Set a different value
const newValue = originalValue === "75" ? "85" : "75";
await menstrualInput.fill(newValue);
// Save and wait for success toast
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Reload the page
await page.reload();
await page.waitForLoadState("networkidle");
// Check the value persisted
const menstrualAfter = page.getByLabel(/menstrual/i);
const afterValue = await menstrualAfter.inputValue();
expect(afterValue).toBe(newValue);
// Restore original value
await menstrualAfter.fill(originalValue);
await saveButton.click();
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
});
test("intensity goal inputs have number type and min attribute", async ({
page,
}) => {
const menstrualInput = page.getByLabel(/menstrual/i);
const isVisible = await menstrualInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Check type attribute
const inputType = await menstrualInput.getAttribute("type");
expect(inputType).toBe("number");
// Check min attribute
const inputMin = await menstrualInput.getAttribute("min");
expect(inputMin).toBe("0");
});
test("all intensity goal inputs are disabled while saving", async ({
page,
}) => {
const menstrualInput = page.getByLabel(/menstrual/i);
const isVisible = await menstrualInput.isVisible().catch(() => false);
if (!isVisible) {
test.skip();
return;
}
// Start saving (slow down the response to catch disabled state)
await page.route("**/api/user", async (route) => {
if (route.request().method() === "PATCH") {
// Delay response to allow testing disabled state
await new Promise((resolve) => setTimeout(resolve, 500));
await route.continue();
} else {
await route.continue();
}
});
const saveButton = page.getByRole("button", { name: /save/i });
await saveButton.click();
// Check inputs are disabled during save
await expect(menstrualInput).toBeDisabled();
// Wait for save to complete
await expect(page.getByText(/settings saved successfully/i)).toBeVisible({
timeout: 10000,
});
// Clean up route interception
await page.unroute("**/api/user");
});
});
});