Skip to content

About

Portfolio site of Neal Thomas: static HTML on S3 and CloudFront, with a .NET 10 Lambda and DynamoDB API behind the experience page

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

386 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mvcprogrammer.com

The source for mvcprogrammer.com, the portfolio site of Neal Thomas.

Live site  Deploy


mvcprogrammer.com, the home page

The site follows the KISS principle: keep it simple, and add nothing that is not earning its place. It is plain HTML, CSS, and vanilla JavaScript with no framework, no build step, no bundler, no third-party scripts, and a strict Content Security Policy. One page, one stylesheet plus a short palette override, one deferred script, shipped exactly as written. The static files sit in a private S3 bucket behind CloudFront, and three small Lambda functions do the only work that needs a server: the contact form, the resume download, and the Experience API. The same page is also built three more times, with Angular, with React, and with Blazor WebAssembly, as a side-by-side comparison; see Four editions.

What is in this repository

Path What it is
index.html, styles.css, vanilla.css, main.js The site. styles.css is shared by all four editions; vanilla.css overrides its design tokens with the eggshell palette for this edition. The Experience timeline is in the HTML so it reads without JavaScript; when the API answers, the script re-renders it from JSON and reveals a skill filter.
404.html, privacy.html, terms.html The not-found page and the policies required for the toll-free SMS registration.
assets/ Self-hosted fonts (Inter and JetBrains Mono as variable WOFF2 subsets) and images. Icons are an inline SVG sprite in the HTML.
angular/ The Angular edition: an Angular 22 workspace with Vitest tests. See angular/README.md and docs/ANGULAR-EDITION.pdf.
fhir/ The FHIR R4 patient explorer, an Angular 22 app of its own for fhir.mvcprogrammer.com, started from the Angular edition's explorer. See fhir/README.md.
react/ The React edition: a React 19 app built with Vite, with Vitest and Testing Library tests. See react/README.md and docs/REACT-EDITION.pdf.
blazor/ The Blazor edition: a Blazor WebAssembly app on .NET 10 with bUnit tests. See blazor/README.md and docs/BLAZOR-EDITION.pdf.
api/ The Experience API and the FHIR API: C# on .NET 10, run on AWS Lambda (arm64); the first reads a single DynamoDB table, the second is a facade over a public FHIR R4 sandbox. Spec-first with OpenAPI 3.1, tested over the in-process host, deployed with SAM. Beside them the HL7 ETL (api/src/Hl7.Etl), a console that lands HL7 v2 messages in Postgres and bulk-loads them into that sandbox as FHIR R4. See api/README.md and the demo runbook docs/HL7-ETL-DEMO.pdf.
infra/ The contact-form and resume-download Lambdas (Node.js 22 on arm64), the CloudFront Function that redirects www and answers the engagement beacon, and the SES DKIM change batch.
tools/ cf-stats.mjs, which reads the CloudFront access logs and prints visits, unique visitors, referrers, and what the beacon recorded; stats-export.mjs, which writes the same figures as day files for the visitor dashboard; stats-report.mjs, which renders an export run's account as the job summary on its Actions page; the shared parser in lib/ and its tests; serve.mjs, a local preview that forwards /api/* to the functions; fhir-seed.mjs, fhir-e2e.mjs, and fhir-reap.mjs, which put the seeded patient back on the FHIR sandbox, drive the explorer's writes end to end and delete what they created, and remove the demo patients visitors left behind (see FHIR sandbox hygiene); board-assign.mjs, board-backfill.mjs, and burndown.mjs, which assign the owner to groomed board items, fill the board's dates and hours from git, and draw the sprint burndown. See Visitor stats and docs/BURNDOWN.md.
stats/ The private visitor dashboard (React 19 + Vite + Chart.js) that reads the day files tools/stats-export.mjs writes. See stats/README.md and docs/STATS-DASHBOARD.pdf.
docs/ DEPLOY.md, the infrastructure walkthrough; BURNDOWN.md, how the board's dates, hours, and burndown are derived; RETROSPECTIVES.md, one section per sprint; the three edition guidelines and the dashboard guideline; and the screenshots above.
.github/workflows/ ci.yml runs every test suite on each pull request and is the check main requires before a merge; deploy.yml is the deploy, which runs that same workflow first and ships nothing if a suite fails, GitHub Actions authenticated with OIDC, so no AWS keys are stored anywhere; stats.yml is the scheduled visitor-stats export; board.yml assigns the owner to groomed board items, backfills the project board from git each night, and redraws the burndown below.

Every npm workspace (angular/, fhir/, react/, stats/, and infra/lambda/stats-auth/) carries a .npmrc that pins legacy-peer-deps=false. A project-level file wins over a developer's ~/.npmrc, so a lockfile refreshed on any machine resolves the same tree the GitHub Actions runner computes with npm's defaults, and npm ci there accepts it. A new workspace gets the same file.

The .NET projects (api/ and blazor/) share the root .editorconfig, which encodes the formatting and naming rules of the coding standard and says next to each setting which rule it enforces. Directory.Build.props turns on EnforceCodeStyleInBuild, so a violation fails dotnet build, and ci.yml runs dotnet format --verify-no-changes on every pull request. The Angular and React workspaces keep their own .editorconfig files.

How it is served

  • Hosting. A private S3 bucket that only CloudFront can read, through Origin Access Control. CloudFront serves over HTTP/2 and HTTP/3 with an ACM certificate, Route 53 aliases the domain, and a CloudFront Function redirects www to the apex.
  • Security. A response headers policy sets a same-origin Content Security Policy, two-year HSTS, X-Frame-Options: DENY, nosniff, and a locked-down Permissions-Policy. No cookies and no third-party analytics: the only measurement is a first-party beacon (GET /ping) that the CloudFront edge answers with 204 and that leaves nothing but a line in the access logs, which tools/cf-stats.mjs reads. It reports the page load (screen class and color scheme), the sections reached, clicks on the edition and profile links, and the seconds the page was visible; it carries no identifier and stays silent only on local previews.
  • Experience API. /api/experience and /api/skills on the same domain. Responses carry ETags and cache headers so CloudFront answers most requests itself, errors follow RFC 9457 problem details, and the function URL accepts only requests signed by CloudFront. The contract is api/openapi.yaml.
  • Deploys. Every push to main assumes a least-privilege IAM role through OIDC, syncs the bucket in two passes (a week of browser caching for fonts and images, five minutes for pages), and invalidates the CloudFront cache. Third-party actions are pinned to commit hashes.

Four editions

The same page is built four ways from this repository. Every edition shows the same content, loads the same stylesheet, and reads the same Experience API; what differs is how the page reaches the browser.

Edition Built with Source Live
Default HTML, CSS, and vanilla JavaScript. No framework, no build step. repository root mvcprogrammer.com
Angular Angular 22: standalone components, signals, and a reactive form in TypeScript. angular/ angular.mvcprogrammer.com
React React 19 with Vite: function components, hooks, and React Router in TypeScript. react/ react.mvcprogrammer.com
Blazor Blazor WebAssembly on .NET 10: Razor components and C# running in the browser. blazor/ blazor.mvcprogrammer.com

Each framework edition has its own test suite, README, and guideline PDF, its own CloudFront distribution and certificate under the same bucket, and its own job in the deploy workflow. The editions mirror each other: a change to one is made to the others in the same commit, and the stylesheet, fonts, images, icons, and seed data are shared from the root rather than copied by hand. The edition menu in the header and the skill chips in the About section link between the editions, so each one is a click away from the others.

Palettes

Color is the one deliberate difference between the editions. Each layers a small palette file over the shared stylesheet that overrides only the design tokens (--bg, --accent, --hover, and the rest) with light-dark() values, so one declaration covers both color schemes and no rule from styles.css is repeated. Older browsers ignore light-dark() and fall back to the shared JetBrains Rider palette.

Edition Palette file Palette Light Dark
Default vanilla.css Eggshell paper with a terracotta accent and a coral hover; a warm charcoal after dark. #f0ead6 #a64a23 #d4602f #211e18 #e0855c #ffb088
Angular angular/src/angular.css Navy and brass: a cool, faintly blue page with navy buttons and a goldenrod hover; a true navy after dark with muted brass for buttons and links and a paler gold on hover. #f3f5fa #1b365d #b7791f #0f1b33 #e3b04b #f2cd7a
React react/src/react.css The react.dev documentation palette: cool slate surfaces, React blue for buttons and links, the logo cyan on hover. #f6f7f9 #067394 #149eca #23272f #58c4dc #61dafb
Blazor blazor/…/wwwroot/blazor.css .NET violet: the .NET brand purple on faint lavender, a deep violet-black with lilac buttons after dark, the Blazor gradient's pink on hover. #f4f1fb #512bd4 #c2187a #1a1430 #b49cff #ff7ac8

Swatches are page, accent, and hover, in that order. The Angular build emits its file under the bundle name angular so that ng serve does not name it styles.css and shadow the shared file, and the React build pins cssTarget to browsers that support light-dark() so Vite's minifier leaves the function alone.

Default Angular React Blazor
The default edition, home page The Angular edition, home page The React edition, home page The Blazor edition, home page

Work tracking

Work is planned on the public project board: every change is an issue, estimated in story points before it starts, and moved through Backlog, Ready, In progress, In review, and Done; an item leaves Ready only after the owner comments Approved on the issue, and the Definition of Ready and Definition of Done in docs/BURNDOWN.md say what each of the other moves requires. The board's Started, Completed, and Actual hours come from git rather than memory, and the sprint burndown below is drawn from the board by tools/burndown.mjs every night. How the numbers are derived, and what they do and do not say, is in docs/BURNDOWN.md.

Sprint 2 burndown: story points remaining per day against the ideal line

Preview locally

python3 -m http.server 8080
# open http://localhost:8080

The full build-and-deploy walkthrough, including the IAM trust policy, the response headers policy, the contact form, and the resume download, is in docs/DEPLOY.md.


Neal Thomas  ·  GitHub profile  ·  LinkedIn  ·  Résumé and contact

License

Copyright (c) 2026 Neal Thomas.

The source code in this repository is licensed under the GNU Affero General Public License, version 3 (AGPL-3.0-only). See LICENSE for the full text.

About

Portfolio site of Neal Thomas: static HTML on S3 and CloudFront, with a .NET 10 Lambda and DynamoDB API behind the experience page

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages