${configured?'Credentials configured; values are never displayed.':status==='invalid'?'Configuration was rejected. Review the fields and try again.':status==='not-configured'||status==='disabled'?'No credentials are configured for this service.':'No safe connection result was returned.'}
`;}).join('')}
Secret values, key fingerprints, and response bodies are never shown.
`;
}
+ function updateAiProviderForm(){const step=$('aiProvider')?.value==='stepfun';const model=$('nousModel'),base=$('nousBaseUrl'),key=$('nousApiKey');if(model){model.previousElementSibling&&(model.previousElementSibling.textContent=step?'StepFun model':'Nous Portal model');model.placeholder=step?'step-3.7-flash':'Hermes 4 405B';}if(base){base.previousElementSibling&&(base.previousElementSibling.textContent=step?'StepFun base URL':'Nous Portal base URL');base.placeholder=step?'https://api.stepfun.ai/v1':'https://inference-api.nousresearch.com/v1';}if(key){key.previousElementSibling&&(key.previousElementSibling.textContent=step?'StepFun API key · write-only · leave blank to keep':'Nous Portal API key · write-only · leave blank to keep');}}
function setAiProviderAdminState() { const state=$('aiProviderAdminState'),form=$('aiProviderForm'),test=$('testAiProviderBtn'); if(!state)return; const editable=canEditAiProviders(); state.hidden=editable; if(!editable){state.innerHTML='Administrator access required
Your role can view safe provider status, but only an administrator can edit or test credentials.
';form?.setAttribute('hidden','');if(test)test.disabled=true;}else{form?.removeAttribute('hidden');if(test)test.disabled=false;} }
- function populateAiProviderFields(payload) { const source=payload?.nous_portal||payload?.provider_config||payload||{}; [['nousModel',source.model||source.nous_model],['nousBaseUrl',source.base_url||source.nous_base_url],['searxngBaseUrl',source.searxng_base_url||source.firecrawl_base_url||payload?.searxng?.base_url||payload?.firecrawl?.base_url]].forEach(([id,value])=>{if(value!==undefined&&$(id))$(id).value=value;}); ['nousApiKey'].forEach(id=>{if($(id))$(id).value='';}); }
+ function populateAiProviderFields(payload) { const source=payload?.nous_portal||payload?.provider_config||payload||{}; if(source.provider&&$("aiProvider"))$("aiProvider").value=source.provider; [['nousModel',source.model||source.nous_model],['nousBaseUrl',source.base_url||source.nous_base_url],['searxngBaseUrl',source.searxng_base_url||source.firecrawl_base_url||payload?.searxng?.base_url||payload?.firecrawl?.base_url]].forEach(([id,value])=>{if(value!==undefined&&$(id))$(id).value=value;}); ['nousApiKey'].forEach(id=>{if($(id))$(id).value='';}); updateAiProviderForm(); }
async function loadAiProviderSettings(){const state=$('aiProviderStatus');if(!state)return;setAiProviderAdminState();state.innerHTML='
Loading AI provider status…
';try{const payload=await jsonRequest('/api/v1/ai/provider-config');populateAiProviderFields(payload);renderAiProviderStatus(payload);}catch(error){if(error.message!=='unauthorized')renderAiProviderStatus({status:error.message.toLowerCase().includes('invalid')?'invalid':'error',message:error.message});}}
- async function saveAiProviderSettings(event){event.preventDefault();if(!canEditAiProviders()){message('aiProviderMessage','Administrator access is required to update provider settings.',true);return;}const form=event.currentTarget,data=Object.fromEntries(new FormData(form).entries()),body={provider:data.provider,model:data.nous_model.trim(),nous_base_url:data.nous_base_url.trim(),searxng_base_url:data.searxng_base_url.trim(),enabled:true,credentials:{}};if(data.nous_api_key)body.credentials.nous_api_key=data.nous_api_key;const button=$('saveAiProviderBtn');button.disabled=true;message('aiProviderMessage','Saving provider settings…');try{await jsonRequest('/api/v1/ai/provider-config',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});form.querySelectorAll('input[type="password"]').forEach(input=>input.value='');message('aiProviderMessage','Provider settings saved. Discovery and AI work were not started.');await loadAiProviderSettings();}catch(error){if(error.message!=='unauthorized')message('aiProviderMessage',error.message||'Unable to save provider settings.',true);}finally{button.disabled=false;}}
+ async function saveAiProviderSettings(event){event.preventDefault();if(!canEditAiProviders()){message('aiProviderMessage','Administrator access is required to update provider settings.',true);return;}const form=event.currentTarget,data=Object.fromEntries(new FormData(form).entries()),body={provider:data.provider,model:data.nous_model.trim(),nous_base_url:data.nous_base_url.trim(),searxng_base_url:data.searxng_base_url.trim(),enabled:true,credentials:{}};if(data.nous_api_key)body.credentials[data.provider==='stepfun'?'step_api_key':'nous_api_key']=data.nous_api_key;const button=$('saveAiProviderBtn');button.disabled=true;message('aiProviderMessage','Saving provider settings…');try{await jsonRequest('/api/v1/ai/provider-config',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});form.querySelectorAll('input[type="password"]').forEach(input=>input.value='');message('aiProviderMessage','Provider settings saved. Discovery and AI work were not started.');await loadAiProviderSettings();}catch(error){if(error.message!=='unauthorized')message('aiProviderMessage',error.message||'Unable to save provider settings.',true);}finally{button.disabled=false;}}
async function testAiProviderConnection(){if(!canEditAiProviders())return;const button=$('testAiProviderBtn');button.disabled=true;const state=$('aiProviderStatus');state.innerHTML='
Testing provider connection…
';try{const payload=await jsonRequest('/api/v1/admin/ai-provider-config/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({})});renderAiProviderStatus(payload);message('aiProviderMessage',payload?.status==='invalid'?'Connection test returned an invalid configuration.':'Connection test completed; no secrets were returned.');}catch(error){if(error.message!=='unauthorized')renderAiProviderStatus({status:error.message.toLowerCase().includes('invalid')?'invalid':'error',message:error.message});}finally{button.disabled=false;}}
async function loadProviderPolicy(){const panel=$('providerPolicyPanel');if(!panel)return;panel.innerHTML='
Loading provider policy…
';try{const payload=await jsonRequest('/api/v1/outreach/provider-config');renderProviderPolicy(payload);}catch(error){if(error.message!=='unauthorized')renderProviderPolicy({status:error.message.toLowerCase().includes('auth')?'auth-required':'error',message:error.message,providers:[]});}}
function renderDetail(p){const s=scoreFor(p),st=statusOf(p),f=freshness(p),factors=p.score_factors||p.factors||[],blocked=st==='review'||st==='suppressed',stage=p.pipeline_stage||p.pipeline?.stage||(Array.isArray(p.pipeline)?p.pipeline.at(-1)?.stage:'')||'new',contacts=p.contacts||[],domains=p.domains||[],websites=p.websites||[],evidence=p.evidence||p.evidence_timeline||[],notes=p.notes||[],review=p.review_status||p.review|| (st==='review'?'Needs review':st[0].toUpperCase()+st.slice(1));$('detailPanel').innerHTML=`
Showing up to 10 rows · Preview only; nothing added yet.`;}
async function login(event){event.preventDefault();const form=event.currentTarget,messageEl=$('loginMessage'),data=Object.fromEntries(new FormData(form).entries());messageEl.textContent='Signing in…';messageEl.className='form-message';try{const res=await fetch(endpoint('/api/v1/auth/login'),{method:'POST',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify(data)});const body=await res.json().catch(()=>({}));if(!res.ok)throw new Error(body.error||'Invalid email or password.');await bootstrap();}catch(e){if(e.message!=='unauthorized'){messageEl.textContent=e.message;messageEl.className='form-message error';}}}
async function logout(){try{await fetch(endpoint('/api/v1/auth/logout'),{method:'POST',credentials:'include'});}finally{showLogin('You have been signed out.');$('loginForm').reset();}}
- async function bootstrap(){let res;try{res=await fetch(endpoint('/api/v1/auth/me'),{credentials:'include'});}catch(e){showLogin('Unable to connect to the workspace. Try again.');return;}if(res.status===401){showLogin();return;}if(!res.ok){showLogin('Unable to verify the workspace session. Try again.');return;}let user;try{user=await res.json();}catch(e){showLogin('Unable to read the workspace session. Try again.');return;}showDashboard(user.user||user);try{await loadData();await loadJobs();await loadSources();await loadDiscoveryRuns();await loadScorePanels();await loadSavedFilters();await loadReviewQueue();await loadCrmPipeline();await loadReports();await loadSuppressions();await loadProviderPolicy();await loadAiProviderSettings();}catch(e){console.error('Workspace dashboard initialization failed',e);$('apiStatus').textContent='● Workspace partially loaded';$('apiStatus').classList.remove('live');setExplorerState('Some workspace panels could not be loaded. Use Refresh to retry.',true);}}
+ async function bootstrap(){let res;try{res=await fetch(endpoint('/api/v1/auth/me'),{credentials:'include'});}catch(e){showLogin('Unable to connect to the workspace. Try again.');return;}if(res.status===401){showLogin();return;}if(!res.ok){showLogin('Unable to verify the workspace session. Try again.');return;}let user;try{user=await res.json();}catch(e){showLogin('Unable to read the workspace session. Try again.');return;}showDashboard(user.user||user);const panelLoads=[loadData(),loadJobs(),loadSources(),loadDiscoveryRuns(),loadScorePanels(),loadSavedFilters(),loadReviewQueue(),loadCrmPipeline(),loadReports(),loadSuppressions(),loadProviderPolicy(),loadAiProviderSettings()];const results=await Promise.allSettled(panelLoads);const rejected=results.filter(result=>result.status==='rejected');if(rejected.length){console.error('Workspace dashboard panels failed',rejected);$('apiStatus').textContent='● Some panels need refresh';$('apiStatus').classList.remove('live');setExplorerState('Some workspace panels could not be loaded. Use Refresh to retry.',true);}}
document.addEventListener('submit',e=>{if(e.target.id==='contactForm')saveContact(e.target);if(e.target.id==='noteForm')saveNote(e.target);if(e.target.id==='pipelineForm')saveStage(e.target);});
document.addEventListener('click',e=>{if(e.target.id==='verifyBtn')verify();if(e.target.id==='recalculateScoreBtn')recalculateScore();if(e.target.id==='generateAiSuggestionBtn')generateAiSuggestion();if(e.target.id==='createOutreachDraftBtn')createOutreachDraft();const outreachButton=e.target.closest?.('[data-outreach-action]');if(outreachButton)approveOutreachDraft(outreachButton.dataset.outreachId);const aiButton=e.target.closest?.('[data-ai-action]');if(aiButton)aiAction(aiButton.dataset.aiAction,aiButton.dataset.aiId);const scoreEdit=e.target.closest?.('[data-score-edit]');if(scoreEdit)updateScoreRule(scoreEdit.dataset.scoreEdit,scoreEdit.dataset.scorePoints);if(e.target.id==='extractContactsBtn'&&selectedId)loadContactExtraction(selectedId,{extract:true});if(e.target.id==='refreshContactExtractionBtn'&&selectedId)loadContactExtraction(selectedId,{extract:true});if(e.target.id==='retryContactExtractionBtn'&&selectedId)loadContactExtraction(selectedId);if(e.target.id==='scanWebsiteBtn'&&selectedId)loadWebsiteScan(selectedId,{scan:true});if(e.target.id==='refreshWebsiteScanBtn'&&selectedId)loadWebsiteScan(selectedId);if(e.target.id==='retryWebsiteScanBtn'&&selectedId)loadWebsiteScan(selectedId);if(e.target.id==='runDomainCheckBtn')runDomainCheck();if(e.target.id==='retryDomainBtn'&&selectedId)loadDomainIntelligence(selectedId);const availability=e.target.closest?.('[data-domain-availability]');if(availability)checkDomainAvailability(availability.dataset.domain,availability);if(e.target.id==='retryDetailBtn'&&selectedId)loadDetail(selectedId);if(e.target.id==='retryDedupBtn'&&selectedId)loadMatchSuggestions(selectedId);if(e.target.id==='retryHistoryBtn'&&selectedId)loadMergeHistory(selectedId);if(e.target.id==='cancelMergeBtn'||e.target.id==='cancelMergeBtnSecondary')closeMergeDialog();if(e.target.id==='confirmMergeBtn')confirmMerge();const mergeButton=e.target.closest?.('[data-merge-target]');if(mergeButton)openMergeDialog(mergeButton.dataset.mergeTarget,mergeButton.dataset.mergeTargetName);const reverseButton=e.target.closest?.('[data-reverse-merge]');if(reverseButton)reverseMerge(reverseButton.dataset.reverseMerge);if(e.target.id==='retryJobDetailBtn'&&selectedJobId)loadJobDetail(selectedJobId);if(e.target.id==='cancelJobBtn')jobAction('cancel');if(e.target.id==='retryJobBtn')jobAction('retry');const row=e.target.closest?.('[data-job-id]');if(row)loadJobDetail(row.dataset.jobId);});
$('loginForm').addEventListener('submit',login);$('logoutBtn').addEventListener('click',logout);$('directDiscoveryForm').addEventListener('submit',submitDirectDiscovery);$('directDiscoveryRefreshBtn').addEventListener('click',loadDiscoveryRuns);$('directDiscoveryRunsState').addEventListener('click',e=>{const row=e.target.closest?.('[data-discovery-run-id]');if(row){const run=discoveryRuns.find(item=>String(item.id)===String(row.dataset.discoveryRunId));if(run)loadDiscoveryRun(run);}if(e.target.id==='retryDiscoveryRunsBtn')loadDiscoveryRuns();});$('searchInput').addEventListener('input',()=>{page=1;renderRows();});['scoreFilter','statusFilter','websiteClassFilter','pipelineFilter'].forEach(id=>$(id).addEventListener('change',()=>{page=1;loadData();}));$('pageSize').addEventListener('change',e=>{pageSize=Number(e.target.value);page=1;loadData();});$('nextPageBtn').addEventListener('click',()=>{if(hasNextPage){page+=1;loadData();}});$('refreshBtn').addEventListener('click',loadData);$('jobsRefreshBtn').addEventListener('click',()=>loadJobs());$('startDemoJobBtn').addEventListener('click',startDemoJob);$('sourcesRefreshBtn').addEventListener('click',loadSources);$('sourceForm').addEventListener('submit',saveSource);$('sourceType').addEventListener('change',e=>{$('sourceCsvField').hidden=e.target.value!=='csv';});$('discoveryForm').addEventListener('submit',e=>{e.preventDefault();runDiscovery(true);});$('discoveryRunBtn').addEventListener('click',()=>runDiscovery(false));$('sourcesList').addEventListener('click',e=>{const button=e.target.closest?.('[data-source-action]');if(button)sourceAction(button.dataset.sourceId,button.dataset.sourceAction);});$('addForm').addEventListener('submit',addProspect);$('csvInput').addEventListener('change',e=>{const file=e.target.files[0];if(file){const reader=new FileReader();reader.onload=()=>renderCsv(parseCsv(reader.result));reader.readAsText(file);}});$('menuBtn').addEventListener('click',()=>document.querySelector('.sidebar').classList.toggle('open'));document.querySelectorAll('[data-scroll]').forEach(b=>b.addEventListener('click',()=>document.querySelector(b.dataset.scroll)?.scrollIntoView()));
$('savedFilterForm').addEventListener('submit',saveCurrentFilters);$('savedFilterSelect').addEventListener('change',e=>{ $('deleteSavedFilterBtn').disabled=!e.target.value;if(e.target.value)loadSavedFilter(e.target.value);});$('deleteSavedFilterBtn').addEventListener('click',deleteSavedFilter);$('reviewQueueState').addEventListener('change',e=>{const input=e.target.closest?.('[data-review-id]');if(input){if(input.checked)selectedReviewIds.add(String(input.dataset.reviewId));else selectedReviewIds.delete(String(input.dataset.reviewId));updateBulkState();}});$('selectAllReview').addEventListener('change',e=>{reviewQueue.forEach(p=>e.target.checked?selectedReviewIds.add(String(p.id)):selectedReviewIds.delete(String(p.id)));renderReviewQueue({items:reviewQueue,count:$('reviewQueueCount').textContent});});$('bulkVerifyBtn').addEventListener('click',()=>bulkReview('verify'));$('bulkRejectBtn').addEventListener('click',()=>bulkReview('reject'));$('reviewQueueState').addEventListener('click',e=>{if(e.target.id==='retryReviewQueueBtn')loadReviewQueue();});document.querySelectorAll('[data-dashboard-filter]').forEach(card=>card.addEventListener('click',()=>{const kind=card.dataset.dashboardFilter;if(kind==='review'||kind==='suppressed')applyFilters({status:kind});else if(kind==='high')applyFilters({score:'high'});else if(kind==='fresh')applyFilters({});else applyFilters({status:'all',score:'all'});}));
- $('interactionForm').addEventListener('submit',saveInteraction);$('suppressionForm').addEventListener('submit',addSuppression);$('aiProviderForm').addEventListener('submit',saveAiProviderSettings);$('aiProviderRefreshBtn').addEventListener('click',loadAiProviderSettings);$('testAiProviderBtn').addEventListener('click',testAiProviderConnection);$('crmRefreshBtn').addEventListener('click',()=>{loadCrmPipeline();loadInteractions(selectedId);});$('reportsRefreshBtn').addEventListener('click',loadReports);$('suppressionRefreshBtn').addEventListener('click',loadSuppressions);$('pipelineViewToggle').addEventListener('click',()=>{crmListMode=!crmListMode;$('pipelineViewToggle').textContent=crmListMode?'▦ Board view':'☷ List view';$('pipelineViewToggle').setAttribute('aria-pressed',String(crmListMode));renderPipeline();});$('pipelineBoard').addEventListener('click',e=>{const selectButton=e.target.closest?.('[data-crm-select]');if(selectButton){selectedId=Number(selectButton.dataset.crmSelect);selectProspect(selectedId);loadInteractions(selectedId);$('crmActivity').scrollIntoView({behavior:'smooth',block:'start'});}const stageButton=e.target.closest?.('[data-crm-save-stage]');if(stageButton)saveCrmStage(stageButton.dataset.crmSaveStage);if(e.target.id==='retryCrmBtn')loadCrmPipeline();if(e.target.id==='retryInteractionsBtn'&&selectedId)loadInteractions(selectedId);});$('suppressionState').addEventListener('change',e=>{const input=e.target.closest?.('[data-suppression-id]');if(input){if(input.checked)selectedSuppressionIds.add(String(input.dataset.suppressionId));else selectedSuppressionIds.delete(String(input.dataset.suppressionId));updateSuppressionSelection();}});$('suppressionState').addEventListener('click',e=>{const button=e.target.closest?.('[data-remove-suppression]');if(button)removeSuppression(button.dataset.removeSuppression);if(e.target.id==='retrySuppressionsBtn')loadSuppressions();});$('selectAllSuppressions').addEventListener('change',e=>{suppressions.forEach(item=>e.target.checked?selectedSuppressionIds.add(String(item.id)):selectedSuppressionIds.delete(String(item.id)));renderSuppressions();});$('bulkReviewSuppressionsBtn').addEventListener('click',bulkReviewSuppressions);$('outreachPolicyRefreshBtn').addEventListener('click',loadProviderPolicy);
+ $('interactionForm').addEventListener('submit',saveInteraction);$('suppressionForm').addEventListener('submit',addSuppression);$('aiProviderForm').addEventListener('submit',saveAiProviderSettings);$('aiProvider').addEventListener('change',updateAiProviderForm);$('aiProviderRefreshBtn').addEventListener('click',loadAiProviderSettings);$('testAiProviderBtn').addEventListener('click',testAiProviderConnection);$('crmRefreshBtn').addEventListener('click',()=>{loadCrmPipeline();loadInteractions(selectedId);});$('reportsRefreshBtn').addEventListener('click',loadReports);$('suppressionRefreshBtn').addEventListener('click',loadSuppressions);$('pipelineViewToggle').addEventListener('click',()=>{crmListMode=!crmListMode;$('pipelineViewToggle').textContent=crmListMode?'▦ Board view':'☷ List view';$('pipelineViewToggle').setAttribute('aria-pressed',String(crmListMode));renderPipeline();});$('pipelineBoard').addEventListener('click',e=>{const selectButton=e.target.closest?.('[data-crm-select]');if(selectButton){selectedId=Number(selectButton.dataset.crmSelect);selectProspect(selectedId);loadInteractions(selectedId);$('crmActivity').scrollIntoView({behavior:'smooth',block:'start'});}const stageButton=e.target.closest?.('[data-crm-save-stage]');if(stageButton)saveCrmStage(stageButton.dataset.crmSaveStage);if(e.target.id==='retryCrmBtn')loadCrmPipeline();if(e.target.id==='retryInteractionsBtn'&&selectedId)loadInteractions(selectedId);});$('suppressionState').addEventListener('change',e=>{const input=e.target.closest?.('[data-suppression-id]');if(input){if(input.checked)selectedSuppressionIds.add(String(input.dataset.suppressionId));else selectedSuppressionIds.delete(String(input.dataset.suppressionId));updateSuppressionSelection();}});$('suppressionState').addEventListener('click',e=>{const button=e.target.closest?.('[data-remove-suppression]');if(button)removeSuppression(button.dataset.removeSuppression);if(e.target.id==='retrySuppressionsBtn')loadSuppressions();});$('selectAllSuppressions').addEventListener('change',e=>{suppressions.forEach(item=>e.target.checked?selectedSuppressionIds.add(String(item.id)):selectedSuppressionIds.delete(String(item.id)));renderSuppressions();});$('bulkReviewSuppressionsBtn').addEventListener('click',bulkReviewSuppressions);$('outreachPolicyRefreshBtn').addEventListener('click',loadProviderPolicy);
bootstrap();
})();
diff --git a/apps/web/asset-manifest.json b/apps/web/asset-manifest.json
index 29c7c36..f256e74 100644
--- a/apps/web/asset-manifest.json
+++ b/apps/web/asset-manifest.json
@@ -14,9 +14,9 @@
],
"integrity": {
"config.js": "sha256-f9c7b4db3eab4cf54146bd25891b5103b09ae75da93c57b548cf57ae93e4a3f6",
- "app.js": "sha256-0bbd76d564ba77d954dd1c8540674352b8d595ee5106098758985bdfcb2217f3",
+ "app.js": "sha256-b8e1708b5a35123b645d85cddc836cae96424e18ab1f282e64f02a500ded3f0f",
"styles.css": "sha256-ca618b1a44a1f310029ab9d6986a4120e9147cd61784c3e7a0ff826cd2d42696",
- "index.html": "sha256-afb767ff483c9e4b8a68c81a350f8faa0dac0eca9c813b55b334365dba0f8de3",
+ "index.html": "sha256-7a17c1568c471db9ed2cccae1529004e26c4a2abc58f4a83a592a702c5613726",
"health.html": "sha256-c352a6f37aa24628cfc8d5709a70ff2d94181d192ed5fe00916cca9378a61d81",
"error.html": "sha256-f3cc28d2dfc9e8af112d0257c6ee47b9e9146e5da8590a754dd589bf43702aaf",
"healthz": "sha256-dc51b8c96c2d745df3bd5590d990230a482fd247123599548e0632fdbf97fc22"
diff --git a/apps/web/index.html b/apps/web/index.html
index 06c9257..e926ca0 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -121,7 +121,7 @@
ADD RULE
Add suppression
Explicit confirmation required
REGISTER
Current suppressions
Sign in to load suppressions.
GOVERNANCE
Outreach provider policy
View approved provider status without exposing credentials or secrets.
Sending is disabled by default. This panel is status-only. Provider configuration never creates a send trigger, and no credentials are displayed.
Sign in to load provider policy.
-
SYSTEM CONFIGURATION
AI Provider Settings
Configure the approved Nous Portal service and the internal SearXNG service used by workspace AI research.
Admin-only and write-only credentials. Keys are sent only over the authenticated API, are never displayed or stored in this browser, and updating settings does not start discovery or AI work.
PROVIDER CONFIGURATION
Service connection
Approved providers only
+
SYSTEM CONFIGURATION
AI Provider Settings
Configure the approved Nous Portal service and the internal SearXNG service used by workspace AI research.
Admin-only and write-only credentials. Keys are sent only over the authenticated API, are never displayed or stored in this browser, and updating settings does not start discovery or AI work.