diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md
new file mode 100644
index 0000000..2d39e36
--- /dev/null
+++ b/frontend/CLAUDE.md
@@ -0,0 +1,43 @@
+# hiveops-aria frontend — CLAUDE.md
+
+## Before writing ANY new component
+
+Read an existing sibling component first. Mandatory. Use `IocManagement.svelte` or `DevicePosture.svelte` as the reference.
+
+## Canonical patterns (copy exactly, no variations)
+
+**Header** — text only, no buttons:
+```svelte
+
+```
+
+**Toolbar** — all buttons go here, never in the header:
+```svelte
+
{/if}
- {#if !sidebarCollapsed}
-
- {/if}
{#if !sidebarCollapsed && userInfo}
@@ -131,6 +134,8 @@
{:else if currentView === 'posture'}
+ {:else if currentView === 'feeds'}
+
{/if}
@@ -184,7 +189,6 @@
.aria-brand-text { display: flex; flex-direction: column; }
.aria-name { font-size: 1.1rem; font-weight: 800; letter-spacing: 2px; color: white; }
.aria-sub { font-size: 0.65rem; color: rgba(255,255,255,0.55); letter-spacing: 0.3px; margin-top: 1px; }
- .sidebar-version { font-size: 0.7rem; color: rgba(255,255,255,0.4); padding-left: 2px; }
.sidebar-nav { display: flex; flex-direction: column; padding: 0.75rem 0; flex: 1; }
diff --git a/frontend/src/components/DevicePosture.svelte b/frontend/src/components/DevicePosture.svelte
index 5da8610..1a79bc7 100644
--- a/frontend/src/components/DevicePosture.svelte
+++ b/frontend/src/components/DevicePosture.svelte
@@ -2,18 +2,13 @@
import { onMount, onDestroy } from 'svelte';
import { ariaApi, type DeviceSecurityPosture, type OsEolStatus } from '../lib/api';
import { addToast } from '../lib/stores';
- import Pagination from './common/Pagination.svelte';
let postures: DeviceSecurityPosture[] = [];
let totalElements = 0;
- let totalPages = 0;
- let currentPage = 0;
- const pageSize = 25;
-
let loading = true;
// Filter sidebar
- let filterSidebarCollapsed = false;
+ let filterSidebarCollapsed = true;
let filterScore: '' | 'low' | 'medium' = '';
let filterEol: OsEolStatus | '' = '';
@@ -23,18 +18,56 @@
let panelOpen = false;
let panelDevice: DeviceSecurityPosture | null = null;
- async function load(page = 0) {
+ // Accordion — collapsed by default
+ let expandedInstitutions = new Set();
+
+ interface InstGroup {
+ key: string;
+ devices: DeviceSecurityPosture[];
+ avgScore: number | null;
+ hasIssues: boolean;
+ }
+
+ $: grouped = groupByInstitution(postures);
+
+ function groupByInstitution(list: DeviceSecurityPosture[]): InstGroup[] {
+ const map = new Map();
+ for (const d of list) {
+ const k = d.institutionKey ?? 'Unknown';
+ if (!map.has(k)) map.set(k, []);
+ map.get(k)!.push(d);
+ }
+ return Array.from(map.entries())
+ .sort((a, b) => a[0].localeCompare(b[0]))
+ .map(([key, devices]) => {
+ const scored = devices.filter(d => d.postureScore != null);
+ const avgScore = scored.length
+ ? Math.round(scored.reduce((s, d) => s + d.postureScore!, 0) / scored.length)
+ : null;
+ const hasIssues = devices.some(d => d.postureScore != null && d.postureScore < 80);
+ return { key, devices, avgScore, hasIssues };
+ });
+ }
+
+ function toggleInstitution(key: string) {
+ if (expandedInstitutions.has(key)) {
+ expandedInstitutions.delete(key);
+ } else {
+ expandedInstitutions.add(key);
+ }
+ expandedInstitutions = expandedInstitutions;
+ }
+
+ async function load() {
loading = true;
try {
- const params: Record = { page, size: pageSize };
+ const params: Record = { page: 0, size: 500 };
if (filterScore) params.scoreFilter = filterScore;
if (filterEol) params.osEolStatus = filterEol;
const res = await ariaApi.getPostures(params);
postures = res.data.content;
totalElements = res.data.page.totalElements;
- totalPages = res.data.page.totalPages;
- currentPage = res.data.page.number;
} catch {
addToast('error', 'Load Failed', 'Could not load device posture data');
} finally {
@@ -45,8 +78,7 @@
function clearAllFilters() {
filterScore = '';
filterEol = '';
- currentPage = 0;
- load(0);
+ load();
}
function openPanel(device: DeviceSecurityPosture) {
@@ -105,13 +137,13 @@
async function doRefresh() {
refreshing = true;
- await load(currentPage);
+ await load();
refreshing = false;
secondsLeft = REFRESH_SECONDS;
}
onMount(() => {
- load(0);
+ load();
tickInterval = setInterval(() => {
if (!autoRefreshEnabled) return;
if (secondsLeft > 1) {
@@ -168,7 +200,7 @@