A personal mobile one-pager on which the teacher grades the lunchbox of the day; the server forwards every report card to a Discord channel.
Requires Node.js 26 (see .nvmrc).
npm ci
npm startOpen http://localhost:4200.
Warning:
npm startruns the real API. WithDISCORD_WEBHOOK_URL=<webhook-url> npm start, submitting a report card in development posts to that Discord channel; without the variable the page works, and every submission fails with 502.
TEACHER_NAME=<name> greets the teacher by name and signs the sent report card with it; without it the page uses neutral copy.
npm test # watch mode
npm test -- --watch=false # single run, as in CInpm run build
NG_ALLOWED_HOSTS=localhost DISCORD_WEBHOOK_URL=<webhook-url> node dist/lunchbox-feedback/server/server.mjsThe server listens on PORT or 4000. The build is self-contained: dist/lunchbox-feedback/ needs no node_modules. Angular SSR answers 400 for every host missing from the comma-separated NG_ALLOWED_HOSTS; security.allowedHosts in angular.json stays empty, so the production domain stays out of the repository.
- A push to
mainruns.github/workflows/deploy.yml: it tests, builds and replaces the content of the branchdeployment/productionwith the build output plushosting/app.cjs, the Passenger startup file. - Plesk pulls
deployment/productionintohttpdocs, triggered by a GitHub push webhook. - The pull rewrites
tmp/restart.txt, so Passenger restarts the app on the next request. - The workflow polls
/build-id.txtuntil it serves the new build, then purges the Cloudflare cache.
An unchanged build produces no commit and triggers nothing.
-
Add the repository secrets
PRODUCTION_URL(the production URL without a trailing slash, for examplehttps://example.com),CF_ZONE_ID(the zone ID of the domain) andCF_API_TOKEN(an API token with the permission Zone → Cache Purge for this zone). The GitHub App credentials come from the BoundfoxStudios organization. -
Run the workflow once (push to
mainorgh workflow run Deploy) so thatdeployment/productionexists. The purge step of this first run fails after its poll timeout because the origin serves nothing yet. -
In Plesk, delete the default files in
httpdocsand create the empty folderhttpdocs/browserin the File Manager. -
Configure Node.js for the domain and decline Plesk's framework auto-configuration:
Setting Value Node.js version 26.10.0 Application Root /httpdocsDocument Root /httpdocs/browserApplication Startup File app.cjsApplication Mode productionCustom environment variables DISCORD_WEBHOOK_URL(the webhook URL as Discord copies it),NG_ALLOWED_HOSTS(the production domain, for exampleexample.com),TEACHER_NAME(the teacher's first name for the greeting and the signature)NPM install none, the build ships no package.json -
Open Git for the domain and add the repository with the deployment path
/httpdocsand automatic deployment. Use the repository's HTTPS URL; it is public, so no deploy key is needed. The dialog has no branch field, so switch todeployment/productionafterwards via Change branch and path. -
Add the webhook URL Plesk shows as a push webhook in the GitHub repository settings, then pull the updates once.
-
Restart the app once.
After the deployment, <production-url>/build-id.txt must return the content of browser/build-id.txt on deployment/production. The page itself answers 400 if NG_ALLOWED_HOSTS does not list the domain. Then submit one report card: it must arrive in the Discord channel. If the page shows the error state instead, check the names and values of the environment variables first: the app reads DISCORD_WEBHOOK_URL only on submission, so a misspelled name breaks nothing but submissions. Plesk does not show the app's console output, so the logs there give no cause.