Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 6 additions & 101 deletions audits/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -107,110 +107,15 @@ <h2 id="networkTitle"><i class="fa-solid fa-network-wired heading-icon"></i>Adre
</div>
</div>

<h2 id="loadTitle"><i class="fa-solid fa-gauge-high heading-icon"></i>Charge moyenne</h2>
<div id="loadCard" class="card" aria-labelledby="loadTitle">
<div id="loadAvg" class="load-container">
<div class="load-main">
<div id="loadGauge" class="gauge" tabindex="0" role="img">
<svg viewBox="0 0 36 36">
<path class="bg" d="M18 2a16 16 0 1 1 0 32 16 16 0 1 1 0-32"/>
<path id="loadGaugePath" class="progress" d="M18 2a16 16 0 1 1 0 32 16 16 0 1 1 0-32" stroke-dasharray="0 100"/>
</svg>
<div class="gauge-value"><span id="load1Val">--</span></div>
</div>
<div class="gauge-label">sur 1 min <i id="loadTrend" class="trend fa-solid fa-chevron-right"></i></div>
</div>
<div class="load-cards">
<div id="load5Card" class="mini-card" tabindex="0">
<div class="mini-title"><span id="load5Dot" class="status-dot"></span>5 min</div>
<div class="mini-bar-row">
<div class="bar" id="load5Bar"><span id="load5Fill" class="fill"></span></div>
<span id="load5Val" class="mini-val">--</span>
</div>
<div id="load5Trend" class="mini-trend">--</div>
</div>
<div id="load15Card" class="mini-card" tabindex="0">
<div class="mini-title"><span id="load15Dot" class="status-dot"></span>15 min</div>
<div class="mini-bar-row">
<div class="bar" id="load15Bar"><span id="load15Fill" class="fill"></span></div>
<span id="load15Val" class="mini-val">--</span>
</div>
<div id="load15Trend" class="mini-trend">--</div>
</div>
</div>
<div class="services-title-row">
<h2><i class="fa-solid fa-list-check heading-icon"></i>Services actifs</h2>
<div class="services-actions">
<span id="servicesCount">0 service</span>
</div>
</div>

<h2 id="cpuTitle"><i class="fa-solid fa-gear heading-icon"></i>CPU</h2>
<section id="cpuSection" class="cpu"></section>

<h2><i class="fa-solid fa-memory heading-icon"></i>Mémoire RAM / Swap</h2>
<section id="memorySection" class="mem grid"></section>

<h2><i class="fa-solid fa-hard-drive heading-icon"></i>Disques</h2>
<div id="disksContainer" class="disk-grid"></div>

<div class="services-title-row">
<h2><i class="fa-solid fa-list-check heading-icon"></i>Services actifs</h2>
<div class="services-actions">
<span id="servicesCount">0 service</span>
</div>
</div>
<p class="services-help">Survolez un service pour voir sa description.</p>
<div class="block-wrapper">
<input type="text" id="serviceSearch" placeholder="Filtrer un service… ex. ssh" />
<div id="categoryFilters" class="filter-chips"></div>
<div class="services-toolbar">
<select id="serviceSort">
<option value="az">A→Z</option>
<option value="za">Z→A</option>
<option value="cat">Par catégorie</option>
</select>
</div>
<div id="servicesList" class="services-grid">
<div class="service-skeleton skeleton"></div>
<div class="service-skeleton skeleton"></div>
<div class="service-skeleton skeleton"></div>
</div>
<template id="tpl-service-item">
<div class="service-item" tabindex="0" aria-expanded="false">
<div class="service-main">
<span class="service-icon"></span>
<span class="service-name"></span>
<span class="service-badge"></span>
</div>
<div class="service-details">
<div>
<strong>Nom de l’unité :</strong>
<code class="service-unit"></code>
<button class="copy-btn small" title="Copier le nom">📋</button>
</div>
<div>
<strong>Type :</strong>
service
</div>
<div>
<strong>Description :</strong>
<span class="service-desc"></span>
</div>
</div>
</div>
</template>
<div id="servicesEmpty" class="empty hidden">
Aucun service ne correspond au filtre.
<button id="resetFilters" class="btn">Réinitialiser les filtres</button>
</div>
</div>

<div class="top-grid">
<div class="top-panel">
<h2><i class="fa-solid fa-fire heading-icon"></i>Top 5 processus - CPU</h2>
<div id="topCpu" class="report-card proc-list"></div>
</div>
<div class="top-panel">
<h2><i class="fa-solid fa-memory heading-icon"></i>Top 5 processus - RAM</h2>
<div id="topMem" class="report-card proc-list"></div>
</div>
<div id="servicesGrid" class="docker-grid"></div>
<div id="servicesEmpty" class="empty hidden">Aucun service actif</div>
</div>

<h2><i class="fa-brands fa-docker heading-icon"></i>Conteneurs Docker</h2>
Expand Down
73 changes: 0 additions & 73 deletions audits/scripts/__tests__/ui.test.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,5 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { fireEvent } from '@testing-library/dom';
import { jest } from '@jest/globals';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const html = fs.readFileSync(path.resolve(__dirname, '../../index.html'), 'utf8');
const bodyHtml = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i)[1];

describe('services UI', () => {
let ServiceStore;
let initServicesUI;
let renderServicesList;
let SERVICE_CATEGORIES;

beforeEach(async () => {
jest.resetModules();
document.body.innerHTML = bodyHtml;
const dataModule = await import('../modules/services/data.js');
ServiceStore = dataModule.default;
SERVICE_CATEGORIES = dataModule.SERVICE_CATEGORIES;
const uiModule = await import('../modules/services/ui.js');
initServicesUI = uiModule.initServicesUI;
renderServicesList = uiModule.renderServicesList;
Object.assign(navigator, {
clipboard: { writeText: jest.fn().mockResolvedValue(undefined) },
});
global.alert = jest.fn();
});

test('initServicesUI creates chips and handles clicks', () => {
ServiceStore.setData(['sshd', 'cron']);
initServicesUI();
renderServicesList();
const chips = document.querySelectorAll('#categoryFilters .filter-chip');
expect(chips.length).toBe(SERVICE_CATEGORIES.length);
const itemsBefore = document.querySelectorAll('#servicesList .service-item');
expect(itemsBefore.length).toBe(2);
const secChip = Array.from(chips).find((c) => c.textContent === 'Sécurité');
fireEvent.click(secChip);
expect(secChip.classList.contains('active')).toBe(false);
const itemsAfter = document.querySelectorAll('#servicesList .service-item');
expect(itemsAfter.length).toBe(1);
});

test('renderServicesList shows full list', () => {
ServiceStore.setData(['sshd', 'cron']);
initServicesUI();
renderServicesList();
const items = document.querySelectorAll('#servicesList .service-item');
expect(items.length).toBe(2);
expect(document.getElementById('servicesCount').textContent).toBe('2 services');
});
});

describe('showStatus', () => {
let showStatus;
Expand Down Expand Up @@ -107,22 +53,3 @@ describe('setupCopy', () => {
});
});

describe('toggleServiceItem', () => {
let toggleServiceItem;
beforeEach(async () => {
jest.resetModules();
({ toggleServiceItem } = await import('../modules/services/ui.js'));
});

test('toggles aria-expanded and class', () => {
const item = document.createElement('div');
item.className = 'service-item';
item.setAttribute('aria-expanded', 'false');
toggleServiceItem(item);
expect(item.classList.contains('expanded')).toBe(true);
expect(item.getAttribute('aria-expanded')).toBe('true');
toggleServiceItem(item);
expect(item.classList.contains('expanded')).toBe(false);
expect(item.getAttribute('aria-expanded')).toBe('false');
});
});
42 changes: 24 additions & 18 deletions audits/scripts/modules/__tests__/services.test.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import { describe, it, expect, beforeEach } from 'vitest';
import ServiceStore from '../services/data.js';
import { renderServices } from '../services.js';

describe('ServiceStore', () => {
describe('renderServices', () => {
beforeEach(() => {
ServiceStore.resetFilters();
ServiceStore.setData([]);
document.body.innerHTML = '<div id="servicesGrid"></div><div id="servicesEmpty" class="hidden"></div><span id="servicesCount"></span>';
});

it('classifies services', () => {
const result = ServiceStore.setData(['sshd', 'unknown']);
const ssh = result.find((s) => s.name === 'sshd');
const other = result.find((s) => s.name === 'unknown');
expect(ssh.category).toBe('Sécurité');
expect(other.category).toBe('Autre');
it('renders sorted services', () => {
renderServices([{ id: 'b.service' }, { id: 'a.service' }]);
const names = Array.from(document.querySelectorAll('.docker-name')).map((el) => el.textContent);
expect(names).toEqual(['a.service', 'b.service']);
expect(document.getElementById('servicesCount').textContent).toBe('2 services');
});

it('filters by category', () => {
ServiceStore.setData(['sshd', 'cron']);
ServiceStore.toggleCategory('Sécurité');
const filtered = ServiceStore.getFiltered().map((s) => s.name);
expect(filtered).toEqual(['cron']);
ServiceStore.toggleCategory('Sécurité');
const again = ServiceStore.getFiltered().map((s) => s.name);
expect(again).toContain('sshd');
it('sorts by unit_name when id missing', () => {
renderServices([{ unit_name: 'b.service' }, { unit_name: 'a.service' }]);
const names = Array.from(document.querySelectorAll('.docker-name')).map((el) => el.textContent);
expect(names).toEqual(['a.service', 'b.service']);
});

it('handles missing fields', () => {
renderServices([{}]);
const card = document.querySelector('.docker-card');
expect(card.querySelector('.docker-name').textContent).toBe('—');
});

it('normalizes string entries', () => {
renderServices(['ssh.service']);
const card = document.querySelector('.docker-card');
expect(card.querySelector('.docker-name').textContent).toBe('ssh.service');
});
});
14 changes: 14 additions & 0 deletions audits/scripts/modules/audits.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { renderServices } from './services.js';
import { renderDocker } from './docker.js';

function renderInfo(data = {}) {
const hostname = document.getElementById('hostname');
const generated = document.getElementById('generatedValue');
const ipLocal = document.getElementById('ipLocal');
const ipPublic = document.getElementById('ipPublic');
const uptime = document.getElementById('uptimeValue');
if (hostname) hostname.textContent = data.hostname || '-';
if (generated) generated.textContent = data.generated || '--';
if (ipLocal) ipLocal.textContent = data.ip_local || 'N/A';
if (ipPublic) ipPublic.textContent = data.ip_pub || 'N/A';
if (uptime) uptime.textContent = data.uptime || '--';
}

export let auditsIndex = [];
export let auditsMap = {};
export let latestEntry = null;
Expand Down Expand Up @@ -63,6 +76,7 @@ export async function init() {
return;
}
const data = await loadAudit(latestEntry.file);
renderInfo(data);
renderServices(data.services || []);
renderDocker(data.docker || []);
showStatus('');
Expand Down
Loading