Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ server/data/unsupported-plugins.json
server/data/*.sqlite
server/data/*.sqlite-shm
server/data/*.sqlite-wal
.worktrees/
24 changes: 24 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import './App.css';
import { ScanProvider, useScanShellContext } from './context/ScanContext.jsx';

const loadScanPage = () => import('./components/pages/ScanPage.jsx');
const loadDomainsPage = () => import('./components/pages/DomainsPage.jsx');
const loadAdminPage = () => import('./components/pages/AdminPage.jsx');
const loadHistoryPage = () => import('./components/pages/HistoryPage.jsx');

const ScanPage = lazy(loadScanPage);
const DomainsPage = lazy(loadDomainsPage);
const AdminPage = lazy(loadAdminPage);
const HistoryPage = lazy(loadHistoryPage);

Expand All @@ -33,6 +35,10 @@ function AppContent() {
void loadHistoryPage();
return;
}
if (page === 'domains') {
void loadDomainsPage();
return;
}
if (page === 'admin') {
void loadAdminPage();
}
Expand All @@ -53,6 +59,16 @@ function AppContent() {
>
Current scan
</button>
<button
type="button"
className={`app__nav-link ${activePage === 'domains' ? 'is-active' : ''}`}
onClick={() => setActivePage('domains')}
onMouseEnter={() => prefetchPage('domains')}
onFocus={() => prefetchPage('domains')}
aria-current={activePage === 'domains' ? 'page' : undefined}
>
Domains
</button>
<button
type="button"
className={`app__nav-link ${activePage === 'history' ? 'is-active' : ''}`}
Expand Down Expand Up @@ -128,6 +144,14 @@ function AppContent() {
);
}

if (activePage === 'domains') {
return (
<Suspense fallback={<PageLoadingState label="Loading domains workspace..." />}>
<DomainsPage headerActions={headerActions} />
</Suspense>
);
}

return (
<Suspense fallback={<PageLoadingState label="Loading scanner..." />}>
<ScanPage headerActions={headerActions} />
Expand Down
63 changes: 63 additions & 0 deletions frontend/src/__tests__/hooks/useDomainTrust.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { mapTrustSnapshot } from '../../services/trust.js';
import { useDomainTrust } from '../../hooks/useDomainTrust.js';
import { setWarningStatus } from '../../api/trust.js';

vi.mock('../../api/trust.js', () => ({
fetchDomainTrust: vi.fn(async () => ({
envelope: { envelopeId: 'env_12' },
warnings: [{ id: 12, status: 'open', severity: 'warn' }]
})),
setWarningStatus: vi.fn(async (_id, status) => ({ warning: { id: 12, status } })),
}));

function createWrapper() {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});

function Wrapper({ children }) {
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}

return Wrapper;
}

describe('mapTrustSnapshot', () => {
it('returns unknown state when no envelope exists', () => {
const mapped = mapTrustSnapshot({ warnings: [] });
expect(mapped.status).toBe('unknown');
});

it('maps unresolved warnings into warning trust state', () => {
const mapped = mapTrustSnapshot({
envelope: { envelopeId: 'env_1' },
warnings: [{ status: 'open', severity: 'warn' }]
});
expect(mapped.status).toBe('warning');
expect(mapped.unresolvedCount).toBe(1);
});
});

describe('useDomainTrust', () => {
it('loads trust state and updates warning status', async () => {
const { result } = renderHook(() => useDomainTrust('example.com'), {
wrapper: createWrapper(),
});

await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});

expect(result.current.trust.status).toBe('warning');

await result.current.updateWarningStatus({ id: 12, status: 'resolved' });

expect(setWarningStatus).toHaveBeenCalledWith(12, 'resolved');
});
});
69 changes: 69 additions & 0 deletions frontend/src/api/trust.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { request } from './client.js';

export async function fetchDomainTrust(domain) {
const result = await request(`/api/admin/trust/domains/${encodeURIComponent(domain)}`);
if (!result.ok) {
throw new Error(result.data?.error ?? 'Failed to load domain trust');
}
return result.data;
}

export async function createTrustEnvelope(payload) {
const result = await request('/api/admin/trust/envelopes', {
method: 'POST',
body: JSON.stringify(payload),
});

if (!result.ok) {
throw new Error(result.data?.error ?? 'Failed to persist trust envelope');
}

return result.data;
}

export async function evaluateTrustEnvelope(payload) {
const result = await request('/api/admin/trust/evaluate', {
method: 'POST',
body: JSON.stringify(payload),
});

if (!result.ok) {
throw new Error(result.data?.error ?? 'Failed to evaluate trust warnings');
}

return result.data;
}

export async function setWarningStatus(id, status) {
const result = await request(`/api/admin/trust/warnings/${id}`, {
method: 'PUT',
body: JSON.stringify({ status }),
});

if (!result.ok) {
throw new Error(result.data?.error ?? 'Failed to update warning status');
}

return result.data;
}

export async function createDeepAuditJob(payload) {
const result = await request('/api/deep-audit/jobs', {
method: 'POST',
body: JSON.stringify(payload),
});

if (!result.ok) {
throw new Error(result.data?.error ?? 'Failed to queue deep audit job');
}

return result.data;
}

export async function fetchDeepAuditJob(jobId) {
const result = await request(`/api/deep-audit/jobs/${encodeURIComponent(jobId)}`);
if (!result.ok) {
throw new Error(result.data?.error ?? 'Failed to load deep audit job');
}
return result.data;
}
Loading