Skip to content

Commit d14f725

Browse files
committed
Merge branch 'master' of https://github.com/MachDatum/ThingConnect.Pulse into logo
2 parents 0bae5d7 + 35908d2 commit d14f725

31 files changed

+2832
-795
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"Bash(ping:*)",
1515
"Bash(findstr:*)",
1616
"Bash(npx eslint:*)",
17-
"Read(//c/ProgramData/ThingConnect.Pulse/logs/**)"
17+
"Read(//c/ProgramData/ThingConnect.Pulse/logs/**)",
18+
"WebSearch"
1819
],
1920
"deny": [],
2021
"ask": []

ThingConnect.Pulse.Server/Controllers/StatusController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public async Task<ActionResult<LiveStatusItemDto>> GetLiveStatusAsync(
3232
{
3333
try
3434
{
35-
_logger.LogInformation("Getting live status - group: {Group}, search: {Search}, page: {Page}, pageSize: {PageSize}",
35+
_logger.LogInformation("Getting live status - group: {Group}, search: {Search}",
3636
group, search);
3737

3838
List<LiveStatusItemDto> result = await _statusService.GetLiveStatusAsync(group, search);

ThingConnect.Pulse.Server/Controllers/UserManagementController.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public async Task<ActionResult<UserInfoDto>> CreateUserAsync([FromBody] CreateUs
187187
_logger.LogInformation("User created: {Username} (ID: {UserId}) by admin {AdminId}",
188188
user.UserName, user.Id, currentUser?.Id);
189189

190-
return CreatedAtAction(nameof(GetUserByIdAsync), new { id = user.Id }, new UserInfoDto
190+
var userDto = new UserInfoDto
191191
{
192192
Id = user.Id,
193193
Username = user.UserName,
@@ -196,7 +196,11 @@ public async Task<ActionResult<UserInfoDto>> CreateUserAsync([FromBody] CreateUs
196196
CreatedAt = user.CreatedAt,
197197
LastLoginAt = user.LastLoginAt,
198198
IsActive = user.IsActive
199-
});
199+
};
200+
201+
// Return Ok for now to avoid routing issues
202+
// TODO: Fix location header generation
203+
return Ok(userDto);
200204
}
201205
catch (Exception ex)
202206
{

ThingConnect.Pulse.Server/Services/Monitoring/OutageDetectionService.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,20 @@ private async Task SaveCheckResultAsync(CheckResult result, CancellationToken ca
530530
};
531531

532532
context.CheckResultsRaw.Add(rawResult);
533+
534+
// Update LastRttMs for successful probes
535+
if (result.Status == UpDown.up && result.RttMs.HasValue)
536+
{
537+
Data.Endpoint? endpoint = await context.Endpoints.FindAsync([result.EndpointId], cancellationToken);
538+
if (endpoint != null)
539+
{
540+
endpoint.LastRttMs = result.RttMs.Value;
541+
_logger.LogInformation(
542+
"Updated LastRttMs for endpoint {EndpointId} to {RttMs}ms",
543+
result.EndpointId, result.RttMs.Value);
544+
}
545+
}
546+
533547
await context.SaveChangesAsync(cancellationToken);
534548
}
535549
}

ThingConnect.Pulse.Server/Services/StatusService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ public async Task<List<LiveStatusItemDto>> GetLiveStatusAsync(string? group, str
8787
? sparklineData[endpoint.Id]
8888
: new List<SparklinePoint>();
8989

90+
_logger.LogInformation(
91+
"Endpoint {EndpointName}: Status = {Status}, LastRttMs = {RttMs}, LastChangeTs = {LastChangeTs}",
92+
endpoint.Name, status, endpoint.LastRttMs, endpoint.LastChangeTs);
93+
9094
items.Add(new LiveStatusItemDto
9195
{
9296
Endpoint = MapToEndpointDto(endpoint),

docs/rtt-tracing.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# RTT Value Tracing and Investigation
2+
3+
## RTT Journey Flow Diagram
4+
5+
```mermaid
6+
graph TD
7+
A[Probe Service] -->|Measure RTT| B[CheckResult]
8+
B -->|RttMs| C[MonitoringBackgroundService]
9+
C -->|Update Endpoint| D[OutageDetectionService]
10+
D -->|Persist LastRttMs| E[StatusService]
11+
E -->|Serialize RTT| F[API Controller]
12+
F -->|Response| G[Frontend Service]
13+
G -->|Render| H[Frontend Components]
14+
15+
subgraph Potential Failure Points
16+
A -->|Null/Zero RTT| B
17+
B -->|Null RttMs| C
18+
C -->|Ignore Null RTT| D
19+
D -->|Skip Update| E
20+
E -->|Empty RTT| F
21+
F -->|Omit RTT| G
22+
G -->|Display Placeholder| H
23+
end
24+
```
25+
26+
## RTT Tracing Points
27+
28+
### 1. Probe Service (`ProbeService.cs`)
29+
- **Probe Types**:
30+
- ICMP: Uses `reply.RoundtripTime`
31+
- TCP: Uses `stopwatch.ElapsedMilliseconds`
32+
- HTTP: Uses `stopwatch.ElapsedMilliseconds`
33+
- **Failure Handling**:
34+
- Sets `RttMs` to null on probe failures
35+
- Different measurement methods may introduce inconsistencies
36+
37+
### 2. Background Monitoring Service
38+
- Logs RTT with trace:
39+
```csharp
40+
_logger.LogTrace("Probed endpoint {EndpointId} ({Name}): {Status} in {RttMs}ms")
41+
```
42+
- Converts null RTT to "N/A"
43+
44+
### 3. Outage Detection Service
45+
- Copies `result.RttMs` to new records
46+
- Potential loss point if RTT is null
47+
48+
### 4. Status Service
49+
- Uses `endpoint.LastRttMs` to populate status responses
50+
- May return null/empty RTT
51+
52+
### 5. Frontend Service and Components
53+
- Uses optional chaining and nullish coalescing
54+
- Handles null RTT with placeholders ('-')
55+
56+
## Investigation Steps
57+
58+
### 1. Probe Measurement Verification
59+
- [ ] Compare RTT measurements across different probe types
60+
- [ ] Add detailed logging in `ProbeService`
61+
- [ ] Verify stopwatch and `RoundtripTime` calculations
62+
63+
### 2. Null Value Propagation Analysis
64+
- [ ] Trace null RTT through each service layer
65+
- [ ] Add comprehensive logging
66+
- [ ] Validate null handling in serialization
67+
68+
### 3. API Contract Validation
69+
- [ ] Inspect API response DTOs
70+
- [ ] Check JSON serialization of RTT values
71+
- [ ] Verify optional/nullable field handling
72+
73+
### 4. Frontend Parsing Investigation
74+
- [ ] Review TypeScript type definitions
75+
- [ ] Validate RTT parsing in API services
76+
- [ ] Test display logic for various RTT scenarios
77+
78+
### 5. Comprehensive Test Scenarios
79+
- [ ] Successful probe with non-zero RTT
80+
- [ ] Failed probe (null RTT)
81+
- [ ] Edge cases: zero, very low, very high RTT values
82+
- [ ] Multiple probe type comparisons
83+
84+
## Potential Improvement Recommendations
85+
- Standardize RTT measurement across probe types
86+
- Implement consistent null/zero RTT handling
87+
- Add more granular logging and tracing
88+
- Create comprehensive test suite for RTT propagation

thingconnect.pulse.client/obj/Debug/package.g.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<PackageJsonDependenciesNextThemes Condition="$(PackageJsonDependenciesNextThemes) == ''">^0.4.6</PackageJsonDependenciesNextThemes>
2828
<PackageJsonDependenciesReact Condition="$(PackageJsonDependenciesReact) == ''">^19.1.1</PackageJsonDependenciesReact>
2929
<PackageJsonDependenciesReactDom Condition="$(PackageJsonDependenciesReactDom) == ''">^19.1.1</PackageJsonDependenciesReactDom>
30+
<PackageJsonDependenciesReactHookForm Condition="$(PackageJsonDependenciesReactHookForm) == ''">^7.62.0</PackageJsonDependenciesReactHookForm>
3031
<PackageJsonDependenciesReactIcons Condition="$(PackageJsonDependenciesReactIcons) == ''">^5.5.0</PackageJsonDependenciesReactIcons>
3132
<PackageJsonDependenciesReactRouterDom Condition="$(PackageJsonDependenciesReactRouterDom) == ''">^7.8.1</PackageJsonDependenciesReactRouterDom>
3233
<PackageJsonDependenciesZod Condition="$(PackageJsonDependenciesZod) == ''">^4.0.17</PackageJsonDependenciesZod>

thingconnect.pulse.client/package-lock.json

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

thingconnect.pulse.client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"next-themes": "^0.4.6",
2929
"react": "^19.1.1",
3030
"react-dom": "^19.1.1",
31+
"react-hook-form": "^7.62.0",
3132
"react-icons": "^5.5.0",
3233
"react-router-dom": "^7.8.1",
3334
"zod": "^4.0.17"
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { apiClient } from '../client';
2+
import type {
3+
UserInfo,
4+
CreateUserRequest,
5+
UpdateUserRequest,
6+
ChangeRoleRequest,
7+
ResetPasswordRequest,
8+
UsersListParams,
9+
PagedResult,
10+
} from '../types';
11+
12+
export class UserManagementService {
13+
/**
14+
* Get paginated list of users with optional filtering
15+
*/
16+
static async getUsers(params: UsersListParams = {}): Promise<PagedResult<UserInfo>> {
17+
const searchParams = new URLSearchParams();
18+
19+
if (params.page) {
20+
searchParams.append('page', params.page.toString());
21+
}
22+
23+
if (params.pageSize) {
24+
searchParams.append('pageSize', params.pageSize.toString());
25+
}
26+
27+
if (params.search) {
28+
searchParams.append('search', params.search);
29+
}
30+
31+
if (params.role) {
32+
searchParams.append('role', params.role);
33+
}
34+
35+
if (params.isActive !== undefined) {
36+
searchParams.append('isActive', params.isActive.toString());
37+
}
38+
39+
const queryString = searchParams.toString();
40+
const url = `/api/UserManagement${queryString ? `?${queryString}` : ''}`;
41+
42+
return apiClient.get<PagedResult<UserInfo>>(url);
43+
}
44+
45+
/**
46+
* Get a single user by ID
47+
*/
48+
static async getUserById(id: string): Promise<UserInfo> {
49+
return apiClient.get<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}`);
50+
}
51+
52+
/**
53+
* Create a new user (admin only)
54+
*/
55+
static async createUser(request: CreateUserRequest): Promise<UserInfo> {
56+
return apiClient.post<UserInfo>('/api/UserManagement', request);
57+
}
58+
59+
/**
60+
* Update user details
61+
*/
62+
static async updateUser(id: string, request: UpdateUserRequest): Promise<UserInfo> {
63+
return apiClient.put<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}`, request);
64+
}
65+
66+
/**
67+
* Delete a user
68+
*/
69+
static async deleteUser(id: string): Promise<void> {
70+
return apiClient.delete<void>(`/api/UserManagement/${encodeURIComponent(id)}`);
71+
}
72+
73+
/**
74+
* Change user role
75+
*/
76+
static async changeUserRole(id: string, request: ChangeRoleRequest): Promise<UserInfo> {
77+
return apiClient.put<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}/role`, request);
78+
}
79+
80+
/**
81+
* Reset user password (admin only)
82+
*/
83+
static async resetUserPassword(id: string, request: ResetPasswordRequest): Promise<void> {
84+
return apiClient.post<void>(`/api/UserManagement/${encodeURIComponent(id)}/reset-password`, request);
85+
}
86+
87+
/**
88+
* Toggle user active status
89+
*/
90+
static async toggleUserStatus(id: string, isActive: boolean): Promise<UserInfo> {
91+
return apiClient.put<UserInfo>(`/api/UserManagement/${encodeURIComponent(id)}`, {
92+
isActive,
93+
});
94+
}
95+
}

0 commit comments

Comments
 (0)