diff --git a/apps/api/app/main.py b/apps/api/app/main.py index a06dfa9..b6c53df 100644 --- a/apps/api/app/main.py +++ b/apps/api/app/main.py @@ -5,6 +5,7 @@ from http.cookies import SimpleCookie from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import parse_qs, urlparse +from urllib.request import Request, urlopen if __package__ in (None, ""): sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from app.domain import deduplication_key, deduplicate_businesses, is_suppressed, normalize_business, normalize_domain, normalize_phone, match_businesses @@ -109,13 +110,44 @@ def row_json(row): def source_config_with_credentials(db, source, config): """Inject a credential only into the in-process adapter call; never persist/return it.""" - if source["kind"] != "google_places": return config + if (source["source_code"] or source["kind"]) != "google_places": return config row = db.execute("SELECT secret_ref FROM source_credentials WHERE source_id=? AND organization_id=? AND provider=? AND key_name=?", (source["id"], source["organization_id"], "google_places", "api_key")).fetchone() if not row or not row["secret_ref"]: return config try: config["_api_key"] = decrypt_provider_secret(row["secret_ref"]) except Exception: pass return config +def resolve_selected_source_codes(db, org, selected): + """Accept dashboard source IDs or adapter codes and normalize to codes.""" + if not isinstance(selected, list): return [] + codes=[] + for value in selected: + row = db.execute("SELECT source_code,kind FROM sources WHERE id=? AND organization_id=?", (int(value), org)).fetchone() if str(value).isdigit() else None + code = (row["source_code"] or row["kind"]) if row else str(value).strip().lower() + if code and code not in codes: codes.append(code) + return codes + +def enrich_source_business(db, org, bid, website_url, actor): + """Bounded post-discovery enrichment for source records with a public website.""" + if not website_url: return + try: scan = scan_website(website_url, max_pages=1) + except Exception: return + now=datetime.now(timezone.utc).replace(microsecond=0); cache_key=hashlib.sha256(str(website_url).encode()).hexdigest() + website=db.execute("SELECT id FROM websites WHERE organization_id=? AND business_id=? AND url=?",(org,bid,website_url)).fetchone() + existing_scan=db.execute("SELECT id FROM website_scans WHERE organization_id=? AND business_id=? AND cache_key=? ORDER BY id DESC LIMIT 1",(org,bid,cache_key)).fetchone() + scan_id=existing_scan["id"] if existing_scan else db.execute("INSERT INTO website_scans(organization_id,business_id,website_id,input_url,classification,result_json,cache_key,scanned_at,cache_expires_at) VALUES(?,?,?,?,?,?,?,?,?)",(org,bid,website["id"] if website else None,website_url,scan.get("classification","unknown"),json.dumps(scan,sort_keys=True),cache_key,now.isoformat(),(now+timedelta(seconds=WEBSITE_SCAN_CACHE_SECONDS)).isoformat())).lastrowid + html=scan.get("html","") + suppressions=[dict(r) for r in db.execute("SELECT kind,value FROM suppressions WHERE organization_id=? AND active=1",(org,))] + for item in extract_contacts(html,website_url,suppressions=suppressions,max_results=25): + key=hashlib.sha256((str(bid)+item["source_url"]+item["kind"]+item["value"]).encode()).hexdigest() + db.execute("INSERT OR IGNORE INTO contact_extractions(organization_id,business_id,website_scan_id,extraction_key,kind,value,label,classification,confidence,source_url,public_business,mx_status,suppressed,do_not_contact,provenance) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",(org,bid,scan_id,key,item["kind"],item["value"],item["label"],item["classification"],item["confidence"],item["source_url"],1,item["mx_status"],int(item["suppressed"]),int(item["do_not_contact"]),item["provenance"])) + if item["kind"]=="email": db.execute("UPDATE businesses SET email=CASE WHEN email='' THEN ? ELSE email END WHERE id=? AND organization_id=?",(item["value"],bid,org)) + if item["kind"]=="phone": db.execute("UPDATE businesses SET phone=CASE WHEN phone='' THEN ? ELSE phone END WHERE id=? AND organization_id=?",(item["value"],bid,org)) + db.execute("UPDATE businesses SET website_class=?,updated_at=CURRENT_TIMESTAMP WHERE id=? AND organization_id=?",(scan.get("classification","unknown"),bid,org)) + current=db.execute("SELECT * FROM businesses WHERE id=? AND organization_id=?",(bid,org)).fetchone() + if current: + scored=score_business_opportunity(row_json(current),website=scan,sources=[actor]); db.execute("UPDATE businesses SET score=?,score_version=?,score_factors=? WHERE id=? AND organization_id=?",(scored["score"],scored["score_version"],json.dumps(scored["explanations"]),bid,org)) + class ApiHandler(BaseHTTPRequestHandler): server_version = "ProspectPlatform/0.1" def send_json(self, status, payload, extra_headers=None): @@ -869,7 +901,7 @@ class ApiHandler(BaseHTTPRequestHandler): def create_scoped_discovery(self, payload, db, user): criteria = payload.get("criteria", {}); seeds = payload.get("seed_urls") - selected_adapters = payload.get("selected_adapters", payload.get("sources", [])) + selected_adapters = resolve_selected_source_codes(db, user["organization_id"], payload.get("selected_adapters", payload.get("source_ids", payload.get("sources", [])))) source_mode = bool(selected_adapters) and seeds is None criteria_only = seeds is None and not source_mode if not isinstance(criteria, dict): return self.send_json(400, {"error": "invalid_criteria"}) @@ -1367,16 +1399,18 @@ class ApiHandler(BaseHTTPRequestHandler): out.append(x) return self.send_json(200,{"organization_id":org,"items":out,"limit":limit,"offset":offset,"has_more":len(rows)>limit}) def create_source(self,payload,db,user): - name=str(payload.get('name','')).strip(); kind=str(payload.get('source_code',payload.get('kind',''))).strip().lower(); config=payload.get('config',{}) - optional = kind not in ('csv','manual') - if not name or kind not in ('csv','manual','google_places','bing_local','approved_directory','public_website','permitted_social','ct_logs','dns','rdap') or not isinstance(config,dict):return self.send_json(400,{"error":"invalid_source"}) + name=str(payload.get('name','')).strip(); requested=str(payload.get('source_code') or payload.get('kind','')).strip().lower(); config=payload.get('config',{}) + provider_kinds={'openstreetmap','wikidata','common_crawl'} + kind='approved_directory' if requested in provider_kinds else requested + source_code=requested + if not name or requested not in ('csv','manual','google_places','bing_local','approved_directory','openstreetmap','wikidata','common_crawl','public_website','permitted_social','ct_logs','dns','rdap') or not isinstance(config,dict):return self.send_json(400,{"error":"invalid_source"}) if contains_secret(config):return self.send_json(400,{"error":"secret_not_permitted"}) try: - validation=adapter_for(kind).validate_config(config) + validation=adapter_for(source_code).validate_config(config) # Registration may precede the actual local payload or optional provider # approval. Discovery/ingest still validates the effective configuration. if config and not validation.valid:return self.send_json(400,{"error":"invalid_source_config","details":validation.errors}) - cur=db.execute("INSERT INTO sources(organization_id,name,kind,source_code,display_name,enabled,approved,config_json,policy_json,quota_json) VALUES(?,?,?,?,?,?,?,?,?,?)",(user['organization_id'],name,kind,kind,str(payload.get('display_name') or adapter_for(kind).display_name),int(bool(payload.get('enabled',False))),int(bool(payload.get('approved',config.get('approved',False)))),json.dumps(config,sort_keys=True),json.dumps(payload.get('policy',{}),sort_keys=True),json.dumps(payload.get('quota',{}),sort_keys=True))) + cur=db.execute("INSERT INTO sources(organization_id,name,kind,source_code,display_name,enabled,approved,config_json,policy_json,quota_json) VALUES(?,?,?,?,?,?,?,?,?,?)",(user['organization_id'],name,kind,source_code,str(payload.get('display_name') or adapter_for(source_code).display_name),int(bool(payload.get('enabled',False))),int(bool(payload.get('approved',config.get('approved',False)))),json.dumps(config,sort_keys=True),json.dumps(payload.get('policy',{}),sort_keys=True),json.dumps(payload.get('quota',{}),sort_keys=True))) except sqlite3.IntegrityError:return self.send_json(409,{"error":"duplicate_source"}) self.audit(db,user,'source.created',str(cur.lastrowid));db.commit();return self.send_json(201,row_json(db.execute("SELECT id,organization_id,name,kind,source_code,display_name,enabled,approved,policy_json,quota_json,health_status,consecutive_failures,circuit_open,last_success_at,last_failure_at,last_error,created_at,updated_at FROM sources WHERE id=?",(cur.lastrowid,)).fetchone())) def update_source(self,sid,payload,db,user): @@ -1385,7 +1419,7 @@ class ApiHandler(BaseHTTPRequestHandler): if 'config' in payload: config=payload.get('config') if not isinstance(config,dict) or contains_secret(config): return self.send_json(400,{"error":"invalid_source_config"}) - adapter=adapter_for(source['kind']); validation=adapter.validate_config(config) + adapter=adapter_for(source['source_code'] or source['kind']); validation=adapter.validate_config(config) if not validation.valid: return self.send_json(400,{"error":"invalid_source_config","details":validation.errors}) db.execute("UPDATE sources SET config_json=?,updated_at=CURRENT_TIMESTAMP WHERE id=? AND organization_id=?",(json.dumps(config,sort_keys=True),sid,user['organization_id'])) self.audit(db,user,'source.configured',str(sid)); db.commit() @@ -1393,7 +1427,7 @@ class ApiHandler(BaseHTTPRequestHandler): row=db.execute("SELECT * FROM sources WHERE id=?",(sid,)).fetchone(); return self.send_json(200,row_json(row)) value=int(bool(payload['enabled'])) if value: - adapter=adapter_for(source['kind']) + adapter=adapter_for(source['source_code'] or source['kind']) try: config=json.loads(source['config_json'] or '{}') except (TypeError,ValueError): config={} metadata=next((item for item in available_adapters() if item['source_code']==source['kind']), {}) @@ -1404,12 +1438,12 @@ class ApiHandler(BaseHTTPRequestHandler): # before they are enabled. if not validation.valid and not (source['kind'] == 'manual' and not config): return self.send_json(409,{"error":"source_not_configured","details":validation.errors}) - if source['kind'] == 'google_places' and not db.execute("SELECT 1 FROM source_credentials WHERE source_id=? AND organization_id=? AND provider=? AND key_name=?", (sid,user['organization_id'],'google_places','api_key')).fetchone(): + if source['source_code'] == 'google_places' and not db.execute("SELECT 1 FROM source_credentials WHERE source_id=? AND organization_id=? AND provider=? AND key_name=?", (sid,user['organization_id'],'google_places','api_key')).fetchone(): return self.send_json(409,{"error":"source_not_configured","details":["Google Places API key is required"]}) db.execute("UPDATE sources SET enabled=?,updated_at=CURRENT_TIMESTAMP WHERE id=?",(value,sid));self.audit(db,user,'source.enabled' if value else 'source.disabled',str(sid));db.commit();return self.send_json(200,row_json(db.execute("SELECT * FROM sources WHERE id=?",(sid,)).fetchone())) def create_query(self,payload,db,user): sid=payload.get('source_id');name=str(payload.get('name','')).strip();query=payload.get('query',{}) - selected=payload.get('selected_adapters',payload.get('sources',[])); location=str(payload.get('location','')).strip(); category=str(payload.get('category','')).strip(); schedule=str(payload.get('schedule','')).strip() + selected=resolve_selected_source_codes(db, user['organization_id'], payload.get('selected_adapters',payload.get('sources',[]))); location=str(payload.get('location','')).strip(); category=str(payload.get('category','')).strip(); schedule=str(payload.get('schedule','')).strip() try: max_records=int(payload.get('max_records',100)); daily_limit=int(payload.get('daily_limit',1000)) except (TypeError,ValueError): return self.send_json(400,{"error":"invalid_limits"}) if not isinstance(selected,list) or any(str(x) not in {a["source_code"] for a in available_adapters()} for x in selected): return self.send_json(400,{"error":"invalid_adapters"}) @@ -1431,7 +1465,7 @@ class ApiHandler(BaseHTTPRequestHandler): def test_source(self,sid,db,user): source=db.execute("SELECT * FROM sources WHERE id=? AND organization_id=?",(sid,user['organization_id'])).fetchone() if not source:return self.send_json(404,{"error":"not_found"}) - try: result=adapter_for(source['kind']).validate(json.loads(source['config_json'])); ok=result.valid; error='; '.join(result.errors) if not ok else None + try: result=adapter_for(source['source_code'] or source['kind']).validate(json.loads(source['config_json'])); ok=result.valid; error='; '.join(result.errors) if not ok else None except Exception as exc:ok=False;error=str(exc)[:300] if ok:db.execute("UPDATE sources SET health_status='healthy',consecutive_failures=0,circuit_open=0,last_success_at=CURRENT_TIMESTAMP,last_error=NULL WHERE id=?",(sid,));action='source.test.succeeded' else:db.execute("UPDATE sources SET health_status='unhealthy',consecutive_failures=consecutive_failures+1,circuit_open=CASE WHEN consecutive_failures+1>=3 THEN 1 ELSE circuit_open END,last_failure_at=CURRENT_TIMESTAMP,last_error=? WHERE id=?",(error,sid));action='source.test.failed' @@ -1445,7 +1479,7 @@ class ApiHandler(BaseHTTPRequestHandler): if len(json.dumps(config).encode())>5*1024*1024:return self.send_json(400,{"error":"ingest_limits"}) if isinstance(config.get('rows'),list) and (len(config['rows'])>1000 or any(not isinstance(r,dict) or len(r)>50 or any(len(str(v))>10000 for v in r.values()) for r in config['rows'])):return self.send_json(400,{"error":"ingest_limits"}) if isinstance(config.get('csv'),str) and config['csv'].count('\n')>1001:return self.send_json(400,{"error":"ingest_limits"}) - try:page=adapter_for(source['kind']).discover(source_config_with_credentials(db, source, config)) + try:page=adapter_for(source['source_code'] or source['kind']).discover(source_config_with_credentials(db, source, config)) except (ValueError,KeyError) as exc:return self.send_json(400,{"error":"invalid_ingest","detail":str(exc)}) inserted=0 for record in page.records[:1000]: @@ -1460,7 +1494,7 @@ class ApiHandler(BaseHTTPRequestHandler): if not source:return self.send_json(404,{"error":"not_found"}) try: config=json.loads(source["config_json"] or "{}") except (TypeError,ValueError): config={} - health=adapter_for(source["kind"]).health_check(config) + health=adapter_for(source["source_code"] or source["kind"]).health_check(config) return self.send_json(200,{"id":sid,"source_code":source["source_code"] or source["kind"],"display_name":source["display_name"] or source["name"],"status":source["health_status"],"configured":health.status=="healthy","circuit_open":bool(source["circuit_open"]),"consecutive_failures":source["consecutive_failures"],"last_error":source["last_error"]}) def discovery_run_action(self,rid,action,db,user): run=db.execute("SELECT * FROM discovery_runs WHERE id=? AND organization_id=?",(rid,user["organization_id"])).fetchone() @@ -1583,7 +1617,7 @@ def _run_source_discovery(db, job, handler): used_today=db.execute("SELECT COUNT(*) FROM source_records WHERE organization_id=? AND source_id=? AND date(created_at)=date('now')",(org,source["id"])).fetchone()[0] if used_today >= daily_limit or per_run_limit <= 0: handler.add_job_event(db,job["id"],org,"source.quota_exceeded",f"Quota reached for {source['kind']}",10,"SOURCE_QUOTA_EXCEEDED"); continue - page=adapter_for(source["kind"]).discover(source_config_with_credentials(db, source, config)) + page=adapter_for(source["source_code"] or source["kind"]).discover(source_config_with_credentials(db, source, config)) except Exception as exc: failures=int(source["consecutive_failures"])+1 db.execute("UPDATE sources SET health_status='unhealthy',consecutive_failures=?,circuit_open=?,last_failure_at=CURRENT_TIMESTAMP,last_error=?,updated_at=CURRENT_TIMESTAMP WHERE id=?",(failures,int(circuit_is_open(failures)),str(exc)[:300],source["id"])) @@ -1613,6 +1647,7 @@ def _run_source_discovery(db, job, handler): if normalized["website_domain"] and not db.execute("SELECT 1 FROM domains WHERE organization_id=? AND business_id=? AND domain=?",(org,bid,normalized["website_domain"])).fetchone(): db.execute("INSERT INTO domains(business_id,organization_id,domain,kind) VALUES(?,?,?,?)",(bid,org,normalized["website_domain"],"website")) if normalized["website"] and not db.execute("SELECT 1 FROM websites WHERE organization_id=? AND business_id=? AND url=?",(org,bid,normalized["website"])).fetchone(): db.execute("INSERT INTO websites(business_id,organization_id,url,website_class) VALUES(?,?,?,?)",(bid,org,normalized["website"],"business_site")) if normalized["website"] or normalized["website_domain"]: db.execute("INSERT OR IGNORE INTO evidence(business_id,organization_id,kind,url,claim) VALUES(?,?,?,?,?)",(bid,org,"source_record",normalized["website"],"Discovered by "+source["kind"])) + if normalized["website"]: enrich_source_business(db,org,bid,normalized["website"],source["source_code"] or source["kind"]) if normalized["email"] or normalized["phone"]: if not db.execute("SELECT 1 FROM contacts WHERE organization_id=? AND business_id=? AND email=? AND phone=?",(org,bid,normalized["email"],normalized["phone"])).fetchone(): db.execute("INSERT INTO contacts(business_id,organization_id,email,phone) VALUES(?,?,?,?)",(bid,org,normalized["email"],normalized["phone"])) if record_id: db.execute("UPDATE source_records SET processing_status='processed',normalized_json=?,normalized_key=? WHERE id=? AND organization_id=?",(norm,nkey,record_id,org)); db.execute("INSERT OR IGNORE INTO enrichment_queue(organization_id,source_record_id,status) VALUES(?,?,?)",(org,record_id,"completed")); db.execute("UPDATE enrichment_queue SET status='completed',updated_at=CURRENT_TIMESTAMP WHERE source_record_id=? AND organization_id=?",(record_id,org)) @@ -1669,13 +1704,30 @@ def _job_worker(server): db.commit() finally: db.close() +def _schedule_worker(server): + """Lightweight durable scheduler for daily/weekday/weekly discovery queries.""" + while not server.job_stop.is_set(): + db=connect(server.db_path) + try: + now=datetime.now(timezone.utc); today=now.date().isoformat() + for query in db.execute("SELECT * FROM discovery_queries WHERE enabled=1 AND schedule IN ('daily','weekdays','weekly')"): + if now.hour < 9 or (query["schedule"]=='weekdays' and now.weekday()>4) or (query["schedule"]=='weekly' and now.weekday()!=0): continue + if db.execute("SELECT 1 FROM discovery_runs WHERE organization_id=? AND criteria_json=? AND date(created_at)=date('now') LIMIT 1",(query["organization_id"],query["query_json"])).fetchone(): continue + key=f"scheduled-query-{query['id']}-{today}" + if db.execute("SELECT 1 FROM jobs WHERE organization_id=? AND idempotency_key=?",(query["organization_id"],key)).fetchone(): continue + cur=db.execute("INSERT INTO jobs(organization_id,idempotency_key,type,payload) VALUES(?,?,?,?)",(query["organization_id"],key,"source_discovery",json.dumps({"discovery_query_id":query["id"],"selected_adapters":json.loads(query["selected_adapters_json"] or '[]'),"max_records":query["max_records"],"daily_limit":query["daily_limit"]}))) + db.execute("INSERT INTO discovery_runs(organization_id,job_id,selected_adapters_json,location,category,max_records,daily_limit,schedule,dry_run,lifecycle,criteria_json,seed_urls_json) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",(query["organization_id"],cur.lastrowid,query["selected_adapters_json"],query["location"],query["category"],query["max_records"],query["daily_limit"],query["schedule"],query["dry_run"],"queued",query["query_json"],"[]")) + db.commit() + finally: db.close() + server.job_wakeup.set(); server.job_stop.wait(30) + def create_server(host="127.0.0.1",port=8000,db_path="prospects.db"): load_config() configure_ai_research_db(db_path) - server=ThreadingHTTPServer((host,port),ApiHandler);server.db_path=db_path;connect(db_path).close();server.job_stop=threading.Event();server.job_wakeup=threading.Event();server.job_thread=threading.Thread(target=_job_worker,args=(server,),daemon=True);server.job_thread.start() + server=ThreadingHTTPServer((host,port),ApiHandler);server.db_path=db_path;connect(db_path).close();server.job_stop=threading.Event();server.job_wakeup=threading.Event();server.job_thread=threading.Thread(target=_job_worker,args=(server,),daemon=True);server.job_thread.start();server.schedule_thread=threading.Thread(target=_schedule_worker,args=(server,),daemon=True);server.schedule_thread.start() original_close=server.server_close def close(): - server.job_stop.set();server.job_wakeup.set();server.job_thread.join(timeout=2);original_close() + server.job_stop.set();server.job_wakeup.set();server.job_thread.join(timeout=2);server.schedule_thread.join(timeout=2);original_close() server.server_close=close return server if __name__=="__main__": diff --git a/apps/api/app/sources.py b/apps/api/app/sources.py index e89b620..78e1066 100644 --- a/apps/api/app/sources.py +++ b/apps/api/app/sources.py @@ -315,6 +315,7 @@ class ApprovedDirectorySource(GatedSource): display_name = "Free public directories" available = True requires_credentials = False + optional = False def validate_config(self, config): result=super().validate_config(config) @@ -375,12 +376,36 @@ class GooglePlacesSource(GatedSource): records.append(normalize_record({"name":name, "website":place.get("websiteUri", ""), "phone":place.get("nationalPhoneNumber") or place.get("internationalPhoneNumber", ""), "location":place.get("formattedAddress", ""), "source_url":place.get("googleMapsUri", "") , "description":"Google Places result"})) return DiscoveryPage(records, metadata={"adapter":self.source_code,"record_count":len(records),"provider":"google_places"}) +class OpenStreetMapSource(ApprovedDirectorySource): + kind = source_code = "openstreetmap" + display_name = "OpenStreetMap / Overpass" + optional = False + def discover(self, config, cursor=None): + page = super().discover({**dict(config), "provider": "openstreetmap"}, cursor) + return DiscoveryPage(page.records, page.next_cursor, {**page.metadata, "adapter": self.source_code}) + +class WikidataSource(ApprovedDirectorySource): + kind = source_code = "wikidata" + display_name = "Wikidata" + optional = False + def discover(self, config, cursor=None): + page = super().discover({**dict(config), "provider": "wikidata"}, cursor) + return DiscoveryPage(page.records, page.next_cursor, {**page.metadata, "adapter": self.source_code}) + +class CommonCrawlSource(ApprovedDirectorySource): + kind = source_code = "common_crawl" + display_name = "Common Crawl index" + optional = False + def discover(self, config, cursor=None): + page = super().discover({**dict(config), "provider": "common_crawl"}, cursor) + return DiscoveryPage(page.records, page.next_cursor, {**page.metadata, "adapter": self.source_code}) + def _gated(code, name): return type(name.replace(" ", ""), (GatedSource,), {"kind":code, "source_code":code, "display_name":name}) BingLocalSource = _gated("bing_local", "Bing / approved local API") PermittedSocialSource = _gated("permitted_social", "Permitted social") -ADAPTERS = {x.source_code: x for x in (ManualSource, CsvSource, GooglePlacesSource, BingLocalSource, ApprovedDirectorySource, PublicWebsiteSource, PermittedSocialSource, CtLogsSource, DnsSource, RdapSource)} +ADAPTERS = {x.source_code: x for x in (ManualSource, CsvSource, GooglePlacesSource, BingLocalSource, ApprovedDirectorySource, OpenStreetMapSource, WikidataSource, CommonCrawlSource, PublicWebsiteSource, PermittedSocialSource, CtLogsSource, DnsSource, RdapSource)} # common aliases used by clients ADAPTER_REGISTRY = ADAPTERS diff --git a/apps/api/tests/test_sources_phase5.py b/apps/api/tests/test_sources_phase5.py index 88bb558..9313be4 100644 --- a/apps/api/tests/test_sources_phase5.py +++ b/apps/api/tests/test_sources_phase5.py @@ -13,7 +13,7 @@ class SourceAdapterTests(unittest.TestCase): self.assertFalse(catalog[code]['available'], code) self.assertTrue(catalog[code]['optional'], code) self.assertTrue(catalog['approved_directory']['available']) - self.assertTrue(catalog['approved_directory']['optional']) + self.assertFalse(catalog['approved_directory']['optional']) self.assertTrue(catalog['google_places']['available']) self.assertTrue(catalog['google_places']['optional']) diff --git a/apps/web/asset-manifest.json b/apps/web/asset-manifest.json index 5805bfe..f9d228d 100644 --- a/apps/web/asset-manifest.json +++ b/apps/web/asset-manifest.json @@ -1,6 +1,6 @@ { "schema": 1, - "version": "phase-24", + "version": "phase-26", "entrypoints": [ "config.js", "app.js", @@ -13,10 +13,10 @@ "healthz" ], "integrity": { - "config.js": "sha256-29953b8de308d7831c606d2116b30ae59744fc0e45f998d970fe71fcbd80fc2a", + "config.js": "sha256-036e9b1a2d16cf89ffc1cb23f22aab71608b1330e8323d2d42a6c6b86051698c", "app.js": "sha256-44809719afe72cd0ddc30819b3c73f36019319bfbda4e59d62055fd040b5c6c0", "styles.css": "sha256-a9fd194b001c0de98775bcf5fbba5da676ccfdd607a889eae84bd30a54c15e08", - "index.html": "sha256-f72befe537a05b7172f94346b367c75f364b0895b9dac233cc6bd78220a222d9", + "index.html": "sha256-f48882419575ac49c389f07ba25791115981330a1dd623ce3c21b026a0dedd3c", "health.html": "sha256-c352a6f37aa24628cfc8d5709a70ff2d94181d192ed5fe00916cca9378a61d81", "error.html": "sha256-f3cc28d2dfc9e8af112d0257c6ee47b9e9146e5da8590a754dd589bf43702aaf", "healthz": "sha256-dc51b8c96c2d745df3bd5590d990230a482fd247123599548e0632fdbf97fc22" diff --git a/apps/web/config.js b/apps/web/config.js index 5eba613..feb5584 100644 --- a/apps/web/config.js +++ b/apps/web/config.js @@ -1,5 +1,5 @@ /* Public, non-secret runtime configuration. Replace this file at deploy time if needed. */ window.__PROSPECT_CONFIG__ = Object.freeze({ apiBase: '', - assetVersion: 'phase-24' + assetVersion: 'phase-26' }); diff --git a/apps/web/index.html b/apps/web/index.html index 3e1796a..033b9c9 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -5,7 +5,7 @@
REVIEW REQUIRED