[verified] add admin AI provider settings UI

This commit is contained in:
Marco0300
2026-09-03 21:15:00 +02:00
parent 4f87ca93f6
commit d07dc77ca1
6 changed files with 62 additions and 20 deletions
+6 -4
View File
@@ -59,12 +59,12 @@ const expectedIds = [
'deleteSavedFilterBtn', 'bulkVerifyBtn', 'bulkRejectBtn', 'nextPageBtn', 'jobs', 'jobsList', 'jobDetailPanel',
'sources', 'sourcesList', 'sourceRecordsList', 'discoveryForm', 'discoveryWorkspace', 'directDiscoveryForm', 'directDiscoveryRunsState', 'directDiscoveryResultState', 'crmPipeline', 'pipelineBoard', 'crmActivity',
'interactionState', 'interactionForm', 'crmReports', 'pipelineReport', 'outcomesReport', 'activityReport',
'suppressionCenter', 'suppressionForm', 'suppressionState', 'outreachSettings', 'providerPolicyPanel',
'suppressionCenter', 'suppressionForm', 'suppressionState', 'outreachSettings', 'providerPolicyPanel', 'aiProviderSettings', 'aiProviderForm', 'aiProviderStatus', 'aiProviderRefreshBtn', 'testAiProviderBtn', 'saveAiProviderBtn',
'scoreRules', 'scoreRulesPanel', 'scoreDistributionPanel', 'mergeDialog', 'confirmMergeBtn'
];
const htmlIds = new Set([...html.matchAll(/\bid=["']([^"']+)["']/g)].map(m => m[1]));
check('dom.critical-ids', 'critical operator DOM IDs are present', all(expectedIds, id => htmlIds.has(id)), listMissing(expectedIds, id => htmlIds.has(id)).join(', '));
const expectedMarkers = ['saved-views', 'review-queue', 'crm-pipeline', 'crm-interactions', 'crm-reports', 'suppression-center', 'outreach-provider-policy', 'score-rules', 'score-distribution'];
const expectedMarkers = ['saved-views', 'review-queue', 'crm-pipeline', 'crm-interactions', 'crm-reports', 'suppression-center', 'outreach-provider-policy', 'ai-provider-settings', 'score-rules', 'score-distribution'];
const smokeMarkers = new Set([...html.matchAll(/data-smoke=["']([^"']+)["']/g)].map(m => m[1]));
check('dom.smoke-markers', 'critical sections expose stable smoke markers', all(expectedMarkers, marker => smokeMarkers.has(marker)), listMissing(expectedMarkers, marker => smokeMarkers.has(marker)).join(', '));
const dynamicMarkers = ['score-breakdown', 'deduplication'];
@@ -75,7 +75,7 @@ const routeContracts = [
'/api/v1/review-queue', '/api/v1/saved-filters', '/api/v1/businesses/bulk-review', '/api/v1/jobs', '/api/v1/sources',
'/api/v1/source-records', '/api/v1/discovery', '/api/v1/discovery-runs', '/api/v1/discovery-queries', '/api/v1/merge-history', '/api/v1/scoring/summary', '/api/v1/score-rules',
'/api/v1/pipeline-entries', '/api/v1/interactions', '/api/v1/reports/pipeline', '/api/v1/reports/outcomes', '/api/v1/reports/activity',
'/api/v1/suppressions', '/api/v1/ai-runs', '/api/v1/outreach/drafts', '/api/v1/outreach/provider-config',
'/api/v1/suppressions', '/api/v1/ai-runs', '/api/v1/outreach/drafts', '/api/v1/outreach/provider-config', '/api/v1/ai/provider-config', '/api/v1/admin/ai-provider-config/test',
'/matches', '/domains/check', '/domain-candidates', '/websites/scan', '/contacts/extract', '/score/recalculate', '/pipeline', '/verify'
];
check('routes.contracts', 'all critical API route contracts are referenced by the client', all(routeContracts, route => js.includes(route)), listMissing(routeContracts, route => js.includes(route)).join(', '));
@@ -105,7 +105,7 @@ if (manifestAssets) {
const linkedAssets = [...new Set([...html.matchAll(/(?:src|href)=["']([^"'#?]+)(?:\?[^"']*)?["']/gi)].map(m => m[1]).filter(asset => !/^(?:https?:|data:|#)/i.test(asset)).map(asset => asset.replace(/^\.\//, '')) )];
check('assets.linked-local', 'all local HTML assets exist and are non-empty', all(linkedAssets, asset => files[asset]?.length > 0), listMissing(linkedAssets, asset => files[asset]?.length > 0).join(', '));
check('css.responsive', 'responsive CSS covers mobile layouts and critical grids', /@media\s*\(\s*max-width\s*:\s*700px\s*\)/.test(css) && all(['.saved-view-controls', '.website-scan-grid', '.crm-two-col', '.reports-grid', '.outreach-panel', '.pipeline-board'], selector => css.includes(selector)));
check('css.responsive', 'responsive CSS covers mobile layouts and critical grids', /@media\s*\(\s*max-width\s*:\s*700px\s*\)/.test(css) && all(['.saved-view-controls', '.website-scan-grid', '.crm-two-col', '.reports-grid', '.outreach-panel', '.pipeline-board', '.ai-provider-grid'], selector => css.includes(selector)));
check('css.layout-contracts', 'critical desktop layout selectors are defined', all(['.app-shell', '.sidebar', '.workspace-grid', '.table-scroll', '.jobs-grid', '.sources-grid', '.score-config-row', '.provider-policy-row'], selector => css.includes(selector)));
const combined = `${html}\n${js}\n${css}\n${config}\n${text('README.md')}`;
@@ -127,6 +127,8 @@ const secretPatterns = [
];
const secretHits = secretPatterns.flatMap(pattern => [...combined.matchAll(new RegExp(pattern.source, `${pattern.flags.replace('g', '')}g`))].map(m => m[0].slice(0, 80)));
check('safety.no-hardcoded-secrets', 'frontend source has no obvious hardcoded secrets', secretHits.length === 0, secretHits.join(' | '));
check('ai-provider.write-only', 'AI provider keys are write-only and cleared after save', all(['type="password"', 'autocomplete="new-password"', "input.value=''", 'input[type="password"]'], token => combined.includes(token)) && !/localStorage[^\n]*(?:nous|firecrawl|api[_-]?key)/i.test(combined));
check('ai-provider.admin-states', 'AI provider settings expose safe status states and admin-only messaging', all(['/api/v1/ai/provider-config', '/api/v1/admin/ai-provider-config/test', 'Not configured', 'Invalid configuration', 'Administrator access required'], token => combined.includes(token)));
async function httpChecks(url) {
for (const asset of [...new Set([...(manifestAssets || []), ...linkedAssets])].sort()) {
+10 -6
View File
@@ -44,25 +44,25 @@ check('dom.operator-markers', 'operator-critical DOM markers are present', has(h
'id="loginScreen"', 'id="dashboardShell"', 'id="explorer"', 'id="detailPanel"',
'id="reviewQueueCount"', 'id="jobsList"', 'id="sourcesList"', 'id="pipelineBoard"',
'id="interactionState"', 'id="pipelineReport"', 'id="suppressionState"',
'id="providerPolicyPanel"', 'id="scoreRulesPanel"', 'id="scoreDistributionPanel"'
'id="providerPolicyPanel"', 'id="aiProviderSettings"', 'id="aiProviderForm"', 'id="aiProviderStatus"', 'id="scoreRulesPanel"', 'id="scoreDistributionPanel"'
]));
check('dom.safety-markers', 'safety and approval sections have stable smoke markers', [
'saved-views', 'review-queue', 'crm-pipeline', 'crm-interactions', 'crm-reports',
'suppression-center', 'outreach-provider-policy', 'score-rules', 'score-distribution'
'suppression-center', 'outreach-provider-policy', 'ai-provider-settings', 'score-rules', 'score-distribution'
].every(marker => markers.has(marker)));
check('dom.required-controls', 'operator controls have stable IDs', [
'loginEmail', 'loginPassword', 'logoutBtn', 'nextPageBtn', 'bulkVerifyBtn', 'bulkRejectBtn',
'savedFilterForm', 'pipelineViewToggle', 'interactionForm', 'suppressionForm', 'outreachPolicyRefreshBtn'
'savedFilterForm', 'pipelineViewToggle', 'interactionForm', 'suppressionForm', 'outreachPolicyRefreshBtn', 'aiProviderRefreshBtn', 'testAiProviderBtn', 'saveAiProviderBtn'
].every(id => htmlIds.has(id)) && has(js, [
'scanWebsiteBtn', 'extractContactsBtn', 'recalculateScoreBtn', 'generateAiSuggestionBtn', 'createOutreachDraftBtn'
]));
check('css.responsive', 'responsive CSS is present for mobile operator layouts',
/@media\s*\(max-width\s*:\s*700px\)/.test(css) &&
has(css, ['.saved-view-controls', '.website-scan-grid', '.crm-two-col', '.reports-grid', '.outreach-panel']));
has(css, ['.saved-view-controls', '.website-scan-grid', '.crm-two-col', '.reports-grid', '.outreach-panel', '.ai-provider-grid']));
check('css.layout-contracts', 'critical layout selectors are defined', has(css, [
'.app-shell', '.sidebar', '.workspace-grid', '.table-scroll', '.pipeline-board',
'.jobs-grid', '.sources-grid', '.score-config-row', '.provider-policy-row'
'.jobs-grid', '.sources-grid', '.score-config-row', '.provider-policy-row', '.ai-provider-status-row'
]));
check('safety.no-send-copy', 'persistent no-send and approval safety copy is present', has(`${html}\n${js}`, [
@@ -79,6 +79,10 @@ check('safety.no-secrets', 'frontend source has no obvious hardcoded secrets',
));
check('safety.approval-gated', 'approval is explicitly human-confirmed and non-delivering',
has(js, ['window.confirm', 'human_approval:true', 'send:false', 'Approval does not send a message.']));
check('ai-provider.write-only', 'AI provider credentials are write-only and never browser-persisted',
has(`${html}\n${js}`, ['type="password"', 'autocomplete="new-password"', "input.value=''", 'input[type="password"]']) && !/localStorage[^\n]*(?:nous|firecrawl|api[_-]?key)/i.test(`${html}\n${js}`));
check('ai-provider.states', 'AI provider status states and admin messaging are represented',
has(`${html}\n${js}`, ['/api/v1/ai/provider-config', '/api/v1/admin/ai-provider-config/test', 'Not configured', 'Invalid configuration', 'Administrator access required']));
const routes = [
'/api/v1/auth/me', '/api/v1/auth/login', '/api/v1/auth/logout', '/api/v1/businesses',
@@ -87,7 +91,7 @@ const routes = [
'/api/v1/merge-history', '/api/v1/scoring/summary', '/api/v1/score-rules',
'/api/v1/pipeline-entries', '/api/v1/interactions', '/api/v1/reports/pipeline',
'/api/v1/reports/outcomes', '/api/v1/reports/activity', '/api/v1/suppressions',
'/api/v1/ai-runs', '/api/v1/outreach/drafts', '/api/v1/outreach/provider-config'
'/api/v1/ai-runs', '/api/v1/outreach/drafts', '/api/v1/outreach/provider-config', '/api/v1/ai/provider-config', '/api/v1/admin/ai-provider-config/test'
];
check('routes.contracts', 'operator API route contracts are referenced by the client',
routes.every(route => js.includes(route)), routes.filter(route => !js.includes(route)).join(', '));