Root cause: the final restart block called Get-WebAppPoolState first to
decide Restart- vs Start-WebAppPool. With \Continue = 'Stop',
any transient error from that check (most likely right after
Wait-AppPoolFullyStopped had to force-kill a lingering worker process,
leaving the pool's WAS state briefly inconsistent) threw an uncaught
error and aborted the rest of the script - silently skipping the admin
site/pool start even though the public site had already restarted
successfully by that point.
Fix: Start-WebAppPool/Start-Website are no-ops if already started, so
drop the state check entirely. Both public and admin site/pool starts
now go through Start-SiteAndPool, which wraps each call independently
in try/catch so one failing doesn't prevent the other from starting,
and reports (rather than swallows or aborts on) any failure.
Stop-WebAppPool only requests a stop - the w3wp.exe worker process can
keep running for a few seconds afterwards, still holding the site's
files open, so the previous fix (stopping both sites before the wipe)
wasn't sufficient on its own.
- Wait-AppPoolFullyStopped: polls app pool state until Stopped (30s
timeout), then force-kills any lingering w3wp.exe for that pool
- Remove-DirectoryContentsWithRetry: retries the wipe a few times with
a short delay, since file handles can take a moment to release even
after the worker process is confirmed gone
The admin site's physical path lives inside RemoteDir (RemoteDir\public-admin).
On a redeploy it was still running while RemoteDir was wiped, so IIS held its
files open and Remove-Item failed with Access is denied. Now both the public
and admin site/app pool are stopped up front, before the wipe.
- New AdminOrdersController + view: table of Orders with search by
email/jurisdiction #, pagination (20/page), and per-row mailto link
to resend the order-continue URL
- OrdersRepository: add GetAll() and SearchPaged() (LIKE search,
PagedQuery-based pagination)
- MailtoEncode helper added to core/helpers.asp (mailto-safe URL
encoding: %20 not +)
- Controller registered in autoload + ControllerRegistry
- public-admin/ folder: dedicated Default.asp routes, web.config with
192.168.1.0/24 ipSecurity, production template with same restriction
- applicationhost.config: second IIS Express site (Admin Web Site,
port 8081)
- run_site.cmd: launches both sites (public in separate window,
admin in foreground)
- build-release.ps1: public-admin in allow-list, swaps its
web.config from production template
- deploy-iis-remote-apply.ps1: optional admin site/app-pool config
+ restart
- deploy-iis.ps1: -AdminSiteName/-AdminAppPool/-AdminBaseUrl params,
admin smoke test
The top-level prune step deleted the whole db/ folder before the
second step meant to keep db/migrations under it ever ran, so no
deploy has actually shipped migrations - the remote apply step was
running runMigrations.vbs against an empty migrations folder and
silently reporting nothing pending.
Also adds scripts/run-migrations-remote(-apply).ps1, a lighter
migrations-only counterpart to deploy-iis.ps1 for catching a
production schema up without redeploying app code - used just now to
patch prod after this bug meant it never received prior migrations.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- scp/ssh now force a fallback to password auth so a missing/rejected
key fails with a real prompt instead of an opaque "Permission denied
(publickey)".
- Smoke test now checks each path against its expected status (/404 is
expected to 404) instead of treating any non-throwing response as a
pass, and surfaces the actual failure reason.
- The remote IIS server has the 64-bit Access Database Engine, unlike
the local dev machine, so its app pool stays 64-bit and migrations
run through the plain (64-bit) cscript.exe instead of SysWOW64.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- .gitattributes + an explicit allow-list in build-release.ps1 assemble a
clean release tree (app/, core/, public/, scripts/, db/migrations only) -
anything not on the allow-list is deleted from the deploy, so a stray
file added later can't ship to production by accident.
- public/web.config.production.template holds the production appSettings
(DB path outside the deploy dir, Environment=Production, error logging on).
- deploy-iis.ps1 builds, zips, and scp's a release to the IIS host, then
runs deploy-iis-remote-apply.ps1 there over ssh to swap in the new files,
run migrations, and restart the site/app pool - the DB and error log live
outside the deploy directory so a redeploy never touches them.
- Removed deploy-iis-git.ps1 and migrate_isbusiness_to_households.vbs,
leftovers from a different template project that didn't apply here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RouteKit Classic ASP MVC starter for the Purple Envelope election-materials
order site: request-order flow, token-based order continuation, SurveyJS
order-details form with server-side validation and a themed confirmation
email, plus the framework core, generators, and dev-only aspunit test harness.