fix(aria): IOC Feeds — restore app, fix API shapes, canonical UI
CD - Develop / build-and-deploy (push) Failing after 10m58s

Root cause: IocFeeds.svelte called feeds.find() on a Map (backend returned
Map<String,Object> not List) → reactive statement crash → entire Svelte tree
broken, all sidebar nav dead.

Backend:
- /feeds/status now returns List<Map> with feedName/configured/enabled/
  lastRunAt/lastRunStatus/lastIocsAdded/lastIocsUpdated fields
- /feeds/runs now returns full Page<IocFeedRun> (not stripped .getContent())
- IocFeedService.isConfigured(feedName) added for per-feed key check
- Feed order fixed to [OTX, MALWARE_BAZAAR, THREAT_FOX] (deterministic)

Frontend:
- loadFeeds: Array.isArray guard prevents crash if shape is ever wrong
- loadRuns: safe fallback for both Page and plain-array responses
- api.ts refreshFeed URL fixed: /feeds/refresh/{name} not /feeds/{name}/refresh
- IocFeedStatus type: added lastIocsUpdated
- IocFeeds.svelte: Refresh All moved from header to toolbar; all buttons
  use canonical btn/btn-primary/btn-secondary/btn-sm classes; pollInterval
  now calls pollStatus (status-only poll) not full refresh trigger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 22:41:21 -04:00
parent 583aff9c2d
commit 571d88db4d
4 changed files with 209 additions and 389 deletions
@@ -5,14 +5,13 @@ import com.hiveops.aria.repository.IocFeedRunRepository;
import com.hiveops.aria.service.IocFeedService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
@RestController
@RequestMapping("/api/aria/feeds")
@@ -20,6 +19,9 @@ import java.util.Optional;
@Slf4j
public class IocFeedController {
private final IocFeedService feedService;
private final IocFeedRunRepository feedRunRepository;
private Thread feedThread(String name, Runnable task) {
Thread t = new Thread(task, name);
t.setUncaughtExceptionHandler((thread, ex) ->
@@ -27,30 +29,36 @@ public class IocFeedController {
return t;
}
private final IocFeedService feedService;
private final IocFeedRunRepository feedRunRepository;
/** Latest run per feed — used by the status cards in the UI */
/** One status object per feed — array, matches IocFeedStatus[] in the frontend */
@GetMapping("/status")
@PreAuthorize("hasRole('BCOS_ADMIN')")
public ResponseEntity<Map<String, Object>> status() {
List<String> names = feedService.feedNames();
Map<String, Object> result = new java.util.LinkedHashMap<>();
for (String name : names) {
public ResponseEntity<List<Map<String, Object>>> status() {
List<Map<String, Object>> result = new ArrayList<>();
for (String name : feedService.feedNames()) {
Optional<IocFeedRun> last = feedRunRepository.findTopByFeedNameOrderByStartedAtDesc(name);
result.put(name, last.orElse(null));
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("feedName", name);
entry.put("configured", feedService.isConfigured(name));
entry.put("enabled", true);
last.ifPresent(r -> {
entry.put("lastRunAt", r.getCompletedAt() != null ? r.getCompletedAt() : r.getStartedAt());
entry.put("lastRunStatus", r.getStatus());
entry.put("lastIocsAdded", r.getIocsAdded());
entry.put("lastIocsUpdated", r.getIocsUpdated());
});
result.add(entry);
}
return ResponseEntity.ok(result);
}
/** Recent run history — default 50 rows */
/** Paginated run history — returns Spring Page so the frontend gets content + page metadata */
@GetMapping("/runs")
@PreAuthorize("hasRole('BCOS_ADMIN')")
public ResponseEntity<List<IocFeedRun>> runs(
public ResponseEntity<Page<IocFeedRun>> runs(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "50") int size) {
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(
feedRunRepository.findAllByOrderByStartedAtDesc(PageRequest.of(page, size)).getContent()
feedRunRepository.findAllByOrderByStartedAtDesc(PageRequest.of(page, size))
);
}
@@ -114,6 +114,15 @@ public class IocFeedService {
}
public List<String> feedNames() {
return List.copyOf(FEED_SOURCES.keySet());
return List.of("OTX", "MALWARE_BAZAAR", "THREAT_FOX");
}
public boolean isConfigured(String feedName) {
return switch (feedName) {
case "OTX" -> otxClient.isConfigured();
case "MALWARE_BAZAAR" -> malwareBazaarClient.isConfigured();
case "THREAT_FOX" -> threatFoxClient.isConfigured();
default -> false;
};
}
}