diff --git a/Dockerfile b/Dockerfile index 5bd3ff9..aee97ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,8 +36,12 @@ COPY --chown=app:app backend/migrations/ ./migrations/ COPY docker/nginx.conf /etc/nginx/nginx.conf COPY docker/entrypoint.sh /usr/local/bin/go-guess-entrypoint +# One user, and it owns everything the runtime writes to. A second USER line here +# silently replaced this one with a uid that owns neither /app nor /tmp/nginx, so +# nginx could not create its pid file or its temp paths and the container failed to +# start. nginx also ignores its own user directive when the master is not root, so +# its workers run as whoever this names. USER app -USER 1000 EXPOSE 8080 ENV ADDRESS=:8081 \ APP_ENV=production \ diff --git a/README.md b/README.md index 120c459..7c10959 100644 --- a/README.md +++ b/README.md @@ -212,6 +212,21 @@ The `Dockerfile` at the repository root still builds one combined image that serves both halves on port `8080`. It is convenient for local runs and browser BDD tests, while Kubernetes deploys the two images separately. +That combined image runs as the `app` user, which owns everything the runtime +writes to. There is deliberately only one `USER` line: a second one silently +replaced it with a uid owning neither `/app` nor `/tmp/nginx`, and nginx could +then not create its pid file or its temp paths, so the container failed to start. +nginx ignores its own `user` directive when the master is not root, so its +workers run as whoever this names. + +`THEME_BASE_URL` points the page at the [go-bananas](https://github.com/urpi/go-bananas) +design-token service, which supplies the palette and the light/dark switch at run +time. It is optional and empty by default, so an image without a theme service +renders exactly as before. The entrypoint writes it into `runtime-config.js` on +every start, so one image serves every environment. See +[the front-end README](frontend/README.md#theming) for the deployment +requirements. + Pushes to `master` publish `go-guess:latest` and `go-guess:` to the private registry configured in `.github/workflows/publish-image.yml`. The repository must provide `REGISTRY_USERNAME` and `REGISTRY_PASSWORD` secrets. diff --git a/deploy/helm/go-guess/templates/api-deployment.yaml b/deploy/helm/go-guess/templates/api-deployment.yaml index 25623f7..eac4ee4 100644 --- a/deploy/helm/go-guess/templates/api-deployment.yaml +++ b/deploy/helm/go-guess/templates/api-deployment.yaml @@ -46,6 +46,13 @@ spec: value: {{ .Values.api.environment | quote }} - name: FRONTEND_URL value: {{ include "go-guess.frontendURL" . | quote }} + {{- with .Values.api.themeBaseUrl }} + # The theme service is read by the browser, not the API, so it is passed + # through to the entrypoint, which writes the runtime config the page + # reads. Empty means this deployment keeps its own styling. + - name: THEME_BASE_URL + value: {{ . | quote }} + {{- end }} - name: TOKEN_TTL value: {{ .Values.api.tokenTTL | quote }} - name: MAX_UPLOAD_MB diff --git a/deploy/helm/go-guess/values.yaml b/deploy/helm/go-guess/values.yaml index 7e40442..fdee21d 100644 --- a/deploy/helm/go-guess/values.yaml +++ b/deploy/helm/go-guess/values.yaml @@ -32,6 +32,12 @@ api: # Public base URL used when building invitation and meeting links. Defaults to # https://. when empty. frontendURL: "" + # Origin of the go-bananas runtime supplying the shared styling contract, for + # example https://themes.urpi.be. Empty keeps this application's own styles, so + # a deployment without a theme service is unaffected. The service must also be + # configured to allow this application's origin, or the browser will refuse to + # load the controls from it. + themeBaseUrl: "" tokenTTL: 8h maxUploadMB: 10 service: diff --git a/docker-compose.yml b/docker-compose.yml index 0db08dd..4450bea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,8 @@ services: DATABASE_URL: postgres://go_guess:go_guess@db:5432/go_guess?sslmode=disable JWT_SECRET: local-development-secret-change-me-12345 FRONTEND_URL: ${FRONTEND_URL:-https://nmbs.guess-dev.urpi.be} + # Where the go-bananas styling contract is served from. Empty disables it. + THEME_BASE_URL: ${THEME_BASE_URL:-} APP_ENV: ${APP_ENV:-development} GO_LOOSE_AUTH_DOMAIN: ${GO_LOOSE_AUTH_DOMAIN:-auth-dev.urpi.be} GO_LOOSE_APP_DOMAIN: ${GO_LOOSE_APP_DOMAIN:-guess-dev.urpi.be} diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 64acaab..7eacb8e 100755 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -13,6 +13,13 @@ seed mkdir -p /tmp/nginx +# The theme service location is environment, not build time, so one image serves +# every environment. The file is regenerated on every start and is read by the +# page before the bundle runs. +cat > /app/frontend/runtime-config.js <` + and imports `$THEME_BASE_URL/rt/v1/components.js`. Both are best effort: a + theme service that is absent, slow, or unreachable must not stop the + application working. +- `src/theme/ThemeControls.tsx` renders the service's own custom elements, + `` and ``. They are custom + elements rather than components written here because the applications this + service feeds are a mix of React, vanilla JavaScript, and server-rendered HTML, + and a custom element is the one shape all three can use with no build step, no + import map, and no second copy of React. React treats an unknown element as an + inert host node, so rendering them unconditionally is safe. +- Every token in `src/styles/global.css` is written as + `var(--bn-*, fallback)`. That ordering is the whole safety property: with no + theme service configured the page renders exactly as it did before, and with + one configured the same rules follow the palette. + +`THEME_BASE_URL` is read from `runtime-config.js`, which the container entrypoint +rewrites from the environment on start, so one image serves every environment. +`VITE_THEME_BASE_URL` is the equivalent for `npm run dev`. + +Two deployment requirements are easy to miss. The service must be configured to +allow this application's origin (`CORS_ALLOWED_ORIGINS` on go-bananas), or the +browser refuses to load the controls from it, because a module script is always +fetched with CORS. And `api.themeBaseUrl` in the Helm values must be set; it is +empty by default, which disables theming. + +`src/theme/contract.test.ts` fetches the live contract and asserts it defines +every `--bn-*` variable this application consumes. A token renamed on the service +would otherwise leave every `var()` resolving to its fallback, and the page would +look correct while never actually following the palette. Invitation and participant-meeting URLs use the validated request origin so links stay on the tenant hostname. Copy actions fall back to a temporary selected textarea when the Clipboard API is unavailable on local HTTP domains. diff --git a/frontend/index.html b/frontend/index.html index ad5434b..c69cac6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,6 +6,9 @@ Go Guess ยท Recruitment workspace + +
diff --git a/frontend/public/runtime-config.js b/frontend/public/runtime-config.js new file mode 100644 index 0000000..9ec8e58 --- /dev/null +++ b/frontend/public/runtime-config.js @@ -0,0 +1,4 @@ +/* Rewritten by the container entrypoint from THEME_BASE_URL on start, so one + * image serves every environment. The committed value disables theming, which is + * what a checkout with no theme service should do. */ +window.__GO_GUESS_CONFIG__ = { themeBaseUrl: '' } diff --git a/frontend/src/components/layout/WorkspaceUtilities.tsx b/frontend/src/components/layout/WorkspaceUtilities.tsx index 8d66030..92dfe2c 100644 --- a/frontend/src/components/layout/WorkspaceUtilities.tsx +++ b/frontend/src/components/layout/WorkspaceUtilities.tsx @@ -1,6 +1,7 @@ import { CalendarDays, Mail } from 'lucide-react' import { NavLink } from 'react-router-dom' import type { User } from '../../types' +import { ThemeControls } from '../../theme' import { UserMenu } from './UserMenu' export function WorkspaceUtilities({ @@ -20,6 +21,7 @@ export function WorkspaceUtilities({