TownOfWiley Website
- Production deploys come from
mainthrough Amplify. - Use short-lived feature branches and merge into
mainonly when the change is build-safe. - Keep deployable app changes in
src/,public/,package*.json,angular.json,tsconfig*,amplify.yml, andscripts/generate-runtime-config.mjs. - Keep maintainer-facing docs and runbooks tracked in the repo under
docs/,README.md,CLERK-CMS-GUIDE.md,bot-training/, and related operational paths. - Do not commit local reports, temp logs, or machine-specific artifacts.
- GitHub Actions validates deployable paths; Amplify handles the production deployment after
mainupdates. - GitHub Actions uses targeted caches for npm, Playwright browsers, and Angular CLI build artifacts; Amplify keeps its own build cache through
amplify.yml.
Detailed policy: docs/git-workflow.md
- Supported production Node lines:
24.xpreferred,22.xacceptable - Do not deploy or build on odd-numbered current releases such as
25.x - This workspace was last observed on local
node v25.2.1, so switch locally before build and test runs if you want parity with Amplify
Recommended local workflow:
nvm install 24
nvm use 24
node -v- App name:
Townofwiley - App ID:
d331voxr1fhoir - Region:
us-east-2 - Repository:
https://github.com/Bigessfour/Townofwiley - Default domain:
d331voxr1fhoir.amplifyapp.com - Production branch:
main - Build command:
npm run build - Build output:
dist/townofwiley-app/browser - Node runtime for Amplify builds:
24.x
Amplify build spec:
version: 1
frontend:
phases:
preBuild:
commands:
- nvm install 24
- nvm use 24
- npm ci
build:
commands:
- npm run build
artifacts:
baseDirectory: dist/townofwiley-app/browser
files:
- "**/*"
cache:
paths:
- node_modules/**/*Current public domain details:
- Public hostnames:
townofwiley.govandwww.townofwiley.gov - Known-good fallback hostname:
https://main.d331voxr1fhoir.amplifyapp.com - Amplify status at last check:
AVAILABLE - Current Amplify branch mapping:
- apex (
townofwiley.gov) ->main www->main
- apex (
- Current Amplify target:
townofwiley.gov->d3fmdu29qcwosh.cloudfront.netwww.townofwiley.gov->d3fmdu29qcwosh.cloudfront.net
- Amplify verification CNAME:
- Name:
_f4cd947025ff4f4f7e1f4fb150940ac9.townofwiley.gov - Target:
_377aa211e662dc086d0721e3a52067df.jkddzztszm.acm-validations.aws
- Name:
This is a client-side routed Angular SPA. Without a catch-all rule, deep links like https://townofwiley.gov/weather return a 403 or 404 from CloudFront on hard refresh.
The postbuild script (scripts/generate-static-route-entrypoints.mjs) copies index.html into each route subdirectory and into dist/.../404.html, so the app side is handled. The hosting layer also needs to be configured once in the Amplify Console:
- Open Amplify Console → App → Hosting → Rewrites and redirects.
- Add a rule: Source
</>— Target/index.html— Type200 (Rewrite). - Save and redeploy.
If the distribution is custom-managed in CloudFront directly, add an Error pages rule: HTTP 403 → /404.html (response code 200), and the same for HTTP 404.
Important custom-domain note for future maintainers:
- Route 53 is the authoritative DNS provider for
townofwiley.gov. - Both
townofwiley.govandwww.townofwiley.govare attached to Amplify and serve the live site. - The Route 53 hosted zone now includes an apex alias
Arecord that points at the Amplify CloudFront target. - Keep the apex hosted through Route 53 alias records; do not replace it with a zone-apex
CNAMEor a random historical CloudFront hostname from a failed setup. - If the domain is ever rebuilt, update the Amplify domain association first, then verify the Route 53 apex alias and
wwwrecord together.
- Hosted zone:
townofwiley.gov - Hosted zone ID:
Z088746831TMIL67NZ0VF - Authoritative nameservers:
ns-360.awsdns-45.comns-1383.awsdns-44.orgns-1718.awsdns-22.co.ukns-530.awsdns-02.net
Route 53 DNS records expected during the current recovery path:
townofwiley.govAalias ->d3fmdu29qcwosh.cloudfront.netwww.townofwiley.govCNAME->d3fmdu29qcwosh.cloudfront.net_f4cd947025ff4f4f7e1f4fb150940ac9.townofwiley.govCNAME->_377aa211e662dc086d0721e3a52067df.jkddzztszm.acm-validations.aws- Route 53 may also create or later require an apex
AAAAalias if IPv6 is enabled for the attached CloudFront target.
Current DNS note:
- Public resolvers can disagree for a while after a nameserver migration. If a resolver still shows the old Cloudflare nameservers, wait for cache expiry and re-check against the four Route 53 nameservers above.
- The AWS-side source of truth is the Route 53 hosted zone plus the Amplify domain association for
townofwiley.gov. - Never use a
CNAMEat the zone apex. - When debugging apex outages, verify these in order:
aws amplify get-domain-association --app-id d331voxr1fhoir --domain-name townofwiley.govaws route53 list-resource-record-sets --hosted-zone-id Z088746831TMIL67NZ0VFResolve-DnsName -Name townofwiley.gov -Type A -Server ns-360.awsdns-45.com
Verification check completed after creating the ACM CNAME:
nslookup -type=CNAME _f4cd947025ff4f4f7e1f4fb150940ac9.townofwiley.gov
_f4cd947025ff4f4f7e1f4fb150940ac9.townofwiley.gov canonical name =
_377aa211e662dc086d0721e3a52067df.jkddzztszm.acm-validations.aws
The blocked Invoke-RestMethod calls came from the Copilot terminal execution policy in this environment, not from any workspace file in this repository. There are no repo-level Copilot customization or hook files present here to change that behavior.
This repository now includes a repo-local user secrets workflow that keeps plaintext credentials out of git while still making encrypted secrets portable across machines.
Tracked files:
secrets/encrypted/user-secrets.lockbox.jsonstores encrypted secrets that can be committed and synced.secrets/templates/user-secrets.template.jsondocuments the supported secret structure and non-secret metadata.scripts/user-secrets.mjsprovides the lock, unlock, status, and environment import commands.
Gitignored shield:
secrets/local/user-secrets.jsonis the editable plaintext file.secrets/local/.passphrasestores the local encryption passphrase if you do not want to pass it in an environment variable.- The
secrets/localfolder is protected by gitignore so plaintext never enters the repository history.
Commands:
npm run secrets:init
npm run secrets:init:local-passphrase
npm run secrets:status
npm run secrets:unlock
npm run secrets:lock
npm run secrets:lock:prune
npm run secrets:prune-local
npm run secrets:import-envCross-machine usage:
- Pull the repository so the encrypted lockbox is present.
- Provide the same passphrase on the new machine through
TOW_SECRETS_PASSPHRASEor, if you accept the local-at-rest tradeoff,secrets/local/.passphrase. - Run
npm run secrets:unlockto hydrate the local gitignored plaintext file.
Practical workflow:
npm run secrets:init- Set
TOW_SECRETS_PASSPHRASEor, for convenience on one machine, runnpm run secrets:init:local-passphrase. npm run secrets:unlock- Edit or import secrets locally.
npm run secrets:lock:pruneto update the tracked ciphertext and remove local plaintext afterward.
Current security hardening:
package.jsonnow overridesundicito^7.24.5so the dependency tree does not stay pinned to the vulnerable7.22.0version pulled in by@angular/build. This override should be re-evaluated after each Angular major upgrade — runnpm auditto check whether the upstream package has resolved the issue so the override can be removed.
public/runtime-config.js is generated at build time by scripts/generate-runtime-config.mjs and is listed in .gitignore. It must never be committed to the repository because it contains live endpoint URLs pulled from secrets.
Required Amplify environment variables (set in Amplify Console → App settings → Environment variables for the main branch):
| Variable | Purpose |
|---|---|
APPSYNC_CMS_ENDPOINT |
AppSync GraphQL endpoint URL |
APPSYNC_CMS_API_KEY |
AppSync public-read API key |
APPSYNC_CMS_REGION |
AWS region (e.g. us-east-2) |
EASYPEASY_CHAT_URL |
Easy-Peasy bot embed URL |
SEVERE_WEATHER_SIGNUP_API_ENDPOINT |
Lambda Function URL for alert signup |
SEVERE_WEATHER_SIGNUP_ENABLED |
true / false |
LOG_ENDPOINT |
Frontend log ingest endpoint |
CONTACT_UPDATE_API_ENDPOINT |
Lambda Function URL for contact updates |
If a variable is missing, generate-runtime-config.mjs silently falls back to an empty string; the feature that depends on it will degrade gracefully rather than break the build.
The site can now load the Easy-Peasy chatbot from deployment-time runtime config instead of hardcoding a bot URL into the Angular app shell.
How it works:
public/runtime-config.jsstores the browser-safe chatbot settings.public/easy-peasy-loader.jsinjects the Easy-Peasy widget only when a chatbot URL is configured.npm startandnpm run buildboth regeneratepublic/runtime-config.jsbefore Angular starts.
Configuration sources:
EASYPEASY_CHAT_URL- Optional
EASYPEASY_BUTTON_POSITION secrets/local/user-secrets.json -> chatbot.easyPeasy.chatUrl
Amplify setup:
- Create the bot in Easy-Peasy and copy the bot URL.
- Add
EASYPEASY_CHAT_URLas an Amplify environment variable for themainbranch. - Redeploy. If the value is present, the widget loads automatically on every page.
If no chatbot URL is configured, the site renders normally and no Easy-Peasy script is injected.
Homepage publishing now relies on Amplify Studio and AppSync. The old browser-local CMS workflow has been disabled.
Plain-language source of truth for staff:
- Daily editing path: Amplify Studio Data Manager
- Clerk handoff page:
/clerk-setup /adminroute: read-only guide and status page only- Non-technical instructions:
CLERK-CMS-GUIDE.md
The /admin page now includes a direct Amplify Studio Data Manager link plus a quick-reference copy of the clerk instructions, so staff can jump straight into CMS editing without navigating the AWS dashboard first.
The repo now includes a scheduled site monitor that emails bigessfour@gmail.com when the public site or CMS API stops behaving normally.
What it checks:
https://townofwiley.gov/https://townofwiley.gov/weatherhttps://townofwiley.gov/noticeshttps://townofwiley.gov/meetingshttps://townofwiley.gov/serviceshttps://townofwiley.gov/recordshttps://townofwiley.gov/businesseshttps://townofwiley.gov/newshttps://townofwiley.gov/contacthttps://townofwiley.gov/accessibilityhttps://townofwiley.gov/documentshttps://townofwiley.gov/adminhttps://townofwiley.gov/clerk-setup- the AppSync CMS endpoint from
src/amplifyconfiguration.json
Deployment and test scripts:
npm run test:infra:monitor
npm run deploy:site-monitorOperational logging note:
- CloudFront access logs are useful for edge-level request patterns, scanner traffic, and status-code spikes, but they do not prove that the correct resident-facing page content rendered.
- The frontend logger reads
LOG_ENDPOINTintopublic/runtime-config.js, but that value must point to a dedicated log-ingest service. Do not point it at the severe-weather signup API unless that backend explicitly implements a/logroute. - The site monitor is the primary route-level guardrail for catching real public-page regressions.
The public site now defaults to English and exposes a runtime language switch so residents can move between Spanish and English without a rebuild.
Current implementation notes:
- The selected language is persisted in browser storage under
tow-site-language. - The public shell, weather panel chrome, AI assistant chrome, and
/adminoperations route all switch languages at runtime. - Homepage CMS content is localized in the frontend with bundled Spanish fallbacks and known-text mappings layered over the current single-language AppSync models.
- If Amplify Studio content changes to brand-new English text that is not yet covered by the translation map, that field will fall back to English until the mapping is updated or bilingual CMS fields are introduced.
Plain-language staff guide:
- See CLERK-CMS-GUIDE.md for the current staff workflow.
Current scope:
- Publishing surface: Amplify Studio Data Manager
- Public read path: AppSync GraphQL API with a runtime-injected read key
- Homepage and operations models in use:
SiteSettings,AlertBanner,Announcement,Event,OfficialContact,EmailAlias /adminroute: read-only operations page that points maintainers to Amplify Studio
Runtime configuration sources for the public CMS read path:
APPSYNC_CMS_REGIONAPPSYNC_CMS_ENDPOINTAPPSYNC_CMS_API_KEYsecrets/local/user-secrets.json -> cms.appSync
Operational notes:
- Homepage content should be changed in Amplify Studio, not in the browser.
- The site falls back to bundled homepage content if AppSync runtime config is missing or the CMS request fails.
- The repo secrets workflow now carries the AppSync endpoint and public read key in the encrypted lockbox for future maintainers.
The public site now includes a resident-facing document hub at /documents.
Current implementation status:
- The homepage records center, transparency actions, selected search results, and meeting-related calls to action now route residents into stable public document destinations instead of generic section anchors.
- The
/documentspage is organized into four resident-facing destinations:- records requests
- meeting documents
- financial documents
- code references
- The
/documentspage now includes a first-pass downloadable archive with stable public files under/documents/archive. - Archive publishing is now driven by a central manifest in
src/app/document-hub/document-archive.tsplus static public files underpublic/documents/archive. - Maintainers now have a repo guide for the publishing workflow in
docs/town-document-publishing-guide.md. - This is still not a CMS-managed document library yet. Official packets, budgets, ordinances, and reports still need to be posted through the new workflow as those files become available.
Traceability:
src/app/document-hub/document-links.tssrc/app/document-hub/document-archive.tssrc/app/document-hub/document-hub.tssrc/app/records-center/records-center.tsdocs/town-document-publishing-guide.mdsrc/app/app.tsdocs/incomplete-items-reference.md
The Town's preferred utility payment rollout path is now Paystar because it best fits the current RVS Mosaics setup and can be incorporated into the AWS Amplify-hosted site with the least friction.
Current implementation status:
- The public payment card still supports billing-help email as the fallback path.
- A Paystar runtime-config scaffold now exists for the resident-services payment card.
- A small town-managed proxy scaffold now exists so the website can keep a stable launch contract while the live processor configuration is finalized.
- The current scaffold is intentionally hosted-first because Paystar's public utility documentation clearly supports linking an existing website into a hosted payment portal, while vendor-specific secret API details are not published publicly.
Traceability:
src/app/payments/paystar-config.tssrc/app/payments/paystar-connection.tssrc/app/resident-services/resident-services.tsinfrastructure/paystar-proxy/index.mjsdocs/incomplete-items-reference.md
Runtime configuration sources:
PAYSTAR_MODEPAYSTAR_PORTAL_URLPAYSTAR_API_ENDPOINTsecrets/local/user-secrets.json -> payments.paystar
Supported modes:
none: keep the resident-facing payment card on staff-help fallback onlyhosted: open the secure Paystar portal directly from the homepage cardapi: call a town-managed endpoint first, then launch the returned Paystar URL
Recommended near-term deployment path:
- Set
PAYSTAR_MODE=hosted. - Set
PAYSTAR_PORTAL_URLto the Town's live Paystar payment page. - Redeploy Amplify so the homepage payment card exposes the secure Paystar action.
- Keep the proxy scaffold for a later phase if the Town wants a deeper server-side launch or receipt workflow.
Operational note:
- The current proxy scaffold does not attempt a direct private vendor integration. It normalizes the launch contract on the Town side and can later be extended once live Paystar credentials, posting behavior, and any non-public API details are confirmed.
The Town mail-routing path should use AWS-managed forwarding rather than personal mailbox rules so townofwiley.gov addresses stay under Town control even when the staff member's current inbox changes.
Selected AWS method:
- Receive inbound town mail through Amazon SES.
- Store the raw inbound message in S3.
- Trigger a Lambda forwarder from the S3 object-created event.
- Look up the destination inbox from a private Amplify Studio
EmailAliasrecord. - Forward the message to the staff member's current inbox by SES using a verified Town sender.
Why this is the best fit here:
- It supports alias-style forwarding such as
steve.mckitrick@townofwiley.gov -> bigessfour@gmail.comwithout moving staff into a new mailbox system first. - The routing data can be managed in Amplify Studio by adding or updating
EmailAliasrecords. - Public contact cards can stay in
OfficialContact, while forwarding destinations remain private and are never exposed through the public API key. - The Lambda forwarder keeps the logic in AWS, so the Town can later swap destination inboxes without editing Route 53 records or personal Gmail rules.
Important scope note:
- This scaffold is for inbound forwarding first.
- If the Town later wants staff to send mail as
townofwiley.govfrom Gmail or another client, that should be handled separately with SES SMTP or Amazon WorkMail after forwarding is stable. - SES inbound receiving may need to live in an AWS region that supports email receiving even if the rest of the site stays in
us-east-2.
Current live SES status in Ohio:
- The
townofwiley.govdomain identity is verified in Amazon SESus-east-2. - Easy DKIM is active and the Route 53 hosted zone now carries the SES DKIM CNAME records for the domain.
- The SES account in
us-east-2is no longer sandbox-limited. - Current Ohio SES sending quotas are
50,000messages per 24 hours and14messages per second. - The current SES account details in
us-east-2reportMailType=TRANSACTIONALandWebsiteURL=http://townofwiley.gov.
What this means now:
- Outbound Town mail through SES in
us-east-2is available. - The live alias router is now configured to forward mail using
steve.mckitrick@townofwiley.govas the sender. - The remaining mail work is now primarily bucket hardening, rollout of the rest of the alias records, and live end-to-end mail validation.
- The
EmailAliasbackend model is now deployed on the live AppSync API and its current main-environment DynamoDB table isEmailAlias-j7b2x3sh7rcezekekkxxiak7hi-main. - The alias router now supports split-region operation so inbound processing can run in an SES-receiving region such as
us-east-1while forwarded outbound mail continues through the verifiedus-east-2SES sender. - The first-pass alias router infrastructure is now deployed with Lambda
TownOfWileyEmailAliasRouter, IAM roleTownOfWileyEmailAliasRouterRole, S3 buckettownofwiley-email-alias-570912405222-us-east-1, and active SES receipt rule setTownOfWileyAliasForwardinginus-east-1. - Route 53 now publishes
townofwiley.gov MX 10 inbound-smtp.us-east-1.amazonaws.comand the change is fully in sync. - The first live
EmailAliasrecord is active forsteve.mckitrick@townofwiley.gov -> bigessfour@gmail.com. - The current AWS principal could not apply
s3:PutBucketPublicAccessBlock, so that bucket-hardening step still needs to be completed by a principal with that permission.
CMS model split:
OfficialContact: public role, label, detail, and public alias email shown on the websiteEmailAlias: private alias-to-destination mapping used only by the forwarding worker
EmailAlias model fields:
aliasAddressdestinationAddressdisplayNameroleLabelactivenotes
Traceability:
amplify/backend/api/townofwiley/schema.graphqlsrc/app/cms-admin/cms-admin.tssrc/app/cms-admin/cms-admin.htmlinfrastructure/email-alias-router/app.pyinfrastructure/email-alias-router/tests/test_app.pyscripts/deploy-email-alias-router.pydocs/town-email-alias-forwarding-runbook.mddocs/incomplete-items-reference.md
Recommended deployment shape:
- Apply S3 public-access-block settings on
townofwiley-email-alias-570912405222-us-east-1with a principal that hass3:PutBucketPublicAccessBlock. - Add the remaining
EmailAliasrecords in Amplify Studio for each Town mailbox alias. - Send live test mail to each alias before staff relies on it.
Repo-backed deployment path:
- Fill in the
mail.aliasForwardingsection insecrets/local/user-secrets.json. - Run
npm run deploy:email-alias-router. - Follow the operator steps in docs/town-email-alias-forwarding-runbook.md.
Required Lambda environment variables:
EMAIL_ALIAS_TABLE- Optional
EMAIL_ALIAS_TABLE_REGIONwhen the EmailAlias table lives outside the Lambda region - Optional
EMAIL_ALIAS_INDEX_NAMEwith defaultbyAliasAddress FORWARDER_FROM- Optional
ALIAS_DOMAINwith defaulttownofwiley.gov
Current first live alias:
- Public alias:
steve.mckitrick@townofwiley.gov - Current destination inbox:
bigessfour@gmail.com
Validation command:
npm run test:infra:mailThe homepage weather panel now supports two modes:
- Direct browser requests to
api.weather.govfor local development and simple fallback behavior. - A Town of Wiley AWS proxy endpoint for production, which is the preferred path because NWS expects a meaningful
User-Agentheader that browsers cannot set.
Runtime configuration sources:
NWS_PROXY_ENDPOINT- Optional
NWS_ALLOW_BROWSER_FALLBACK secrets/local/user-secrets.json -> weather.nws.apiEndpointsecrets/local/user-secrets.json -> weather.nws.allowBrowserFallback
Maintainer reference values for this site:
- Town: Wiley, Colorado
- ZIP code:
81092 - Primary display location used in the UI:
Wiley, CO - Point lookup used by the frontend and proxy:
38.154,-102.72 - Forecast page link used by the UI:
https://forecast.weather.gov/MapClick.php?lat=38.155356&lon=-102.719248 - Forecast zone used for alert filtering:
COZ098 - Zone label from NWS:
Lamar Vicinity / Prowers County - Current design intent: treat
COZ098as the practical Wiley service area, which covers Wiley plus the surrounding area well beyond a 20-mile radius
Why COZ098 matters:
- The severe-weather logic does not try to calculate a literal radius.
- The NWS zone is the official alert boundary used by the site.
- If alerts ever look wrong, verify the zone first before changing code.
- Current alert endpoint pattern:
https://api.weather.gov/alerts/active?zone=COZ098
Core operational files:
- Frontend weather component: src/app/weather-panel/weather-panel.ts
- Frontend weather template: src/app/weather-panel/weather-panel.html
- AWS weather proxy handler: infrastructure/nws-weather-proxy/index.mjs
- Runtime config generator: scripts/generate-runtime-config.mjs
- Local/encrypted secrets workflow: scripts/user-secrets.mjs
Required runtime settings:
- Amplify environment variable:
NWS_PROXY_ENDPOINT - Optional Amplify environment variable:
NWS_ALLOW_BROWSER_FALLBACK - Lambda environment variable:
NWS_USER_AGENT - Optional Lambda environment variable:
NWS_API_KEY
Current resident-facing weather UI behavior:
- The weather panel shows the forecast, active alerts, and a severe-weather signup form when
weather.alertSignup.enabledandweather.alertSignup.apiEndpointare present in runtime config. - The signup form posts to
POST /subscriptionson the severe-weather backend and asks residents to confirm before alerts begin. - The resident-facing signup is currently limited to ZIP code
81092because the backend enforces that service area. - The checked-in runtime config currently enables this signup form and points it at the live severe-weather backend, so if the form disappears in production the first thing to verify is whether
public/runtime-config.jswas regenerated with the expected alert-signup block during the build. - The live severe-weather backend sender is now
alerts@townofwiley.gov, which is allowed through the verifiedtownofwiley.govSES domain identity inus-east-2. - Email confirmations are working through SES, but SMS confirmations are still blocked because Amazon SNS SMS in
us-east-2reportsIsInSandbox: truefor this account. - SES and SNS SMS are separate AWS delivery systems. SES production access does not remove the SNS SMS sandbox.
Recommended NWS_USER_AGENT format:
TownOfWileyWeather/1.0 (contact: bigessfour@gmail.com)
AWS account and hosting identifiers that future maintainers will need:
- AWS account ID:
570912405222 - AWS region:
us-east-2 - Amplify app ID:
d331voxr1fhoir - Amplify app name:
Townofwiley - Production branch:
main - Static build output:
dist/townofwiley-app/browser
Expected runtime behavior:
- If
NWS_PROXY_ENDPOINTis set, the weather panel uses the AWS proxy. - If the proxy fails and browser fallback is enabled, the site retries against public
api.weather.gov. - If the proxy fails and browser fallback is disabled, the site shows an error state and links residents to the full forecast page.
Common failure points and what to verify:
- No weather data appears at all:
Check that
public/runtime-config.jscontains the expectedweatherblock after build or deploy. - Proxy returns errors:
Check that the Lambda has
NWS_USER_AGENTset and that the string still includes a valid maintainer contact. - Alerts look too broad or too narrow:
Reconfirm that Wiley is still being mapped to
COZ098and that NWS has not changed the point-to-zone mapping. - Browser works locally but production fails:
Verify
NWS_PROXY_ENDPOINTin Amplify and confirm the deployed proxy URL still responds with JSON. - Tests fail only on mobile: Check the Playwright smoke suite first; mobile interactions are covered there specifically for chat and weather refresh.
Useful manual verification URLs:
- Point metadata:
https://api.weather.gov/points/38.154,-102.72 - Active alerts for Wiley area:
https://api.weather.gov/alerts/active?zone=COZ098 - Public forecast page:
https://forecast.weather.gov/MapClick.php?lat=38.155356&lon=-102.719248 - National forecast maps:
https://www.weather.gov/forecastmaps
Production recommendation:
- Deploy
infrastructure/nws-weather-proxy/index.mjsas an AWS Lambda-backed HTTP endpoint. - Set
NWS_USER_AGENTon that function. - Set
NWS_PROXY_ENDPOINTin Amplify so the Angular app uses the AWS proxy instead of direct browser requests. - Leave browser fallback enabled only if you want a safety net during rollout.
The repository now includes a Python AWS backend for resident severe weather signups, confirmation links, unsubscribe handling, and scheduled NWS alert fanout for Wiley service area residents.
Core backend files:
- Signup Lambda handler: infrastructure/severe-weather-signup/app.py
- Lambda entrypoint shim: infrastructure/severe-weather-signup/index.py
- Backend tests: infrastructure/severe-weather-signup/tests/test_app.py
- Deployment script: scripts/deploy-severe-weather-backend.py
- Frontend signup form logic: src/app/weather-panel/weather-panel.ts
- Frontend signup form template: src/app/weather-panel/weather-panel.html
Service contract:
- Allowed resident ZIP code:
81092 - NWS alert zone:
COZ098 - Supported notification channels:
emailandsms - Supported alert languages:
enandes - Public routes:
POST /subscriptionsGET /confirmGET /unsubscribeGET /health
- Scheduled route source: EventBridge
rate(5 minutes)by default
Required AWS resources created by the deploy script:
- Lambda function running on
python3.13 - Lambda Function URL with public unauthenticated access
- DynamoDB subscriptions table
- DynamoDB delivery deduplication table
- EventBridge schedule for repeated alert polling
- IAM role with Lambda basic execution, DynamoDB access, SNS publish, SES send permissions, and Amazon Translate
TranslateText
Live backend identifiers at last successful deployment:
- Lambda function name:
TownOfWileySevereWeatherBackend - Lambda role:
arn:aws:iam::570912405222:role/TownOfWileySevereWeatherRole - Public Function URL:
https://myqlw4fgzf5hwnes5ki2msye2m0bbbue.lambda-url.us-east-2.on.aws - Subscriptions table:
TownOfWileySevereWeatherSubscriptions - Deliveries table:
TownOfWileySevereWeatherDeliveries - EventBridge rule name:
TownOfWileySevereWeatherPoller - Current sender email:
bigessfour@gmail.com - Current notification sender name:
Town of Wiley Alerts - Current NWS user agent:
TownOfWileyWeather/1.0 (contact: bigessfour@gmail.com) - Current allowed ZIP code:
81092 - Current alert zone:
COZ098
Amplify branch settings related to alert signup at last successful deployment:
SEVERE_WEATHER_SIGNUP_API_ENDPOINT=https://myqlw4fgzf5hwnes5ki2msye2m0bbbue.lambda-url.us-east-2.on.awsSEVERE_WEATHER_SIGNUP_ENABLED=true
Operational warning for future maintainers:
- If email confirmations suddenly stop working, verify the SES identity status for
bigessfour@gmail.cominus-east-2first. - If the Function URL starts returning
403, check both Lambda resource-policy statements for Function URL access before changing app code. - The current IAM user still lacks
events:DescribeRule, so deployment verification from this workspace may not be able to read back the EventBridge rule even when the scheduler itself already exists.
Required runtime and secret settings:
- Lambda environment variables:
SUBSCRIPTIONS_TABLEDELIVERIES_TABLESENDER_EMAILNOTIFICATION_SENDER_NAMEALLOWED_ZIP_CODEALERT_ZONE_CODEPUBLIC_API_BASE_URLNWS_USER_AGENT- Optional
NWS_API_KEY
- Amplify branch environment variables:
SEVERE_WEATHER_SIGNUP_API_ENDPOINTSEVERE_WEATHER_SIGNUP_ENABLED
- Repo-local secrets support:
weather.alertSignup.enabledweather.alertSignup.apiEndpointweather.alertSignup.senderEmail
Deployment flow:
- Unlock or import repo-local secrets so AWS credentials, Amplify app ID, and NWS sender values are available.
- Ensure the sender address you plan to use in
SENDER_EMAILis verified in SES forus-east-2. - Run
npm run deploy:severe-weather-backend. - The script packages the Python backend, creates or updates the Lambda function, creates the Function URL, provisions DynamoDB tables, configures the EventBridge poller, updates the Amplify branch environment, and starts an Amplify release unless skipped.
Optional deployment flags:
python scripts/deploy-severe-weather-backend.py --skip-amplify-release
python scripts/deploy-severe-weather-backend.py --sender-email alerts@townofwiley.gov
python scripts/deploy-severe-weather-backend.py --branch-name mainOperational notes:
- The deploy script reads AWS credentials and default metadata from
secrets/local/user-secrets.jsonwhen environment variables are not already set. - The weather signup form now lets residents choose English or Spanish alerts. The backend stores that preference and uses Amazon Translate
translate_textfor Spanish confirmation and alert delivery, while preserving confirmation and unsubscribe URLs. - The script updates Amplify
mainbranch environment values to keep the Angular runtime config aligned with the live backend URL. - Email confirmation and alert delivery will remain blocked until the configured SES sender identity is verified.
- SMS sending uses SNS directly, so destination-country and spend-limit policies still apply in the AWS account.
The weather integration is now covered at three layers:
- Angular browser unit tests for direct NWS, proxy mode, and proxy fallback.
- Node-level proxy tests for the AWS handler.
- Playwright smoke coverage for homepage weather rendering, severe-weather signup, and refresh behavior.
Commands:
npm run test:unit:browser
npm run test:infra
npm run test:infra:alerts
npm run test:e2e:smoke
npm run test:regressionMobile-specific regression coverage now checks:
- Programmatic chat submission on the mobile homepage
- Chat fallback handling when the proxy returns malformed data
- Weather refresh behavior on the mobile homepage without a full page reload
This repository now includes a tracked Git pre-push hook at .githooks/pre-push.
Behavior:
- Runs Trunk formatting across tracked repository files before every push.
- Allows the push to continue only if Trunk leaves the tracked file set unchanged.
- Aborts the push if formatting changed any tracked file so the formatted result can be reviewed and committed first.
One-time setup for each clone:
git config core.hooksPath .githooksManual verification:
trunk fmt --allOperational note:
- The hook requires the Trunk CLI to be installed and on
PATH. - The current repo-local Trunk configuration lives in .trunk/trunk.yaml.