7.8 KiB
Fix Agent Filtering on ATM List (Issue #7)
Context
The ATM list in HiveOps Incident Management has three filter categories: Status, Model, and Agent Connection. The Agent Connection filter UI (Connected/Disconnected/Never Connected) is fully implemented but does not actually filter the results. Users can select filter options, but all ATMs continue to be displayed regardless of the selection.
Root Cause: The agentFilter variable exists in the frontend component but is never sent to the backend API. The backend also lacks the implementation to filter by agent connection status.
Agent connection status is calculated based on the lastHeartbeat timestamp in the AtmProperties table:
- NEVER_CONNECTED:
lastHeartbeatis null - CONNECTED:
lastHeartbeatis within the last 5 minutes - DISCONNECTED:
lastHeartbeatis older than 5 minutes
Implementation Plan
1. Add Bidirectional Relationship in Atm Entity
File: /source/hiveops-src/hiveops-incident/backend/src/main/java/com/hiveops/incident/entity/Atm.java
Add @OneToOne relationship to AtmProperties after line 38:
@OneToOne(mappedBy = "atm", fetch = FetchType.LAZY)
private AtmProperties atmProperties;
public AtmProperties getAtmProperties() { return atmProperties; }
public void setAtmProperties(AtmProperties atmProperties) { this.atmProperties = atmProperties; }
This enables JPA Criteria API joins in the specification.
2. Implement Agent Status Filtering in Specification
File: /source/hiveops-src/hiveops-incident/backend/src/main/java/com/hiveops/incident/specification/AtmSpecification.java
Update withFilters() method signature (line 13) to accept agentStatuses parameter and implement time-based filtering logic:
public static Specification<Atm> withFilters(String search, List<String> statuses,
List<String> models, List<String> agentStatuses) {
return (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
// ... existing search, status, model filtering ...
// NEW: Agent connection status filtering
if (agentStatuses != null && !agentStatuses.isEmpty()) {
var propsJoin = root.join("atmProperties", jakarta.persistence.criteria.JoinType.LEFT);
List<Predicate> agentPredicates = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
LocalDateTime fiveMinutesAgo = now.minusMinutes(5);
for (String agentStatus : agentStatuses) {
switch (agentStatus.toLowerCase()) {
case "connected":
agentPredicates.add(cb.and(
cb.isNotNull(propsJoin.get("lastHeartbeat")),
cb.greaterThan(propsJoin.get("lastHeartbeat"), fiveMinutesAgo)
));
break;
case "disconnected":
agentPredicates.add(cb.and(
cb.isNotNull(propsJoin.get("lastHeartbeat")),
cb.lessThanOrEqualTo(propsJoin.get("lastHeartbeat"), fiveMinutesAgo)
));
break;
case "never_connected":
agentPredicates.add(cb.isNull(propsJoin.get("lastHeartbeat")));
break;
}
}
if (!agentPredicates.isEmpty()) {
predicates.add(cb.or(agentPredicates.toArray(new Predicate[0])));
}
}
return cb.and(predicates.toArray(new Predicate[0]));
};
}
Add import: import java.time.LocalDateTime;
3. Update Controller to Accept Agent Status Parameter
File: /source/hiveops-src/hiveops-incident/backend/src/main/java/com/hiveops/incident/controller/AtmController.java
Update method signature (lines 41-48) to add agentStatus parameter:
@GetMapping("/paginated")
public ResponseEntity<Page<AtmDTO>> getAllAtmsPaginated(
@RequestParam(required = false) String search,
@RequestParam(required = false) List<String> status,
@RequestParam(required = false) List<String> model,
@RequestParam(required = false) List<String> agentStatus,
Pageable pageable) {
return ResponseEntity.ok(atmService.getAllAtmsPaginated(search, status, model, agentStatus, pageable));
}
4. Update Service Layer
File: /source/hiveops-src/hiveops-incident/backend/src/main/java/com/hiveops/incident/service/AtmService.java
Update method signature (lines 66-69) to pass agentStatuses to specification:
public Page<AtmDTO> getAllAtmsPaginated(String search, List<String> statuses,
List<String> models, List<String> agentStatuses,
Pageable pageable) {
Specification<Atm> spec = AtmSpecification.withFilters(search, statuses, models, agentStatuses);
return atmRepository.findAll(spec, pageable).map(this::mapToDto);
}
5. Update Frontend API Interface
File: /source/hiveops-src/hiveops-incident/frontend/src/lib/api.ts
Add agentStatus parameter to API interface (lines 156-157):
getPaginated: (params: PaginationParams & {
search?: string;
status?: string[];
model?: string[];
agentStatus?: string[]
}) =>
6. Send Agent Filter from Frontend
File: /source/hiveops-src/hiveops-incident/frontend/src/components/AtmManagement/AtmList.svelte
Change 1: Update fetchAtms() function (lines 32-44) to send agentFilter:
function fetchAtms() {
const params: any = {
page: currentPage,
size: pageSize,
sort: `${sortField},${sortDirection}`,
};
if (searchQuery) params.search = searchQuery;
const statuses = [...statusFilter].map(s => s.toUpperCase());
if (statuses.length > 0) params.status = statuses;
const models = [...modelFilter];
if (models.length > 0) params.model = models;
const agentStatuses = [...agentFilter]; // NEW
if (agentStatuses.length > 0) params.agentStatus = agentStatuses; // NEW
loadAtmsPaginated(params);
}
Change 2: Update reactive statement (line 146) to re-fetch on agent filter change:
$: if (statusFilter || modelFilter || agentFilter) {
Verification
Manual Testing
- Start backend and frontend servers
- Navigate to ATM List page
- Test single agent filter: Select "Connected" only → verify only ATMs with heartbeat ≤ 5 min show
- Test multiple selections: Select "Connected" + "Disconnected" → verify all ATMs except never_connected show
- Test "Never Connected" → verify only ATMs with null lastHeartbeat show
- Test combined filters: Select agent filter + status filter → verify both filters apply
- Verify pagination works with agent filter active
- Verify "Clear all" button clears agent filter
- Check browser console for API requests → confirm
agentStatusparameter is sent
Backend Testing
Check database query performance with agent filtering:
-- Verify query plan includes join to atm_properties
EXPLAIN SELECT * FROM atms
LEFT JOIN atm_properties ON atms.id = atm_properties.atm_id
WHERE atm_properties.last_heartbeat > NOW() - INTERVAL 5 MINUTE;
Files to Modify
/source/hiveops-src/hiveops-incident/backend/src/main/java/com/hiveops/incident/entity/Atm.java/source/hiveops-src/hiveops-incident/backend/src/main/java/com/hiveops/incident/specification/AtmSpecification.java/source/hiveops-src/hiveops-incident/backend/src/main/java/com/hiveops/incident/controller/AtmController.java/source/hiveops-src/hiveops-incident/backend/src/main/java/com/hiveops/incident/service/AtmService.java/source/hiveops-src/hiveops-incident/frontend/src/lib/api.ts/source/hiveops-src/hiveops-incident/frontend/src/components/AtmManagement/AtmList.svelte