# Implementation Plan: ATM Auto-Registration and Management UI ## Overview Implement three key features: 1. **Auto-register ATMs** when hiveops-agent communicates for the first time 2. **Add "ATM Management" menu** to the frontend navigation 3. **Create ATM list view** showing all ATMs with agent connection status 4. **Enable manual ATM creation** from the frontend ## Architecture Decisions ### Auto-Registration Strategy - Modify existing `POST /api/journal-events` endpoint to accept BOTH database ID (Long) AND agent identifier (String) - When agent identifier is provided, look up or auto-create ATM - Auto-create ATM with defaults if not found: location="Unknown", address="Auto-registered", model="Unknown" - Maintains backward compatibility - existing UI calls using database ID continue to work - `/api/agent` base path reserved for future agent-related functionality (not used in this implementation) ### Connection Status Tracking - Use last-seen timestamp approach via `AtmProperties.lastHeartbeat` field - Update timestamp on every agent communication (journal events, config sync) - Calculate connection status dynamically: - Connected: lastHeartbeat within 5 minutes - Disconnected: lastHeartbeat older than 5 minutes - Never Connected: lastHeartbeat is null - Add computed `agentConnectionStatus` field to `AtmDTO` ## Backend Changes ### 1. Modified and New DTOs #### File: `backend/src/main/java/com/hiveops/incident/dto/CreateJournalEventRequest.java` (MODIFY) Modify existing DTO to support both database ID and agent identifier: ```java @Data @NoArgsConstructor @AllArgsConstructor @Builder public class CreateJournalEventRequest { private Long atmId; // Database ID (for UI/existing integrations) private String agentAtmId; // Agent identifier e.g., "ATM-001" (for hiveops-agent) private Long incidentId; private String eventType; private String eventDetails; private Integer cardReaderSlot; private String cardReaderStatus; private String cassetteType; private Integer cassetteFillLevel; private Integer cassetteBillCount; private String cassetteCurrency; private String eventSource; } ``` **Validation**: Either `atmId` OR `agentAtmId` must be provided (not both, not neither) #### File: `backend/src/main/java/com/hiveops/incident/dto/CreateAtmRequest.java` (NEW) ```java @Data @NoArgsConstructor @AllArgsConstructor @Builder public class CreateAtmRequest { @NotBlank private String atmId; // Unique identifier @NotBlank private String location; @NotBlank private String address; @NotBlank private String model; private Double latitude; private Double longitude; } ``` #### File: `backend/src/main/java/com/hiveops/incident/dto/AtmDTO.java` (MODIFY) Add two new fields: ```java private String agentConnectionStatus; // "CONNECTED", "DISCONNECTED", "NEVER_CONNECTED" private LocalDateTime lastHeartbeat; ``` ### 2. Service Layer Changes #### File: `backend/src/main/java/com/hiveops/incident/service/AtmService.java` (MODIFY) **Add new methods:** ```java @Transactional public Atm findOrCreateAtm(String agentAtmId) { return atmRepository.findByAtmId(agentAtmId) .orElseGet(() -> autoRegisterAtm(agentAtmId)); } @Transactional public void updateLastHeartbeat(Atm atm) { AtmProperties props = atmPropertiesRepository.findByAtmId(atm.getId()) .orElseGet(() -> { AtmProperties newProps = new AtmProperties(); newProps.setAtm(atm); return atmPropertiesRepository.save(newProps); }); props.setLastHeartbeat(LocalDateTime.now()); atmPropertiesRepository.save(props); } private Atm autoRegisterAtm(String agentAtmId) { Atm atm = Atm.builder() .atmId(agentAtmId) .location("Unknown") .address("Auto-registered - pending configuration") .model("Unknown") .build(); logger.info("Auto-registering new ATM: {}", agentAtmId); return atmRepository.save(atm); } @Transactional public AtmDTO createAtm(CreateAtmRequest request) { if (atmRepository.findByAtmId(request.getAtmId()).isPresent()) { throw new RuntimeException("ATM with ID " + request.getAtmId() + " already exists"); } Atm atm = Atm.builder() .atmId(request.getAtmId()) .location(request.getLocation()) .address(request.getAddress()) .model(request.getModel()) .latitude(request.getLatitude()) .longitude(request.getLongitude()) .build(); Atm saved = atmRepository.save(atm); return mapToDto(saved); } @Transactional public AtmDTO updateAtm(Long id, CreateAtmRequest request) { Atm atm = atmRepository.findById(id) .orElseThrow(() -> new RuntimeException("ATM not found")); // Cannot change atmId (unique identifier) atm.setLocation(request.getLocation()); atm.setAddress(request.getAddress()); atm.setModel(request.getModel()); atm.setLatitude(request.getLatitude()); atm.setLongitude(request.getLongitude()); Atm saved = atmRepository.save(atm); return mapToDto(saved); } @Transactional public void deleteAtm(Long id) { Atm atm = atmRepository.findById(id) .orElseThrow(() -> new RuntimeException("ATM not found")); atm.setStatus(AtmStatus.INACTIVE); // Soft delete atmRepository.save(atm); } ``` **Modify existing method `mapToDto(Atm atm)` at line 89:** ```java private AtmDTO mapToDto(Atm atm) { // Get lastHeartbeat from properties LocalDateTime lastHeartbeat = atmPropertiesRepository.findByAtmId(atm.getId()) .map(AtmProperties::getLastHeartbeat) .orElse(null); // Calculate connection status String connectionStatus = calculateConnectionStatus(lastHeartbeat); return AtmDTO.builder() .id(atm.getId()) .atmId(atm.getAtmId()) .location(atm.getLocation()) .address(atm.getAddress()) .status(atm.getStatus().name()) .latitude(atm.getLatitude()) .longitude(atm.getLongitude()) .model(atm.getModel()) .lastServiceDate(atm.getLastServiceDate()) .createdAt(atm.getCreatedAt()) .updatedAt(atm.getUpdatedAt()) .agentConnectionStatus(connectionStatus) .lastHeartbeat(lastHeartbeat) .build(); } private String calculateConnectionStatus(LocalDateTime lastHeartbeat) { if (lastHeartbeat == null) { return "NEVER_CONNECTED"; } long minutesAgo = java.time.Duration.between(lastHeartbeat, LocalDateTime.now()).toMinutes(); return minutesAgo <= 5 ? "CONNECTED" : "DISCONNECTED"; } ``` #### File: `backend/src/main/java/com/hiveops/incident/service/JournalEventService.java` (MODIFY) **Add dependency injection in constructor:** ```java private final AtmService atmService; ``` **Modify existing `createEvent` method (replace lines 25-46):** ```java public JournalEventDTO createEvent(CreateJournalEventRequest request) { Atm atm; // Support both database ID and agent identifier if (request.getAgentAtmId() != null && !request.getAgentAtmId().isEmpty()) { // Agent identifier provided - find or auto-register atm = atmService.findOrCreateAtm(request.getAgentAtmId()); atmService.updateLastHeartbeat(atm); } else if (request.getAtmId() != null) { // Database ID provided (existing behavior) atm = atmRepository.findById(request.getAtmId()) .orElseThrow(() -> new RuntimeException("ATM not found")); } else { throw new RuntimeException("Either atmId or agentAtmId must be provided"); } JournalEvent event = JournalEvent.builder() .atm(atm) .incident(request.getIncidentId() != null ? incidentRepository.findById(request.getIncidentId()).orElse(null) : null) .eventType(EventType.valueOf(request.getEventType())) .eventDetails(request.getEventDetails()) .cardReaderSlot(request.getCardReaderSlot()) .cardReaderStatus(request.getCardReaderStatus()) .cassetteType(request.getCassetteType()) .cassetteFillLevel(request.getCassetteFillLevel()) .cassetteBillCount(request.getCassetteBillCount()) .cassetteCurrency(request.getCassetteCurrency()) .eventSource(request.getEventSource() != null ? request.getEventSource() : "MANUAL") .build(); JournalEvent saved = journalEventRepository.save(event); return mapToDto(saved); } ``` ### 3. Controller Changes #### File: `backend/src/main/java/com/hiveops/incident/controller/JournalEventController.java` (NO CHANGES) The existing `POST /api/journal-events` endpoint will automatically support both formats through the modified `CreateJournalEventRequest` DTO and updated service logic. No controller changes needed. **Endpoint behavior:** - UI sends: `{ "atmId": 123, ... }` - works as before - Agent sends: `{ "agentAtmId": "ATM-001", ... }` - auto-registers if needed #### File: `backend/src/main/java/com/hiveops/incident/controller/AtmController.java` (MODIFY) Add CRUD endpoints: ```java @PostMapping public ResponseEntity createAtm(@Valid @RequestBody CreateAtmRequest request) { AtmDTO atm = atmService.createAtm(request); return ResponseEntity.ok(atm); } @PutMapping("/{id}") public ResponseEntity updateAtm(@PathVariable Long id, @Valid @RequestBody CreateAtmRequest request) { AtmDTO atm = atmService.updateAtm(id, request); return ResponseEntity.ok(atm); } @DeleteMapping("/{id}") public ResponseEntity deleteAtm(@PathVariable Long id) { atmService.deleteAtm(id); return ResponseEntity.noContent().build(); } ``` ## Frontend Changes ### 1. API Client Updates #### File: `frontend/src/lib/api.ts` (MODIFY) Add to `atmAPI` object: ```typescript create: (data: Partial) => api.post('/atms', data), update: (id: number, data: Partial) => api.put(`/atms/${id}`, data), delete: (id: number) => api.delete(`/atms/${id}`), ``` ### 2. New Components #### File: `frontend/src/components/AtmManagement/index.ts` (NEW) ```typescript export { default } from './AtmManagement.svelte'; ``` #### File: `frontend/src/components/AtmManagement/AtmManagement.svelte` (NEW) Container component with tabs for List and Create views: ```svelte
{#if activeTab === 'list'} {:else if activeTab === 'create'} {/if}
``` #### File: `frontend/src/components/AtmManagement/AtmList.svelte` (NEW) ATM list with connection status indicators: - Table showing: ID, ATM ID, Location, Model, Status, Agent Status, Last Heartbeat, Actions - Connection status dot (green=connected, red=disconnected, gray=never) - Search/filter functionality - Edit/Delete actions - Follow pattern from `IncidentList.svelte` Key helper function: ```typescript function getConnectionStatus(status: string, lastHeartbeat: string | null) { if (status === 'NEVER_CONNECTED') { return { label: 'Never Connected', color: '#9ca3af', dot: '⚫' }; } if (status === 'CONNECTED') { return { label: 'Connected', color: '#10b981', dot: '🟢' }; } return { label: 'Disconnected', color: '#ef4444', dot: '🔴' }; } ``` #### File: `frontend/src/components/AtmManagement/CreateAtm.svelte` (NEW) Manual ATM creation form with fields: - ATM ID (text, required, unique) - Location (text, required) - Address (text, required) - Model (text, required) - Latitude (number, optional) - Longitude (number, optional) Follow pattern from `CreateIncident.svelte` with form validation, loading states, and success messages. ### 3. Navigation Updates #### File: `frontend/src/App.svelte` (MODIFY) **Add state variables (around line 20-30):** ```typescript let atmManagementExpanded = false; let atmManagementTab: 'list' | 'create' = 'list'; const atmManagementTabs = [ { id: 'list', label: 'ATM List', icon: '📋' }, { id: 'create', label: 'Add ATM', icon: '➕' }, ]; function selectAtmManagementTab(tabId: string) { atmManagementTab = tabId as 'list' | 'create'; currentView = 'atm-management'; } ``` **Add menu section (insert between ATM Properties and Fleet Management, around line 173):** ```svelte ``` **Add import (around line 10):** ```typescript import AtmManagement from './components/AtmManagement'; ``` **Add route (in main content section, around line 245):** ```svelte {:else if currentView === 'atm-management'}
``` ## Implementation Sequence ### Phase 1: Backend Auto-Registration 1. Modify `CreateJournalEventRequest.java` to add `agentAtmId` field 2. Add `findOrCreateAtm()`, `autoRegisterAtm()`, `updateLastHeartbeat()` to `AtmService` 3. Inject `AtmService` into `JournalEventService` constructor 4. Modify `createEvent()` method in `JournalEventService` to support both atmId formats 5. Test existing `/api/journal-events` endpoint with both formats ### Phase 2: Backend ATM Management 1. Create `CreateAtmRequest.java` 2. Modify `AtmDTO` to add `agentConnectionStatus` and `lastHeartbeat` fields 3. Add `createAtm()`, `updateAtm()`, `deleteAtm()` to `AtmService` 4. Modify `mapToDto()` and add `calculateConnectionStatus()` to `AtmService` 5. Add POST, PUT, DELETE endpoints to `AtmController` ### Phase 3: Frontend Components 1. Add `create`, `update`, `delete` methods to `atmAPI` in `api.ts` 2. Create `AtmList.svelte` component with connection status display 3. Create `CreateAtm.svelte` form component 4. Create `AtmManagement.svelte` container component 5. Create `index.ts` export file ### Phase 4: Frontend Navigation 1. Add state variables and tab configuration to `App.svelte` 2. Add import for `AtmManagement` component 3. Add menu section between ATM Properties and Fleet Management 4. Add route handler in main content section ## Critical Files **Backend:** - `backend/src/main/java/com/hiveops/incident/service/AtmService.java` - `backend/src/main/java/com/hiveops/incident/service/JournalEventService.java` - `backend/src/main/java/com/hiveops/incident/controller/AtmController.java` - `backend/src/main/java/com/hiveops/incident/dto/CreateJournalEventRequest.java` - `backend/src/main/java/com/hiveops/incident/dto/AtmDTO.java` **Frontend:** - `frontend/src/App.svelte` - `frontend/src/lib/api.ts` - `frontend/src/components/AtmManagement/` (new directory) ## Verification ### Test Auto-Registration ```bash # Send agent journal event with new ATM (using agent identifier) curl -X POST http://localhost:8080/api/journal-events \ -H "Content-Type: application/json" \ -d '{ "agentAtmId": "ATM-001", "eventType": "CARD_READER_DETECTED", "eventDetails": "Card reader initialized", "eventSource": "HIVEOPS_AGENT" }' # Verify ATM was auto-created curl http://localhost:8080/api/atms/search?query=ATM-001 # Test backward compatibility (using database ID) curl -X POST http://localhost:8080/api/journal-events \ -H "Content-Type: application/json" \ -d '{ "atmId": 1, "eventType": "CASSETTE_LOW", "eventDetails": "Cassette running low", "eventSource": "MANUAL" }' ``` ### Test Manual ATM Creation 1. Navigate to ATM Management > Add ATM 2. Fill form with: atmId="ATM-002", location="New York", address="123 Main St", model="Hyosung 2700" 3. Submit and verify ATM appears in list 4. Check connection status shows "Never Connected" (gray) ### Test Connection Status 1. Send journal event from existing ATM 2. Refresh ATM list 3. Verify connection status changes to "Connected" (green) 4. Wait 6 minutes 5. Verify status changes to "Disconnected" (red) ### Test Edit/Delete 1. Click edit on an ATM 2. Modify location and address 3. Save and verify changes 4. Click delete 5. Verify ATM status changes to INACTIVE