feat(admin): frontend dashboard complet pour #71
CI / lint (push) Successful in 36s
CI / security (push) Successful in 23s
CI / test (push) Successful in 46s
CI / build (push) Successful in 22s
CI / e2e (push) Successful in 5m43s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s

Page admin standalone + widgets temps réel + CRUD users via AdminPanel existant.

- frontend/admin.html : page admin (header + 4 sections : stats temps réel,
  audit logs, backups, gestion utilisateurs + footer)
- frontend/js/admin.js : module ES avec init(), connectSSE(), loadStatsOnce(),
  loadAuditLogs(), loadBackupStats(), renderStatsWidget(), renderAuditTable(),
  renderBackups() + helpers (formatBytes, formatUptime, severityColor)
- frontend/locales/{fr,en}.json : 36 clés admin.* identiques (diff vérifié)
- frontend/js/auth.js : adminRow.onclick redirige vers /admin.html (au lieu de
  AdminPanel.show() qui reste fonctionnel)
- tests/frontend/unit.test.mjs : 3 nouveaux tests (admin.js existe + exports + parse)
- tests/test_auth_api.py : 2 nouveaux tests smoke (PATCH + DELETE /api/auth/admin/users)

Vérifié :
- pytest : 494 passed, 5 skipped (492 baseline + 2 nouveaux)
- frontend unit : 7 passed (4 baseline + 3 admin)
- validate-imports : 30 modules validated (29 + admin.js)
- pane-manager JSDOM : 9/9 passed
- ruff check backend/ : All checks passed
This commit is contained in:
2026-09-07 11:14:46 -04:00
parent 3908e1601a
commit 46be24f6a3
7 changed files with 995 additions and 1 deletions
+63
View File
@@ -127,6 +127,66 @@ function testModulesHaveImports() {
console.log(' ✓ All modules have imports and exports (except state.js)');
}
// ── Test admin.js exports (ROADMAP #71) ─────────────────────────────────
// Static analysis of frontend/js/admin.js — confirms the ES module shape
// without depending on a DOM environment.
const ADMIN_PATH = join(JS_DIR, 'admin.js');
const ADMIN_REQUIRED_EXPORTS = [
'init',
'connectSSE',
'loadStatsOnce',
'loadAuditLogs',
'loadBackupStats',
'renderStatsWidget',
'renderAuditTable',
'renderBackups',
'getAuthHeaders',
'formatBytes',
'formatUptime',
'severityColor',
];
function _collectExportedNames(content) {
const names = new Set();
// Multi-line export { ... } blocks
for (const m of content.matchAll(/^export\s*\{([\s\S]*?)\}/gm)) {
for (const part of m[1].split(',')) {
const n = part.trim().split(/\s+as\s+/)[0].trim();
if (n) names.add(n);
}
}
// export function/const/let/class/async function NAME
for (const m of content.matchAll(/^export\s+(?:async\s+)?(?:function|const|let|class)\s+(\w+)/gm)) {
names.add(m[1]);
}
return names;
}
function testAdminModuleExists() {
const content = readFileSync(ADMIN_PATH, 'utf-8');
assert.ok(content.length > 0, 'admin.js must be a non-empty file');
// Sanity: it must import from auth.js, state.js, utils.js, i18n.js
assert.ok(/from\s+['"]\.\/auth\.js['"]/.test(content), 'admin.js must import from ./auth.js');
assert.ok(/from\s+['"]\.\/state\.js['"]/.test(content), 'admin.js must import from ./state.js');
assert.ok(/from\s+['"]\.\/utils\.js['"]/.test(content), 'admin.js must import from ./utils.js');
assert.ok(/from\s+['"]\.\/i18n\.js['"]/.test(content), 'admin.js must import from ./i18n.js');
console.log(' ✓ admin.js exists and imports the expected modules');
}
function testAdminModuleExports() {
const content = readFileSync(ADMIN_PATH, 'utf-8');
const exported = _collectExportedNames(content);
const missing = ADMIN_REQUIRED_EXPORTS.filter((name) => !exported.has(name));
assert.strictEqual(missing.length, 0, `admin.js missing exports: ${missing.join(', ')}`);
console.log(` ✓ admin.js exports all ${ADMIN_REQUIRED_EXPORTS.length} expected names`);
}
function testAdminModuleSyntax() {
// node --check verifies parseability without executing side effects.
execSync(`node --check "${ADMIN_PATH}"`, { stdio: 'pipe' });
console.log(' ✓ admin.js parses without syntax errors');
}
// ── Run all tests ──────────────────────────────────────────────────────────
async function main() {
let passed = 0, failed = 0;
@@ -136,6 +196,9 @@ async function main() {
['state keys', testStateKeys],
['module syntax', testAllModulesParse],
['module structure', testModulesHaveImports],
['admin module exists', testAdminModuleExists],
['admin module exports', testAdminModuleExports],
['admin module syntax', testAdminModuleSyntax],
];
for (const [name, fn] of tests) {
+46
View File
@@ -221,6 +221,52 @@ class TestAdmin:
data = resp.json()
assert data["username"] == "testuser"
def test_patch_user(self, auth_client):
"""PATCH /api/auth/admin/users/{username} updates fields."""
token = self._login_admin(auth_client)
# Create a user first, then patch it.
auth_client.post("/api/auth/admin/users", headers={
"Authorization": f"Bearer {token}",
}, json={
"username": "patchuser",
"password": "origpass",
"role": "user",
"vaults": ["TestVault"],
})
resp = auth_client.patch("/api/auth/admin/users/patchuser", headers={
"Authorization": f"Bearer {token}",
}, json={
"display_name": "Patched User",
"active": False,
})
assert resp.status_code == 200
data = resp.json()
assert data["display_name"] == "Patched User"
assert data["active"] is False
def test_delete_user(self, auth_client):
"""DELETE /api/auth/admin/users/{username} removes the user."""
token = self._login_admin(auth_client)
# Create a throwaway user, then delete it.
auth_client.post("/api/auth/admin/users", headers={
"Authorization": f"Bearer {token}",
}, json={
"username": "deleteuser",
"password": "delpass",
"role": "user",
"vaults": ["TestVault"],
})
resp = auth_client.delete("/api/auth/admin/users/deleteuser", headers={
"Authorization": f"Bearer {token}",
})
assert resp.status_code == 200
# Confirm it's gone via list.
list_resp = auth_client.get("/api/auth/admin/users", headers={
"Authorization": f"Bearer {token}",
})
usernames = [u["username"] for u in list_resp.json()]
assert "deleteuser" not in usernames
def test_logout(self, auth_client):
token = self._login_admin(auth_client)
resp = auth_client.post("/api/auth/logout", headers={