Add Healthcare/NPI section to nav dropdown across all static pages

The site's pre-rendered public/**/index.html pages each embed their own copy
of the Services mega-dropdown and do not read src/partials/nav.html, so the
earlier nav.html-only edit never appeared. inject_healthcare_nav.py adds the
canonical Healthcare block (Medicare Revalidation, Medicare Enrollment, NPI/
NPPES Services, free NPI Compliance Check) to the desktop Column 3 + mobile
menu of all 80 static pages. Idempotent.
This commit is contained in:
justin 2026-06-05 03:05:19 -05:00
parent e212f20a34
commit 5cfe9702e2
82 changed files with 332 additions and 160 deletions

View file

@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Inject the Healthcare nav column into the pre-rendered static pages.
The site is mostly static HTML under site/public/**/index.html, each carrying
its own copy of the Services mega-dropdown (desktop + mobile). The Astro
Base.astro layout reads src/partials/nav.html, but the static pages do NOT,
so adding a sector to nav.html alone does not show up on those pages.
This injects the (canonical) Healthcare block from nav.html into every static
page that has the dropdown but is missing Healthcare. Idempotent.
"""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PUBLIC = ROOT / "site" / "public"
NAV = ROOT / "site" / "src" / "partials" / "nav.html"
nav = NAV.read_text()
m_desk = re.search(
r'(<p class="text-\[11px\][^>]*>Healthcare</p>.*?npi-compliance-check.*?</a>)',
nav, re.S,
)
m_mob = re.search(
r'(<p class="text-xs font-semibold[^>]*>Healthcare</p>.*?npi-compliance-check.*?</a>)',
nav, re.S,
)
if not (m_desk and m_mob):
sys.exit("Could not extract Healthcare blocks from nav.html")
DESKTOP_BLOCK = m_desk.group(1)
MOBILE_BLOCK = m_mob.group(1)
# Desktop insertion: place the Healthcare block at the end of Column 3, right
# before the "Form a Business" CTA that closes that column.
DESKTOP_ANCHOR = '<a href="/order/formation" class="mt-3 block py-2 px-3 text-sm font-medium text-white bg-pw-700 hover:bg-pw-800 rounded-lg text-center transition-colors">Form a Business</a>'
# Mobile insertion: place Healthcare right before the mobile Corporate heading.
MOBILE_ANCHOR = '<p class="text-xs font-semibold text-slate-500 uppercase tracking-wider px-2 pt-3">Corporate</p>'
def inject(html: str) -> tuple[str, bool]:
if "services-menu" not in html:
return html, False # no dropdown on this page
if "npi-compliance-check" in html:
return html, False # already has Healthcare
changed = False
if DESKTOP_ANCHOR in html and DESKTOP_BLOCK not in html:
html = html.replace(DESKTOP_ANCHOR, DESKTOP_BLOCK + " " + DESKTOP_ANCHOR, 1)
changed = True
if MOBILE_ANCHOR in html and MOBILE_BLOCK not in html:
html = html.replace(MOBILE_ANCHOR, MOBILE_BLOCK + " " + MOBILE_ANCHOR, 1)
changed = True
return html, changed
def main():
files = sorted(PUBLIC.rglob("index.html")) + [PUBLIC / "404.html"]
touched, skipped, nodrop = 0, 0, 0
for f in files:
if not f.exists():
continue
html = f.read_text()
new, changed = inject(html)
if changed:
f.write_text(new)
touched += 1
elif "services-menu" not in html:
nodrop += 1
else:
skipped += 1
print(f"injected: {touched} already-had/partial: {skipped} no-dropdown: {nodrop}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,90 @@
"""Probe whether our undetected (patchright) browser can reach NPPES / PECOS
and how detectable it looks. Honest, no assertions from memory it visits
real endpoints and a fingerprint-detection page and prints what it sees.
Run: python3 scripts/probe_npi_undetected.py
"""
import asyncio
import sys
sys.path.insert(0, "scripts")
from workers.services.telecom.undetected_browser import ( # noqa: E402
undetected_browser, is_using_patchright,
)
TARGETS = [
# NPPES public registry UI (where NPI lookups/updates happen)
("NPPES registry", "https://npiregistry.cms.hhs.gov/"),
# NPPES public API (already used by our free tool — sanity check)
("NPPES API", "https://npiregistry.cms.hhs.gov/api/?version=2.1&number=1234567893"),
# PECOS / I&A login surface (Identity & Access)
("PECOS portal", "https://pecos.cms.hhs.gov/pecos/login.do"),
("I&A portal", "https://nppes.cms.hhs.gov/IAWeb/login.do"),
]
# Public bot-detection fingerprint check.
SANNYSOFT = "https://bot.sannysoft.com/"
async def probe(headless: bool):
print(f"\n{'='*60}\nbackend = {'patchright' if is_using_patchright() else 'vanilla-playwright'} | headless={headless}\n{'='*60}")
async with undetected_browser(headless=headless) as (ctx, page):
# 1. navigator.webdriver + a couple of fingerprint signals
try:
await page.goto("about:blank")
fp = await page.evaluate("""() => ({
webdriver: navigator.webdriver,
plugins: navigator.plugins.length,
languages: navigator.languages,
chrome: typeof window.chrome,
ua: navigator.userAgent,
})""")
print("fingerprint:", fp)
except Exception as e:
print("fingerprint eval failed:", e)
# 2. real target reachability
for name, url in TARGETS:
try:
resp = await page.goto(url, wait_until="domcontentloaded", timeout=30000)
status = resp.status if resp else "?"
title = await page.title()
body = (await page.content())[:400].lower()
blocked = any(w in body for w in [
"access denied", "are you a human", "captcha", "blocked",
"incapsula", "akamai", "unusual traffic", "request unsuccessful",
])
print(f" [{status}] {name:14} blocked={blocked} title={title[:60]!r}")
except Exception as e:
print(f" [ERR] {name:14} {type(e).__name__}: {str(e)[:80]}")
# 3. sannysoft fingerprint scorecard (count red FAILs)
try:
await page.goto(SANNYSOFT, wait_until="networkidle", timeout=30000)
await asyncio.sleep(2)
fails = await page.evaluate("""() => {
const rows = [...document.querySelectorAll('tr')];
const bad = [];
for (const r of rows) {
const cls = r.className || '';
const txt = r.innerText.replace(/\\s+/g,' ').trim();
if (/fail|warn/i.test(cls)) bad.push(txt.slice(0,80));
}
return bad;
}""")
if fails:
print(f" sannysoft FAIL/WARN rows ({len(fails)}):")
for f in fails:
print(f" - {f}")
else:
print(" sannysoft: no FAIL/WARN rows detected (clean)")
except Exception as e:
print(" sannysoft check failed:", e)
async def main():
await probe(headless=True)
if __name__ == "__main__":
asyncio.run(main())

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long