# EventCentral (v1 prototype)

Implements the contract in "CSC Event PR Service — Upstream Integration Handoff": an
upstream backend `POST`s validated event JSON, this service turns it into a markdown
file in the `www/www-new` layout, pushes a branch, and opens a PR against
`www/www-new`.

This is a **prototype**, not the final service. Several pieces from the handoff doc
are intentionally left unfinished — see "What's deferred" below — because the info
needed to build them for real wasn't available yet. They're stubbed so the rest of
the pipeline is real and testable, not faked.

## Layout

- `backend/` — Express + TypeScript service implementing `GET /health`,
  `POST /api/events/publish`, `GET /api/events/:requestId`, plus a dev-only
  `POST /api/dev-uploads` (see below).
- `frontend/` — Next.js form. There's no real upstream backend yet, so this stands
  in for one: it POSTs directly to `backend`'s `/api/events/publish` so the pipeline
  is exercisable end to end. It is not the actual "upstream backend" from the
  handoff doc.

## Ported from the legacy Eventer app

The legacy app at `www/Eventer` already solved a few things this prototype
initially skipped. They're now ported over on top of the contract-based design:

- **Poster handling.** `posterHandler.ts` downloads `event.poster.sourceUrl`,
  validates content-type (png/jpeg), size (15MB cap), and squareness (±20%
  tolerance) — the same limits the legacy app enforced. `publishEvent.ts` then
  commits the image to `images/events/{year}/{term}/{filename}` alongside the
  markdown file, and sets the `poster:` frontmatter field so it renders in
  `EventDescriptionCard`.
- **Per-submitter allowlist.** `allowlist.ts` + `allowedSubmitters.json` port the
  legacy app's `allowedUsers.json` check: `submittedByEmail`'s WatIAM ID must be on
  the list, or the request is rejected with a message pointing to the web
  committee's Discord (same wording as the legacy `unauthorized.html`). This is
  independent of `UPSTREAM_API_SECRET` — one authenticates the calling service,
  the other authorizes the individual submitting on its behalf.
- **Dev upload endpoint.** The real contract has the *upstream backend* host
  posters and hand this service a URL. Since no real upstream exists yet, the
  frontend needs somewhere to put a file the user picked — `POST /api/dev-uploads`
  (multer, same type/size limits) does that and hands back a `sourceUrl`. This
  endpoint is **not** part of the documented contract; it only exists so the test
  harness frontend can produce one.
- **Client-side checks.** The frontend now has a poster file input, a
  confirm-before-submit dialog, and an explicit end-after-start check — mirroring
  `public/client.js` in the legacy app.

## Running locally

```
cd backend && cp .env.example .env && npm install && npm run compile && npm run server
cd frontend && cp .env.local.example .env.local && npm install && npm run dev
```

`TARGET_GIT_SSH_URL` in `backend/.env` defaults to the real `www/www-new` repo.
Point it at a local/throwaway bare repo instead if you don't want to push real
branches while testing.

## Deploying to the CSC server

This repo deploys the same way `www/Eventer` does: Drone CI (`.drone.yml`) SSHes
into `webcom@caffeine.csclub.uwaterloo.ca` on push to `main` and runs a deploy
script from that account's `~/bin`. Only `backend/` gets deployed — `frontend/`
is a local test harness standing in for a real upstream backend, not a
production service, so it has no place on the server.

`deploy.sh` at the repo root is the source of truth for what that script should
do (clone, build, swap into a live dir, restart under `pm2`) — copy it to
`~/bin/deploy-eventcentral.sh` on the `webcom` account. What's needed beyond
that, all one-time, all requiring `webcom`/Drone-admin access this assistant
doesn't have:

1. `~/bin/deploy-eventcentral.sh` on `webcom@caffeine` — copy of `deploy.sh` here.
2. `/users/webcom/live-deploys/eventcentral-secrets/.env` on that account, using
   `backend/.env.example` as the template. For this first deploy that can just be
   the v1 placeholders (nothing set except `PORT`/`TARGET_GIT_SSH_URL`/`PR_OWNER`/
   `PR_REPONAME`) — same behavior as local testing: branches push for real, PR
   creation and Discord notify log what they'd do instead of calling out.
3. An `SSH_KEY` secret on this repo in Drone, matching the key
   `deploy-eventcentral.sh`'s account trusts.
4. A reverse-proxy vhost pointing at
   `/users/webcom/live-deploys/eventcentral/app.sock` (mirroring however
   `Eventer`'s is configured) — this isn't in either repo, so check with syscom.

Once those are in place, pushing to `main` deploys automatically. Until then,
none of this runs anywhere but locally.

## What's deferred (and why)

- **Upstream service secret.** The handoff doc specifies a shared-secret Bearer
  token (`UPSTREAM_API_SECRET`) authenticating the calling backend. Left unset by
  default: the middleware allows every request through and logs a warning. Set the
  env var once the real secret exists — no code change needed. (The per-submitter
  allowlist is a separate, already-implemented check — see above.)
- **Discord notification.** The handoff doc says one notification gets posted,
  mentioning a fixed role, when a PR is opened — but the specifics (webhook vs. bot,
  message format, which role) weren't available when this was built. `discordNotifier.ts`
  posts a plain webhook message mentioning `DISCORD_ROLE_ID` if
  `DISCORD_NOTIFY_ENABLED=true` and `DISCORD_WEBHOOK_URL` is set; otherwise it logs
  and no-ops. Expect to rewrite this once the real spec is in hand.
- **Gitea PR creation.** Git branch push works for real (it only needs your SSH key).
  Opening the PR via the Gitea API needs `GITEA_API_KEY`, which isn't provisioned
  yet — without it, the service still creates and pushes the branch, then logs the
  PR it would have opened (`status: "pr_skipped_no_credentials"`).
- **Fork-based PR flow.** The handoff doc says branches get pushed to a Gitea fork,
  then a PR is opened against `www/www-new`. `FORK_OWNER` supports this once a bot
  account/fork exists; unset, branches push straight to `PR_OWNER`/`PR_REPONAME`.
- **Job persistence.** Jobs live in an in-memory `Map` (`jobStore.ts`) and are lost
  on restart. Fine for a prototype; needs a real store before this runs unattended.
- **Response/error shapes.** The handoff doc's response bodies and full error catalog
  weren't fully received (it was truncated after the field reference tables). The
  shapes here are a reasonable v1 guess consistent with the parts of the doc we did
  get — revisit once the rest of the doc is available.
