- deploy.sh/deploy-dev.sh: bring up listmonk-hc (upstream image, excluded from
build); document the one-time listmonk_hc DB create + --install.
- docker-compose.dev.override.yml: dev-only override (committed) that drops the
prod host-port bindings and pins dev's own postgres volume (dev-pgdata) via
compose !override tags. deploy-dev ships it as docker-compose.override.yml so
syncing the canonical compose to the shared host no longer breaks dev's
api-postgres (port :5432 clash + volume switch). Discovered + fixed while
validating listmonk-hc on dev.
- pw-hc-rampcap.sh: healthcare analogue of pw-listmonk-rampcap, ramps the
listmonk_hc cap 100->1000/h off /etc/postfix/hc-warmup-start, fully
independent of the trucking ramp/cap.
Isolated from the trucking listmonk: own DB (listmonk_hc), own uploads volume,
own sliding-window cap. Configured (on dev) with 3 SMTP servers pointing at the
host Postfix hc submission ports 2526/2527/2528 so it round-robins the dedicated
hc IPs .107/.108/.109. Reaches the host MTA via the docker bridge gateway.
Note: the listmonk image needs an explicit one-time '--install --idempotent
--yes' against listmonk_hc (env vars alone do not auto-install this image tag).
Validated on dev: listmonk-hc container (172.19.0.16) -> host :2526 (hcsubmit107)
-> hcout1 (.107) -> real gmail MX; both listmonk instances Up.
Adds 3 hc submission ports (2526/2527/2528) in the single Postfix instance,
each content_filter'd onto a dedicated hc transport (hcout1/2/3) binding the
hc IPs .107/.108/.109 with hc HELO identity (hcmta01-03) and hotter concurrency.
listmonk-hc round-robins the 3 ports.
Discovered + documented the constraint that drove this shape: transport_maps
randmap is owned by the shared trivial-rewrite(8) and is global, so neither a
per-smtpd -o transport_maps nor a FILTER randmap:{...} can scope a separate IP
pool (FILTER parses randmap as a literal transport). content_filter=hcoutN:
(empty nexthop) overrides transport_maps and keeps the real recipient domain.
Verified end-to-end on the server: :2527 -> hcout2 (.108) -> real gmail MX;
trucking transport_maps (.94-.96) untouched. Idempotent, postfix-check gated
with auto-rollback.
Add scripts/healthcare_email_streams.py as the single source of truth for
classifying NPPES-endpoint emails into institutional (HOT stream) / consumer
(trucking-discipline stream) / direct (DirectTrust, parked), plus an exclude set
for non-prospect giants (va.gov, *.mil, cvshealth, walgreens, walmart).
Rework build_npi_outreach_lists.py to emit one CSV per stream
(npi_healthcare_institutional/consumer + npi_direct_secure), overdue-first
sorted, with companion files (revalidation/leie/optout) now optional.
Verified on May 2026 NPPES endpoint_pfile: 89,557 institutional / 19,366 consumer
/ 242,441 direct rows.
Measured against May 2026 NPPES endpoint_pfile (tightened HISP filter):
- 92,592 institutional NPIs across 38,873 practice domains (76% single-provider)
- 19,072 consumer-webmail NPIs (ride trucking discipline)
- 242,441 Direct/HISP rows parked until DirectTrust.
Decisions: single Postfix + class hc transport (:2526), 2nd Listmonk instance
(listmonk-hc, own cap + own listmonk_hc DB), 10k/day institutional ceiling.
Today one global Listmonk cap + shared Postfix rotation pool governs all mail,
sized to protect consumer-ISP (Gmail/MS/Yahoo) reputation for trucking cold mail.
Healthcare practice-domain (institutional) mail has an independent deliverability
profile and should run hotter without endangering the warmed trucking IPs.
Plan: isolate two streams sharing one Postfix/Listmonk:
- carve hc-dedicated sending IPs (.107-.109) with their own PTR/SPF + warmup;
- a 2nd Postfix submission service (:2526) bound to the hc pool;
- a 2nd Listmonk instance (or SMTP server) with its own sliding-window cap;
- split the healthcare list into institutional (hot) vs consumer-webmail (rides
trucking discipline) vs DirectTrust (parked);
- free MX+SMTP verify the institutional list on a non-sending IP first.
Includes mermaid topology, separate hc warmup/cap schedule, validation (isolation/
identity/deliverability/cap proofs), and open decisions for sizing.
deploy.sh: include proxy-relay in the default service set; it's an upstream
image (ginuerzh/gost) with no build context, so exclude it from 'compose build'
while keeping it in 'compose up'.
deploy-dev.sh: rsync docker-compose.yml to the dev server (it was never synced,
so new services like proxy-relay never reached dev) and add proxy-relay to the
'compose up --build' set.
Chromium rejects authenticated SOCKS5 ('Browser does not support socks5 proxy
authentication'). Add a gost (ginuerzh/gost:2.11.5) 'proxy-relay' sidecar that
listens unauthenticated on socks5://proxy-relay:11080 and forwards to the
authenticated residential upstream (HEALTHCARE_PROXY_UPSTREAM_URL). Workers point
Playwright at the relay via HEALTHCARE_PROXY_URL=socks5://proxy-relay:11080.
env template: split into HEALTHCARE_PROXY_UPSTREAM_URL (authenticated, password
percent-encoded so '#' -> %23) and HEALTHCARE_PROXY_URL (the relay address).
Validated end-to-end on dev: workers Chromium -> proxy-relay -> residential
egress IP 76.228.206.147; NPPES + PECOS both HTTP 200.
The residential proxy password contains a '#', which urlparse() misreads as a
URL fragment and corrupts the port (ValueError: Port could not be cast...).
Parse scheme://creds@host:port manually and percent-decode user/pass so both
raw ('#') and encoded ('%23') passwords work. Verified against the live
credential.
CMS healthcare portals (NPPES, PECOS, I&A) block datacenter IPs, so the
healthcare browser automation needs to egress via the residential proxy on
hg409y7ez04.sn.mynetname.net (username 'performancewest').
- undetected_browser: use_proxy now accepts an env-var name, so callers can
select a domain-specific proxy. _proxy_config(proxy_env) reads it and falls
back to UNDETECTED_PROXY_URL. Healthcare uses 'HEALTHCARE_PROXY_URL'.
- probe_npi_undetected: launches with use_proxy='HEALTHCARE_PROXY_URL' when set.
- npi_provider: documents that the (future) automated NPPES/PECOS flows must
use the healthcare proxy.
- Plumb HEALTHCARE_PROXY_URL (+ UNDETECTED_PROXY_URL fallback) through the
ansible env template and docker-compose workers env.
The credential itself is NOT in the repo. Set the full URL in the ansible
vault as vault_healthcare_proxy_url:
socks5://performancewest:<password>@hg409y7ez04.sn.mynetname.net:<port>
Verified parsing + Playwright proxy-dict wiring with a unit test.
The site header / Services mega-dropdown was duplicated across two render
systems (Astro pages via Base.astro->nav.html, and ~80 pre-rendered static
public/**/index.html pages each embedding their own copy). They had drifted
into 5 different variants (missing 'New Carrier Setup', misplaced Healthcare
column, NEW vs FREE badges, em-dash encoding differences), so
dev.performancewest.net, the order pages, and the rest of the site disagreed.
- Make site/src/partials/nav.html the single source of truth (adopts the most
complete variant).
- Add scripts/sync_nav.py to rewrite every static page's <nav> block from
nav.html (idempotent; --check guards against drift in CI/deploy).
- Run the sync automatically in deploy.sh and scripts/deploy-dev.sh.
- Deprecate scripts/inject_healthcare_nav.py (now delegates to sync_nav.py).
- Neutralize the broken no-op SiteNav.astro component.
All 80 headers + the Astro-built order pages now render the identical dropdown.
Copy: drop paper/electronic/fax framing across the revalidation + enrollment
marketing pages and the order-confirmation email; present two service tiers:
- Standard filing (no CMS account; we prepare CMS-855, you sign, we submit to MAC)
- Expedited filing (CMS I&A surrogate access; same-day PECOS filing + tracking)
Internal worker todos + the _STANDARD_FILING_SLUGS identifier updated to match.
New scripts/test_healthcare_e2e.py validates the whole order line (slug
consistency x6 places, price agreement, intake field collection+enforcement,
worker dispatch, handler execution producing CMS-855 PDF+anchor, free-tool
action_urls). 45 checks.
Bugs found + fixed by the test:
- medicare-enrollment requires practice_state server-side but the wizard never
enforced it -> orders could be paid then stall. Wizard now requires it.
- determine_form_type defaulted org NPIs to the individual 855I because
enumeration_type is never collected -> wrong form, CMS rejection. Now does a
live NPPES lookup (safe 855I fallback).
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.
- cms855_pdf_filler.py: fills official CMS-855I/B/O/A AcroForms from intake
(name, NPI, DOB, cert-page printed name) and records the signature anchor at
the form's official /Sig box so the e-sign stamper lands on the cert line.
- npi_provider handlers (revalidation/reactivation/enrollment) now generate the
paper CMS-855, upload it to MinIO, request_esign with anchors, and email the
signing link. Human completes/verifies + USPS Priority Mails to the MAC.
- scripts/Dockerfile: copy the official CMS-855I/B/O/A forms into the image.
- order-confirmation email presents both filing methods: paper CMS-855 (no
account needed, client e-signs one page, we print+mail to their MAC) and
I&A surrogacy (faster, needs CMS account). NPPES-only services note that
surrogacy is required (web-only).
- npi_provider handlers record the access model per service in admin todos.
- marketing copy leads with the lowest-friction paper option.
Adds a systemd-timed worker that nudges customers who paid but never completed
their intake form (which stalls fulfillment).
- migration 087: intake_reminder_count + intake_reminder_last_at on
compliance_orders (makes the daily run idempotent and bounded), plus a
partial index for the paid-order eligibility scan.
- scripts/workers/intake_reminder.py: each run emails any paid order with
intake_data_validated != TRUE, capped at 10 reminders/order, at most one
consolidated email per customer per day (groups a customer's incomplete
services into one email). Reuses the post-payment intake URL format
(/order/{slug}?order={n}) and the API's email validation, skipping
placeholder/invalid addresses (synthetic@, pipeline.com, etc.). Sends via
smtplib with SMTP_PASS (verified working in the worker container).
- worker-crons: pw-intake-reminder timer, daily ~noon ET (16:00 UTC).
Two build fixes surfaced while shipping the set-password rename:
1. erpnext/Dockerfile cloned frappe/payments unpinned; its default branch now
requires Python >=3.14 while frappe/erpnext:v15 ships 3.11, so the image
build failed with 'Package payments requires a different Python'. Pin the
clone to --branch version-15.
2. deploy.sh built the erpnext image without first staging the custom Frappe
apps into the build context (erpnext/build.sh). That meant a baked-code
change could silently ship stale code. Stage apps when erpnext is built.
Root cause of the 'Link invalid' onboarding link: Frappe's TemplatePage
resolves a www page's Python controller by converting hyphens to underscores
(see frappe/website/page_renderers/template_page.py set_pymodule: it looks for
'set_password.py' next to 'set-password.html'). Our controller was named
'set-password.py' (hyphen), so os.path.exists() missed it, pymodule_name stayed
None, get_context never ran over HTTP, and the template rendered with no
context -> raw {{ email }}, title 'Link invalid', token never verified. (It
worked under bench/in-process only because we called get_context directly.)
Fix: rename www/set-password.py -> www/set_password.py (route stays
/set-password, driven by the .html filename) and update the whitelisted submit
endpoint path in set-password.html to ...www.set_password.submit.
NOTE: the sibling legacy CRTC/CDR admin pages (admin-filings.py,
admin-resellers.py, cdr-*.py) have the same latent hyphen bug; left as-is since
they're outside the compliance portal, but they are silently controller-less.