diff --git a/apps/api/app/provider_config.py b/apps/api/app/provider_config.py index 128a31d..b530f06 100644 --- a/apps/api/app/provider_config.py +++ b/apps/api/app/provider_config.py @@ -22,6 +22,7 @@ MAX_MODEL = 160 ALLOWED_PROVIDERS = {"nous_portal", "nous_portal_web_research"} DEFAULT_NOUS_URL = "https://inference-api.nousresearch.com/v1" DEFAULT_FIRECRAWL_URL = "https://api.firecrawl.dev/v2" +DEFAULT_SEARXNG_URL = "http://searxng:8080" def key_path() -> Path: @@ -82,7 +83,7 @@ def validate_payload(payload: dict) -> dict: enabled = payload.get("enabled", True) if not isinstance(enabled, bool): raise ValueError("invalid_enabled") - urls = {"nous_base_url": DEFAULT_NOUS_URL, "firecrawl_base_url": DEFAULT_FIRECRAWL_URL} + urls = {"nous_base_url": DEFAULT_NOUS_URL, "firecrawl_base_url": str(payload.get("searxng_base_url", DEFAULT_SEARXNG_URL)).strip() or DEFAULT_SEARXNG_URL} for field, default in urls.items(): value = str(payload.get(field, default)).strip().rstrip("/") parsed = urlparse(value) diff --git a/apps/web/app.js b/apps/web/app.js index 1c37487..b175a72 100644 --- a/apps/web/app.js +++ b/apps/web/app.js @@ -172,9 +172,9 @@ state.innerHTML=`
${rows.map(service=>{const status=providerStatusValue(service.status||service.state||(service.configured?'configured':'not-configured')),configured=['configured','ready','enabled','connected'].includes(status);return `
${esc(service.name||service.provider||'Approved provider')}${esc(service.model||service.base_url||'Configuration details unavailable')}
${esc(providerStatusLabel(status))}

${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 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],['firecrawlBaseUrl',source.firecrawl_base_url||payload?.firecrawl?.base_url]].forEach(([id,value])=>{if(value!==undefined&&$(id))$(id).value=value;}); ['nousApiKey','firecrawlApiKey'].forEach(id=>{if($(id))$(id).value='';}); } + 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='';}); } 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(),firecrawl_base_url:data.firecrawl_base_url.trim(),enabled:true,credentials:{}};if(data.nous_api_key)body.credentials.nous_api_key=data.nous_api_key;if(data.firecrawl_api_key)body.credentials.firecrawl_api_key=data.firecrawl_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.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=`

PROSPECT DETAIL

${esc(p.name)}

${esc(p.website_domain||'no detected website')}

${esc(review)}
Fit score${s}/ 100
${esc(p.confidence||(s>=80?'High':s>=60?'Medium':'Low'))} confidence
Loading score breakdown…

Pipeline stage

Contacts ${contacts.length}

${listItems(contacts,'No contacts added.','email')}

Loading public contacts…
Loading website scan…
Loading domain intelligence…

AI ASSISTANCE

Evidence-grounded suggestion

AI suggestions summarize workspace evidence and citations only. Review and approve explicitly; No autonomous action or outreach is taken.

Loading AI assistance…

OUTREACH PREPARATION

Evidence-grounded outreach draft

Draft-only preparation. Review the recipient and citations before approval. No message will be sent from this screen.

Loading outreach drafts…

Sending is disabled by policy; approval never sends a message.

Domains & websites

${listItems(domains,'No domains recorded.','domain')}${listItems(websites,'No websites recorded.','url')}

Evidence timeline

${listItems(evidence,'No evidence events recorded.','description')}${factors.length?factors.map(x=>`

✓ ${esc(labelFactor(x))}${esc(p.confidence||'Medium')}

`).join(''):''}

Notes ${notes.length}

${listItems(notes,'No notes added.','body')}

Review status

${esc(review)}

${st!=='suppressed'?'':''}

${blocked?`

${st==='suppressed'?'Suppressed records cannot be contacted.':'Review this prospect before outreach is available.'}

`:''}`;renderScoreBreakdown(p);renderContactExtractionPanel(p);loadContactExtraction(p.id);renderWebsiteScanPanel(p);renderDomainPanel(p);loadAiAssistance(p.id);loadOutreachDrafts(p.id);renderDedupPanel();} diff --git a/apps/web/asset-manifest.json b/apps/web/asset-manifest.json index afd15ff..29c7c36 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-34521da899dacc84d58200258735f09515d4b527028e1ba8c53ce6b66ad77c8a", + "app.js": "sha256-0bbd76d564ba77d954dd1c8540674352b8d595ee5106098758985bdfcb2217f3", "styles.css": "sha256-ca618b1a44a1f310029ab9d6986a4120e9147cd61784c3e7a0ff826cd2d42696", - "index.html": "sha256-d2f21cd3babbd276c90742e3aba4c815ce7833d151ec4660ae5cab6078011eee", + "index.html": "sha256-afb767ff483c9e4b8a68c81a350f8faa0dac0eca9c813b55b334365dba0f8de3", "health.html": "sha256-c352a6f37aa24628cfc8d5709a70ff2d94181d192ed5fe00916cca9378a61d81", "error.html": "sha256-f3cc28d2dfc9e8af112d0257c6ee47b9e9146e5da8590a754dd589bf43702aaf", "healthz": "sha256-dc51b8c96c2d745df3bd5590d990230a482fd247123599548e0632fdbf97fc22" diff --git a/apps/web/index.html b/apps/web/index.html index 0538891..06c9257 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.

PROVIDER CONFIGURATION

Service connection

Approved providers only