Skip to content

Commit a2abe82

Browse files
committed
feat: implement backend computation with progress tracking improvements
Add server-side weight trend calculations, improve progress tracking system, fix UI issues, and restructure project documentation. feat(api): add MeasurementComputationService for server-side trend calculations - Implement exponentially smoothed moving average (alpha=0.1) - Add ComputedMeasurement and SourceMeasurement models - Update MeasurementsResponse to use computedMeasurements array - Support optional includeSource parameter for raw data access - Add comprehensive test coverage for computation service refactor(web): improve progress tracking system and remove client-side computation - Remove client-side computation logic from dashboard hooks - Update queries to consume backend-computed measurements - Centralize toast management in SyncProgressProvider to prevent duplicates - Simplify useSyncProgress API by hiding internal functions - Remove redundant useSyncProgressId hook - Fix React hooks rule violations with proper useWithProgress wrapper - Update all test mocks for new API structure fix(web): download page skeleton width now matches actual table dimensions docs: restructure documentation with steering documents and commit guidelines - Add steering docs for product, structure, tech, and conventions - Document spec-driven development approach - Add comprehensive commit message guidelines with multi-topic format - Remove outdated architecture and testing docs
1 parent 33986eb commit a2abe82

42 files changed

Lines changed: 6191 additions & 2398 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENT.md

Lines changed: 7 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -1,168 +1,7 @@
1-
# CLAUDE.md
2-
3-
AI-specific guidance for working with the TrendWeight codebase.
4-
5-
## Project Context
6-
- TrendWeight tracks weight trends using smart scales (Withings/Fitbit)
7-
- C# ASP.NET Core API backend + Vite React TypeScript frontend
8-
- Clerk for auth (NOT Supabase Auth), Supabase for PostgreSQL only
9-
- Project name: **TrendWeight** (capital T, capital W, no space)
10-
11-
## Code Search Strategy
12-
### Use Semantic Search (mcp__code-context__search_code) for:
13-
- Conceptual searches (finding code by what it does)
14-
- Algorithm/calculation implementations
15-
- Feature exploration across files
16-
- Cross-cutting concerns (auth, error handling, validation)
17-
- Initial investigation when location unknown
18-
19-
### Use Direct Tools (Read, Grep, Glob) for:
20-
- Known file locations
21-
- Literal string matches, variable names
22-
- File patterns by extension/naming
23-
- Configuration values, constants
24-
- Quick verification of specific lines
25-
26-
### Search Performance:
27-
- Semantic excels: business logic, UI components, features, tests
28-
- Semantic struggles: infrastructure config, connection strings, cache settings
29-
- Direct excels: exact matches, known patterns, config files, imports
30-
31-
## TypeScript/React Rules
32-
- Use kebab-case for all frontend files (`user-profile.tsx`)
33-
- Component names in files contents remain PascalCase (`UserProfile`)
34-
- Enable strict mode, avoid `any` type
35-
- Use functional components with hooks
36-
- Cache Intl formatters at module level
37-
- Use TanStack Query for all API calls
38-
- Use `ExternalLink` component for external URLs
39-
- Use standard UI components (Button, Heading, Select, etc) never raw HTML
40-
41-
## Tailwind CSS Rules
42-
- Use semantic color variables from index.css (e.g., `bg-background`, `text-foreground`)
43-
- Never use explicit colors (e.g., `bg-gray-500`, `text-blue-600`)
44-
- Avoid `dark:` prefixes - CSS variables handle theme switching automatically
45-
- Rare exceptions: only when CSS variables don't cover the specific need
46-
- Check index.css for available semantic variables before adding styles
47-
- Semantic colors automatically adapt to light/dark mode
48-
49-
## Route File Pattern (MANDATORY)
50-
```typescript
51-
import { createFileRoute } from "@tanstack/react-router";
52-
import { Layout } from "../components/Layout";
53-
import { ComponentName } from "../components/feature-folder/ComponentName";
54-
55-
export const Route = createFileRoute("/route-path")({
56-
beforeLoad: requireAuth, // Only if needed
57-
loader: async () => { }, // Only if needed
58-
component: RouteNamePage,
59-
});
60-
61-
function RouteNamePage() {
62-
const { param } = Route.useParams(); // Only if route has params
63-
return (
64-
<Layout title="Page Title">
65-
<ComponentName param={param} />
66-
</Layout>
67-
);
68-
}
69-
```
70-
- Routes must be minimal (<30 lines)
71-
- NO business logic, hooks, or UI in routes
72-
- ALL logic in feature components under `apps/web/src/components/`
73-
- Feature folders match route purpose
74-
75-
## C# Backend Rules
76-
- Use feature folders for organization
77-
- Inherit from `BaseAuthController` for auth endpoints
78-
- Keep controllers thin, logic in services
79-
- Use async/await for all I/O
80-
- Use IOptions<T> for configuration
81-
- Use JSONB for flexible data storage
82-
- Use constants for magic numbers
83-
- Store timestamps as ISO 8601 strings
84-
- Store weights in kg
85-
86-
## Development Workflow
87-
### Pre-commit checks (MANDATORY):
88-
```bash
89-
npm run check && npm run test
90-
```
91-
- Run from root directory only
92-
- Never skip TypeScript compilation check
93-
94-
### Commit message guidelines:
95-
- Never use "BREAKING CHANGE" in commit messages
96-
- This is an application, not a library - there are no breaking changes
97-
- Use conventional commit format without breaking change notation
98-
99-
### Command locations:
100-
- Always run commands from repo root
101-
- Turborepo handles optimization
102-
- Never run commands in workspace subdirectories
103-
104-
## Testing Requirements
105-
### Frontend:
106-
- Use MSW for HTTP mocking (never mock fetch directly)
107-
- Suppress console for expected errors: `vi.spyOn(console, "error").mockImplementation(() => {})`
108-
- Test business logic, not framework integration
109-
110-
### Backend:
111-
- Use xUnit for all tests
112-
- Extract business logic to testable services
113-
- Don't test framework integration directly
114-
115-
## Database Guidelines
116-
- Modify schema in Supabase dashboard
117-
- Update C# models with `Db` prefix
118-
- Update schema documentation when changed
119-
- All timestamps as ISO 8601 strings
120-
- All weights in kilograms
121-
122-
## Spec-Workflow Rules (MANDATORY)
123-
- **CRITICAL: ALWAYS call `spec-workflow-guide` BEFORE ANY spec work** - this is the first tool to call when:
124-
- User mentions working on a spec or task
125-
- User asks to execute/implement a task number
126-
- Returning to spec work after any break
127-
- After conversation compaction
128-
- **ALWAYS load full spec context** (requirements, design, tasks) via `get-spec-context` before implementing
129-
- **NEVER read/write spec files directly** - use spec-workflow tools exclusively:
130-
- `spec-workflow-guide` - Load workflow guide first (MANDATORY FIRST STEP)
131-
- `get-spec-context` - Load spec documents before work
132-
- `manage-tasks` - Update task status and get context
133-
- `create-spec-doc` - Create/update spec documents
134-
- **NEVER assume spec details** from memory after conversation compaction
135-
136-
## Common Pitfalls to Avoid
137-
- Never add CORS to API (Vite proxy handles it)
138-
- Never expose exception details to users
139-
- Avoid unnecessary type assertions
140-
- Never create files unless absolutely necessary
141-
- Never proactively create documentation files
142-
- Always use `git mv` for renaming tracked files
143-
- Always extract magic numbers to constants
144-
- Always use standard UI components over raw HTML
145-
146-
## Lessons Learned
147-
- Functions should sort input data before processing
148-
- Route files were refactored to minimal pattern - maintain this
149-
150-
## File Update Policy
151-
**Auto-update this file when learning:**
152-
- AI-specific coding patterns
153-
- User coding preferences affecting generation
154-
- Technical discoveries impacting code generation
155-
- Lessons that help future AI instances
156-
157-
**Do NOT add:**
158-
- General architecture info (→ docs/ARCHITECTURE.md)
159-
- Feature documentation (→ docs/ARCHITECTURE.md)
160-
- Setup instructions (→ docs/README.md)
161-
162-
## CLAUDE.md Format Guidelines
163-
- Keep directives as concise bullet points
164-
- Avoid verbose explanations or human-readable prose
165-
- Group related items under clear headings
166-
- Use code examples only when pattern is complex
167-
- Prefer directive lists over paragraph explanations
168-
- Remove redundancy - state each rule once
1+
- Always read the steering docs before doing any planning or coding:
2+
- @docs/steering/product.md
3+
- @docs/steering/structure.md
4+
- @docs/steering/tech.md
5+
- @docs/steering/conventions.md
6+
- Always re-read all steering docs after compacting a conversation.
7+
- Always keep steering documents up to date as details change over time

apps/api/TrendWeight.Tests/Features/Measurements/Controllers/MeasurementsControllerTests.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class MeasurementsControllerTests : TestBase
2323
private readonly Mock<IProfileService> _profileServiceMock;
2424
private readonly Mock<IProviderIntegrationService> _providerIntegrationServiceMock;
2525
private readonly Mock<IMeasurementSyncService> _measurementSyncServiceMock;
26+
private readonly Mock<IMeasurementComputationService> _measurementComputationServiceMock;
2627
private readonly Mock<ILogger<MeasurementsController>> _loggerMock;
2728
private readonly Mock<ICurrentRequestContext> _requestContextMock;
2829
private readonly MeasurementsController _sut;
@@ -32,6 +33,7 @@ public MeasurementsControllerTests()
3233
_profileServiceMock = new Mock<IProfileService>();
3334
_providerIntegrationServiceMock = new Mock<IProviderIntegrationService>();
3435
_measurementSyncServiceMock = new Mock<IMeasurementSyncService>();
36+
_measurementComputationServiceMock = new Mock<IMeasurementComputationService>();
3537
_loggerMock = new Mock<ILogger<MeasurementsController>>();
3638
_requestContextMock = new Mock<ICurrentRequestContext>();
3739
_requestContextMock.SetupAllProperties();
@@ -40,6 +42,7 @@ public MeasurementsControllerTests()
4042
_profileServiceMock.Object,
4143
_providerIntegrationServiceMock.Object,
4244
_measurementSyncServiceMock.Object,
45+
_measurementComputationServiceMock.Object,
4346
_loggerMock.Object,
4447
_requestContextMock.Object);
4548
}
@@ -69,6 +72,8 @@ public async Task GetMeasurements_WithValidUser_ReturnsDataWithProviderStatus()
6972
{ "fitbit", new ProviderSyncStatus { Success = true } }
7073
}
7174
});
75+
_measurementComputationServiceMock.Setup(x => x.ComputeMeasurements(sourceData, user.Profile))
76+
.Returns(new List<ComputedMeasurement>());
7277

7378
// Act
7479
var result = await _sut.GetMeasurements();
@@ -78,7 +83,8 @@ public async Task GetMeasurements_WithValidUser_ReturnsDataWithProviderStatus()
7883
var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
7984
var response = okResult.Value.Should().BeOfType<MeasurementsResponse>().Subject;
8085
response.IsMe.Should().Be(true);
81-
response.Data.Should().BeEquivalentTo(sourceData);
86+
response.ComputedMeasurements.Should().NotBeNull();
87+
response.SourceData.Should().BeNull(); // Default includeSource=false
8288
response.ProviderStatus.Should().NotBeNull();
8389

8490
response.ProviderStatus.Should().HaveCount(2);
@@ -141,6 +147,8 @@ public async Task GetMeasurements_WhenDataNeedsRefresh_RefreshesAndReturnsData()
141147
{ "withings", new ProviderSyncStatus { Success = true } }
142148
}
143149
});
150+
_measurementComputationServiceMock.Setup(x => x.ComputeMeasurements(sourceData, user.Profile))
151+
.Returns(new List<ComputedMeasurement>());
144152

145153
// Act
146154
var result = await _sut.GetMeasurements();
@@ -149,7 +157,8 @@ public async Task GetMeasurements_WhenDataNeedsRefresh_RefreshesAndReturnsData()
149157
result.Should().NotBeNull();
150158
var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
151159
var response = okResult.Value.Should().BeOfType<MeasurementsResponse>().Subject;
152-
response.Data.Should().BeEquivalentTo(sourceData);
160+
response.ComputedMeasurements.Should().NotBeNull();
161+
response.SourceData.Should().BeNull(); // Default includeSource=false
153162
}
154163

155164
[Fact]
@@ -241,6 +250,8 @@ public async Task GetMeasurementsBySharingCode_WithValidCode_ReturnsDataWithoutP
241250
{ "withings", new ProviderSyncStatus { Success = true } }
242251
}
243252
});
253+
_measurementComputationServiceMock.Setup(x => x.ComputeMeasurements(sourceData, user.Profile))
254+
.Returns(new List<ComputedMeasurement>());
244255

245256
// Act
246257
var result = await _sut.GetMeasurementsBySharingCode(sharingCode);
@@ -250,7 +261,8 @@ public async Task GetMeasurementsBySharingCode_WithValidCode_ReturnsDataWithoutP
250261
var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
251262
var response = okResult.Value.Should().BeOfType<MeasurementsResponse>().Subject;
252263
response.IsMe.Should().Be(false);
253-
response.Data.Should().BeEquivalentTo(sourceData);
264+
response.ComputedMeasurements.Should().NotBeNull();
265+
response.SourceData.Should().BeNull(); // Default includeSource=false
254266
response.ProviderStatus.Should().BeNull(); // No provider status for shared view
255267
}
256268

apps/api/TrendWeight.Tests/Features/Measurements/Services/MeasurementSyncServiceTests.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using TrendWeight.Features.Measurements.Models;
88
using TrendWeight.Features.Providers;
99
using TrendWeight.Features.Providers.Models;
10+
using TrendWeight.Features.SyncProgress;
1011
using TrendWeight.Tests.Fixtures;
1112
using Xunit;
1213

@@ -35,7 +36,7 @@ public MeasurementSyncServiceTests()
3536
_sourceDataServiceMock.Object,
3637
_loggerMock.Object,
3738
_environmentMock.Object,
38-
null); // ISyncProgressReporter
39+
Mock.Of<ISyncProgressReporter>()); // ISyncProgressReporter
3940
}
4041

4142
#region Constructor Tests
@@ -53,7 +54,7 @@ public void Constructor_InProductionEnvironment_SetsCacheDurationTo300Seconds()
5354
_sourceDataServiceMock.Object,
5455
_loggerMock.Object,
5556
_environmentMock.Object,
56-
null); // ISyncProgressReporter
57+
Mock.Of<ISyncProgressReporter>()); // ISyncProgressReporter
5758

5859
service.Should().NotBeNull();
5960
}
@@ -70,7 +71,7 @@ public void Constructor_InDevelopmentEnvironment_SetsCacheDurationTo10Seconds()
7071
_sourceDataServiceMock.Object,
7172
_loggerMock.Object,
7273
_environmentMock.Object,
73-
null); // ISyncProgressReporter
74+
Mock.Of<ISyncProgressReporter>()); // ISyncProgressReporter
7475

7576
// Assert - This will be tested indirectly through the GetMeasurementsForUserAsync tests
7677
service.Should().NotBeNull();
@@ -596,7 +597,7 @@ public async Task GetMeasurementsForUserAsync_WithDevelopmentEnvironment_UsesSho
596597
_sourceDataServiceMock.Object,
597598
_loggerMock.Object,
598599
environmentMock.Object,
599-
null); // ISyncProgressReporter
600+
Mock.Of<ISyncProgressReporter>()); // ISyncProgressReporter
600601

601602
var userId = Guid.NewGuid();
602603
var activeProviders = new List<string> { "withings" };
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using TrendWeight.Features.Measurements.Models;
2+
using TrendWeight.Features.Profile.Models;
3+
4+
namespace TrendWeight.Features.Measurements;
5+
6+
/// <summary>
7+
/// Service for computing measurements from raw source data
8+
/// Contains all computation logic as private methods
9+
/// </summary>
10+
public interface IMeasurementComputationService
11+
{
12+
/// <summary>
13+
/// Computes measurements from source data for a user
14+
/// </summary>
15+
/// <param name="sourceData">Raw source data from providers</param>
16+
/// <param name="profile">User profile for timezone/preferences</param>
17+
/// <returns>Computed measurements with trends</returns>
18+
List<ComputedMeasurement> ComputeMeasurements(List<SourceData> sourceData, ProfileData profile);
19+
}

0 commit comments

Comments
 (0)