Add activity logs, sidebar badges, and logout with working alert colors.

Record auth audits and surface them in Activity, poll sidebar counts without resetting idle, and add a profile logout path plus theme red/green/blue tokens so failure badges render.
This commit is contained in:
2026-07-18 21:43:44 +02:00
parent e3793f380c
commit 463aa9a7a3
32 changed files with 3282 additions and 171 deletions
+79
View File
@@ -9,6 +9,7 @@ import {
fetchGroups,
fetchHealth,
fetchNetwork,
fetchAuthLogs,
fetchNodeLogs,
fetchSecurity,
fetchStatus,
@@ -396,6 +397,51 @@ describe('fetchNodeLogs', () => {
})
})
describe('fetchAuthLogs', () => {
afterEach(() => {
vi.restoreAllMocks()
})
it('returns parsed auth logs payload', async () => {
const events = [
{
id: 'a1',
at: '2026-01-01T00:00:00Z',
action: 'login',
outcome: 'success',
category: 'auth',
actor: 'Admin',
target: 'Admin',
},
]
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ events }),
})
const payload = await fetchAuthLogs(fetchMock)
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:8080/api/v1/auth-logs',
expect.objectContaining({ credentials: 'include' }),
)
expect(payload.events).toEqual(events)
})
it('sends idle-exempt header when requested', async () => {
const { SESSION_IDLE_EXEMPT_HEADER } = await import('./client')
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ events: [] }),
})
await fetchAuthLogs(fetchMock, { idleExempt: true })
const init = fetchMock.mock.calls[0][1] as { headers: Headers }
expect(init.headers.get(SESSION_IDLE_EXEMPT_HEADER)).toBe('1')
})
})
describe('fetchSecurity', () => {
afterEach(() => {
vi.restoreAllMocks()
@@ -565,6 +611,39 @@ describe('nodes API', () => {
)
})
it('sends idle-exempt header when requested', async () => {
const { fetchNodes, SESSION_IDLE_EXEMPT_HEADER } = await import('./client')
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ nodes: [] }),
})
await fetchNodes(undefined, fetchMock, { idleExempt: true })
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:8080/api/v1/nodes',
expect.objectContaining({
credentials: 'include',
headers: expect.any(Headers),
}),
)
const init = fetchMock.mock.calls[0][1] as { headers: Headers }
expect(init.headers.get(SESSION_IDLE_EXEMPT_HEADER)).toBe('1')
})
it('omits idle-exempt header by default', async () => {
const { fetchNodes, SESSION_IDLE_EXEMPT_HEADER } = await import('./client')
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ nodes: [] }),
})
await fetchNodes(undefined, fetchMock)
const init = fetchMock.mock.calls[0][1] as { headers: Headers }
expect(init.headers.get(SESSION_IDLE_EXEMPT_HEADER)).toBeNull()
})
it('creates a node with generate options', async () => {
const { createNode } = await import('./client')
const fetchMock = vi.fn().mockResolvedValue({