This commit is contained in:
@@ -94,6 +94,8 @@ class SourceApiTests(unittest.TestCase):
|
|||||||
payload={'name':'OpenStreetMap / Overpass · plumbers','kind':'openstreetmap','config':{'provider':'openstreetmap','query':'plumbers','location':'Cape Town','approved':True,'public_access':True,'terms_accepted':True,'rate_limit':1}}
|
payload={'name':'OpenStreetMap / Overpass · plumbers','kind':'openstreetmap','config':{'provider':'openstreetmap','query':'plumbers','location':'Cape Town','approved':True,'public_access':True,'terms_accepted':True,'rate_limit':1}}
|
||||||
status, created=self.req('POST','/api/v1/sources',payload); self.assertEqual(status,201); self.assertTrue(created['created'])
|
status, created=self.req('POST','/api/v1/sources',payload); self.assertEqual(status,201); self.assertTrue(created['created'])
|
||||||
status, reused=self.req('POST','/api/v1/sources',payload); self.assertEqual(status,200); self.assertFalse(reused['created']); self.assertEqual(reused['id'],created['id'])
|
status, reused=self.req('POST','/api/v1/sources',payload); self.assertEqual(status,200); self.assertFalse(reused['created']); self.assertEqual(reused['id'],created['id'])
|
||||||
|
second={**payload,'name':'OpenStreetMap / Overpass · plumbers · Durban','config':{**payload['config'],'location':'Durban'}}
|
||||||
|
status, other=self.req('POST','/api/v1/sources',second); self.assertEqual(status,201); self.assertTrue(other['created']); self.assertNotEqual(other['id'],created['id'])
|
||||||
|
|
||||||
def test_source_configuration_can_be_saved_before_enablement(self):
|
def test_source_configuration_can_be_saved_before_enablement(self):
|
||||||
status, source = self.req('POST', '/api/v1/sources', {'name': 'DNS', 'kind': 'dns', 'config': {}})
|
status, source = self.req('POST', '/api/v1/sources', {'name': 'DNS', 'kind': 'dns', 'config': {}})
|
||||||
|
|||||||
+2
-2
@@ -296,7 +296,7 @@
|
|||||||
if (!value || !value.trim()) return;
|
if (!value || !value.trim()) return;
|
||||||
const text = value.trim();
|
const text = value.trim();
|
||||||
const config = type === 'public_website' ? {urls:[text]} : type === 'dns' ? {domains:[text]} : type === 'rdap' || type === 'ct_logs' ? {domain:text} : {provider:type,query:text,location:(window.prompt('Enter the location (optional):','South Africa') || 'South Africa').trim(),approved:true,public_access:true,terms_accepted:true,rate_limit:1};
|
const config = type === 'public_website' ? {urls:[text]} : type === 'dns' ? {domains:[text]} : type === 'rdap' || type === 'ct_logs' ? {domain:text} : {provider:type,query:text,location:(window.prompt('Enter the location (optional):','South Africa') || 'South Africa').trim(),approved:true,public_access:true,terms_accepted:true,rate_limit:1};
|
||||||
try { const registered=await jsonRequest('/api/v1/sources', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:`${sourceLabel(preview)} · ${text}`,kind:type,config})}); await loadSources(); sourceMessage(registered.created === false ? 'Existing source reloaded. Test it, then enable it once the health check succeeds.' : 'Source registered. Test it, then enable it once the health check succeeds.'); }
|
try { const registered=await jsonRequest('/api/v1/sources', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name:`${sourceLabel(preview)} · ${text}${['openstreetmap','wikidata','common_crawl'].includes(type) ? ` · ${config.location}` : ''}`,kind:type,config})}); await loadSources(); sourceMessage(registered.created === false ? 'Existing source reloaded. Test it, then enable it once the health check succeeds.' : 'Source registered. Test it, then enable it once the health check succeeds.'); }
|
||||||
catch (error) { if (error.message !== 'unauthorized') sourceMessage(error.message || 'Unable to register source.', true); }
|
catch (error) { if (error.message !== 'unauthorized') sourceMessage(error.message || 'Unable to register source.', true); }
|
||||||
}
|
}
|
||||||
async function sourceAction(id, action) { if (action === 'setup') return registerAdapterPreview(id); const source = findRegisteredSource(id); if (!source || !source.id) { sourceMessage('This source is an adapter preview, not a registered source.', true); return; } if (action === 'review') { selectedSourceId=source.id; document.querySelector(`[data-source-id="${CSS.escape(String(id))}"]`)?.scrollIntoView({behavior:'smooth',block:'center'}); sourceMessage('Source details are shown below. Review terms, owner, limits, health, and circuit state before enabling.'); return; } try { let notice=''; if (action === 'test') { await jsonRequest(`/api/v1/sources/${encodeURIComponent(source.id)}/test`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({})}); notice='Source test completed.'; } else { const enabled = sourceState(source); await jsonRequest(`/api/v1/sources/${encodeURIComponent(source.id)}`, {method:'PATCH', headers:{'Content-Type':'application/json'}, body:JSON.stringify({enabled:!enabled})}); notice=`Source ${enabled ? 'disabled' : 'enabled'} by the workspace.`; } await loadSources(); sourceMessage(notice); } catch (error) { if (error.message !== 'unauthorized') sourceMessage(error.message || `Unable to ${action} source.`, true); } }
|
async function sourceAction(id, action) { if (action === 'setup') return registerAdapterPreview(id); const source = findRegisteredSource(id); if (!source || !source.id) { sourceMessage('This source is an adapter preview, not a registered source.', true); return; } if (action === 'review') { selectedSourceId=source.id; document.querySelector(`[data-source-id="${CSS.escape(String(id))}"]`)?.scrollIntoView({behavior:'smooth',block:'center'}); sourceMessage('Source details are shown below. Review terms, owner, limits, health, and circuit state before enabling.'); return; } try { let notice=''; if (action === 'test') { await jsonRequest(`/api/v1/sources/${encodeURIComponent(source.id)}/test`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({})}); notice='Source test completed.'; } else { const enabled = sourceState(source); await jsonRequest(`/api/v1/sources/${encodeURIComponent(source.id)}`, {method:'PATCH', headers:{'Content-Type':'application/json'}, body:JSON.stringify({enabled:!enabled})}); notice=`Source ${enabled ? 'disabled' : 'enabled'} by the workspace.`; } await loadSources(); sourceMessage(notice); } catch (error) { if (error.message !== 'unauthorized') sourceMessage(error.message || `Unable to ${action} source.`, true); } }
|
||||||
@@ -354,7 +354,7 @@ $('aiProviderForm').addEventListener('submit',saveAiProviderSettings);$('aiProvi
|
|||||||
if (badge) badge.textContent = 'Public and operator-controlled sources';
|
if (badge) badge.textContent = 'Public and operator-controlled sources';
|
||||||
if (copy) copy.textContent = 'Register bounded public sources or operator-controlled imports. New sources start disabled and must be tested before use.';
|
if (copy) copy.textContent = 'Register bounded public sources or operator-controlled imports. New sources start disabled and must be tested before use.';
|
||||||
sourceSetup.insertAdjacentHTML('afterend', `<article class="panel source-config-panel free-sources-panel"><div class="panel-heading"><div><p class="eyebrow">NO BILLING REQUIRED</p><h3>Free public source</h3></div><span class="small-label">Rate limited</span></div><p class="muted">Search OpenStreetMap, Wikidata, or Common Crawl without a Google API key.</p><form id="freeSourceForm" class="crm-form"><label>Provider<select name="provider"><option value="openstreetmap">OpenStreetMap / Overpass</option><option value="wikidata">Wikidata</option><option value="common_crawl">Common Crawl</option></select></label><label>Search query<input name="query" required placeholder="e.g. plumbers"></label><label>Location <span class="optional">optional</span><input name="location" value="South Africa" placeholder="e.g. Eastern Cape"></label><p id="freeSourceMessage" class="form-message" role="status"></p><button class="button primary" type="submit">Add free source</button></form></article>`);
|
sourceSetup.insertAdjacentHTML('afterend', `<article class="panel source-config-panel free-sources-panel"><div class="panel-heading"><div><p class="eyebrow">NO BILLING REQUIRED</p><h3>Free public source</h3></div><span class="small-label">Rate limited</span></div><p class="muted">Search OpenStreetMap, Wikidata, or Common Crawl without a Google API key.</p><form id="freeSourceForm" class="crm-form"><label>Provider<select name="provider"><option value="openstreetmap">OpenStreetMap / Overpass</option><option value="wikidata">Wikidata</option><option value="common_crawl">Common Crawl</option></select></label><label>Search query<input name="query" required placeholder="e.g. plumbers"></label><label>Location <span class="optional">optional</span><input name="location" value="South Africa" placeholder="e.g. Eastern Cape"></label><p id="freeSourceMessage" class="form-message" role="status"></p><button class="button primary" type="submit">Add free source</button></form></article>`);
|
||||||
$('freeSourceForm')?.addEventListener('submit', async event => { event.preventDefault(); const fields=Object.fromEntries(new FormData(event.currentTarget).entries()), msg=$('freeSourceMessage'); msg.textContent='Registering source…'; msg.className='form-message'; try { await jsonRequest('/api/v1/sources',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:`${fields.provider} · ${fields.query}`,kind:'approved_directory',config:{provider:fields.provider,query:fields.query.trim(),location:(fields.location||'South Africa').trim(),approved:true,public_access:true,terms_accepted:true,rate_limit:1},policy:{owner:'workspace operator',rate_limit:1,terms_url:'https://www.openstreetmap.org/copyright'}})}); msg.textContent='Source added disabled. Test it, then enable it below.'; event.currentTarget.reset(); event.currentTarget.querySelector('[name="location"]').value='South Africa'; await loadSources(); } catch(error) { msg.textContent=error.message||'Unable to add source.'; msg.className='form-message error'; } });
|
$('freeSourceForm')?.addEventListener('submit', async event => { event.preventDefault(); const fields=Object.fromEntries(new FormData(event.currentTarget).entries()), msg=$('freeSourceMessage'); msg.textContent='Registering source…'; msg.className='form-message'; try { await jsonRequest('/api/v1/sources',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:`${fields.provider} · ${fields.query.trim()} · ${(fields.location||'South Africa').trim()}`,kind:'approved_directory',config:{provider:fields.provider,query:fields.query.trim(),location:(fields.location||'South Africa').trim(),approved:true,public_access:true,terms_accepted:true,rate_limit:1},policy:{owner:'workspace operator',rate_limit:1,terms_url:'https://www.openstreetmap.org/copyright'}})}); msg.textContent='Source added disabled. Test it, then enable it below.'; event.currentTarget.reset(); event.currentTarget.querySelector('[name="location"]').value='South Africa'; await loadSources(); } catch(error) { msg.textContent=error.message||'Unable to add source.'; msg.className='form-message error'; } });
|
||||||
sourceSetup.insertAdjacentHTML('afterend', `<article class="panel source-config-panel google-places-panel"><div class="panel-heading"><div><p class="eyebrow">OPTIONAL PROVIDER</p><h3>Google Places discovery</h3></div><span class="small-label">Secure key storage</span></div><p class="muted">Add a Google Places API key here. The key is sent directly to the server, encrypted, and never displayed again.</p><form id="googlePlacesForm" class="crm-form"><label>Search query<input name="query" required placeholder="e.g. plumbers in Cape Town"></label><label>Region code<input name="region_code" value="ZA" maxlength="2"></label><label>Google API key<input name="api_key" type="password" autocomplete="new-password" required placeholder="Paste your key"></label><label class="checkbox-line"><input name="approved" type="checkbox" required> I have enabled Places API, billing, and accepted Google's terms.</label><p id="googlePlacesMessage" class="form-message" role="status"></p><button class="button primary" type="submit">Save key and register source</button></form></article>`);
|
sourceSetup.insertAdjacentHTML('afterend', `<article class="panel source-config-panel google-places-panel"><div class="panel-heading"><div><p class="eyebrow">OPTIONAL PROVIDER</p><h3>Google Places discovery</h3></div><span class="small-label">Secure key storage</span></div><p class="muted">Add a Google Places API key here. The key is sent directly to the server, encrypted, and never displayed again.</p><form id="googlePlacesForm" class="crm-form"><label>Search query<input name="query" required placeholder="e.g. plumbers in Cape Town"></label><label>Region code<input name="region_code" value="ZA" maxlength="2"></label><label>Google API key<input name="api_key" type="password" autocomplete="new-password" required placeholder="Paste your key"></label><label class="checkbox-line"><input name="approved" type="checkbox" required> I have enabled Places API, billing, and accepted Google's terms.</label><p id="googlePlacesMessage" class="form-message" role="status"></p><button class="button primary" type="submit">Save key and register source</button></form></article>`);
|
||||||
$('googlePlacesForm')?.addEventListener('submit', async event => { event.preventDefault(); const form=event.currentTarget, fields=Object.fromEntries(new FormData(form).entries()); const msg=$('googlePlacesMessage'); msg.textContent='Saving securely…'; msg.className='form-message'; try { const created=await jsonRequest('/api/v1/sources',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:'Google Places',kind:'google_places',config:{approved:true,public_access:true,terms_accepted:true,credential_ref:'google_places_api_key',rate_limit:1,query:fields.query.trim(),region_code:(fields.region_code||'ZA').toUpperCase()}})}); await jsonRequest(`/api/v1/sources/${encodeURIComponent(created.id)}/credentials`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({provider:'google_places',key_name:'api_key',api_key:fields.api_key})}); msg.textContent='Google Places is registered and the key is encrypted. Test it, then enable the source.'; form.reset(); form.querySelector('[name="region_code"]').value='ZA'; await loadSources(); } catch(error) { msg.textContent=error.message||'Unable to save Google Places settings.'; msg.className='form-message error'; } });
|
$('googlePlacesForm')?.addEventListener('submit', async event => { event.preventDefault(); const form=event.currentTarget, fields=Object.fromEntries(new FormData(form).entries()); const msg=$('googlePlacesMessage'); msg.textContent='Saving securely…'; msg.className='form-message'; try { const created=await jsonRequest('/api/v1/sources',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:'Google Places',kind:'google_places',config:{approved:true,public_access:true,terms_accepted:true,credential_ref:'google_places_api_key',rate_limit:1,query:fields.query.trim(),region_code:(fields.region_code||'ZA').toUpperCase()}})}); await jsonRequest(`/api/v1/sources/${encodeURIComponent(created.id)}/credentials`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({provider:'google_places',key_name:'api_key',api_key:fields.api_key})}); msg.textContent='Google Places is registered and the key is encrypted. Test it, then enable the source.'; form.reset(); form.querySelector('[name="region_code"]').value='ZA'; await loadSources(); } catch(error) { msg.textContent=error.message||'Unable to save Google Places settings.'; msg.className='form-message error'; } });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"version": "phase-33",
|
"version": "phase-34",
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"config.js",
|
"config.js",
|
||||||
"app.js",
|
"app.js",
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
"healthz"
|
"healthz"
|
||||||
],
|
],
|
||||||
"integrity": {
|
"integrity": {
|
||||||
"config.js": "sha256-8ba805b718f74e47a97f859fab817127ccc49f4b3a827c1ca324dc8370b8bf9e",
|
"config.js": "sha256-7792697d8640937cb0573d314897e8be96230b39418fc1b8a40b66c8138005df",
|
||||||
"app.js": "sha256-ee539f85c77a082f9bc97fa853ebc6ed0c45f0863d68ff6abf3c0e135e9cb152",
|
"app.js": "sha256-fe50db5a4d978922cd761e03c80502b775611e14e2e1118f29147a2de689dddc",
|
||||||
"styles.css": "sha256-ba90290ab11e82a6b2639dfd70d1e74502c1cacb2b26cf1db92b45beb67ac03f",
|
"styles.css": "sha256-ba90290ab11e82a6b2639dfd70d1e74502c1cacb2b26cf1db92b45beb67ac03f",
|
||||||
"index.html": "sha256-ed6d261126818460a058ccc0c561b843d92c17e83faccc449648dd2ddc843159",
|
"index.html": "sha256-3bd7fda6c92bdb3b65e61ac4457c855c99e5bbd82fcfc26959ce70d93b064aa9",
|
||||||
"health.html": "sha256-c352a6f37aa24628cfc8d5709a70ff2d94181d192ed5fe00916cca9378a61d81",
|
"health.html": "sha256-c352a6f37aa24628cfc8d5709a70ff2d94181d192ed5fe00916cca9378a61d81",
|
||||||
"error.html": "sha256-f3cc28d2dfc9e8af112d0257c6ee47b9e9146e5da8590a754dd589bf43702aaf",
|
"error.html": "sha256-f3cc28d2dfc9e8af112d0257c6ee47b9e9146e5da8590a754dd589bf43702aaf",
|
||||||
"healthz": "sha256-dc51b8c96c2d745df3bd5590d990230a482fd247123599548e0632fdbf97fc22"
|
"healthz": "sha256-dc51b8c96c2d745df3bd5590d990230a482fd247123599548e0632fdbf97fc22"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/* Public, non-secret runtime configuration. Replace this file at deploy time if needed. */
|
/* Public, non-secret runtime configuration. Replace this file at deploy time if needed. */
|
||||||
window.__PROSPECT_CONFIG__ = Object.freeze({
|
window.__PROSPECT_CONFIG__ = Object.freeze({
|
||||||
apiBase: '',
|
apiBase: '',
|
||||||
assetVersion: 'phase-33'
|
assetVersion: 'phase-34'
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-3
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>ProspectOS · Pipeline intelligence</title>
|
<title>ProspectOS · Pipeline intelligence</title>
|
||||||
<meta name="description" content="Prospect discovery and review dashboard">
|
<meta name="description" content="Prospect discovery and review dashboard">
|
||||||
<link rel="stylesheet" href="styles.css?v=phase-33">
|
<link rel="stylesheet" href="styles.css?v=phase-34">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<section class="login-screen" id="loginScreen" aria-labelledby="loginTitle">
|
<section class="login-screen" id="loginScreen" aria-labelledby="loginTitle">
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
<div class="merge-dialog" id="mergeDialog" hidden role="dialog" aria-modal="true" aria-labelledby="mergeDialogTitle">
|
<div class="merge-dialog" id="mergeDialog" hidden role="dialog" aria-modal="true" aria-labelledby="mergeDialogTitle">
|
||||||
<div class="merge-dialog-card"><div class="panel-heading"><div><p class="eyebrow">REVIEW REQUIRED</p><h2 id="mergeDialogTitle">Confirm merge</h2></div><button class="icon-button" id="cancelMergeBtn" type="button" aria-label="Close merge confirmation">×</button></div><p id="mergeDialogCopy"></p><div class="merge-warning"><strong>This action is reversible.</strong> The merge will be recorded in history and can be reversed later.</div><p id="mergeDialogMessage" class="form-message" role="alert" aria-live="polite"></p><div class="merge-dialog-actions"><button class="button ghost" id="cancelMergeBtnSecondary" type="button">Cancel</button><button class="button primary" id="confirmMergeBtn" type="button">Confirm merge</button></div></div>
|
<div class="merge-dialog-card"><div class="panel-heading"><div><p class="eyebrow">REVIEW REQUIRED</p><h2 id="mergeDialogTitle">Confirm merge</h2></div><button class="icon-button" id="cancelMergeBtn" type="button" aria-label="Close merge confirmation">×</button></div><p id="mergeDialogCopy"></p><div class="merge-warning"><strong>This action is reversible.</strong> The merge will be recorded in history and can be reversed later.</div><p id="mergeDialogMessage" class="form-message" role="alert" aria-live="polite"></p><div class="merge-dialog-actions"><button class="button ghost" id="cancelMergeBtnSecondary" type="button">Cancel</button><button class="button primary" id="confirmMergeBtn" type="button">Confirm merge</button></div></div>
|
||||||
</div>
|
</div>
|
||||||
<script src="config.js?v=phase-33"></script>
|
<script src="config.js?v=phase-34"></script>
|
||||||
<script src="app.js?v=phase-33"></script>
|
<script src="app.js?v=phase-34"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user