Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7341c51
chore(self-hosting): use traefik proxy, add optional https setup (@fe…
fehmer Aug 14, 2026
3cc1305
chore(deps-dev): bump postcss from 8.5.15 to 8.5.23 (#8320)
dependabot[bot] Aug 14, 2026
048da41
chore(deps-dev): bump postcss from 8.5.23 to 8.5.24 in /frontend (#8319)
dependabot[bot] Aug 14, 2026
edb9fb8
fix: last word with only commit character is invisible (@Leonabcd123)…
Leonabcd123 Aug 14, 2026
8f1998b
impr(regex-words-filter): allow patterns without slashes (@Leonabcd12…
Leonabcd123 Aug 14, 2026
223f8a4
fix(account): don't show leaderboard section if not qualified (@fehme…
fehmer Aug 14, 2026
d2da420
fix: live progress text off center in non-chromium browsers (@shrewdd…
shrewddd Aug 14, 2026
06f52c8
feat(result): option to copy slow words list with filtering (@vrutant…
vrutant08 Aug 14, 2026
96402aa
chore(deps): bump uuid from 14.0.0 to 14.0.1 (#8286)
dependabot[bot] Aug 14, 2026
e9f47f8
fix(keymap): restore handling of shift/capslock (@fehmer) (#8285)
fehmer Aug 14, 2026
c3048ff
chore: regenerate lock file
Miodec Aug 14, 2026
7706bec
chore: update pnpm to 11 (@fehmer) (#8288)
fehmer Aug 14, 2026
594d1bc
fix: keymap mode react not working with prefers-reduced-motion (@fehm…
fehmer Aug 14, 2026
b9e6b7e
fix: handling of empty string in InputField (@fehmer) (#8272)
fehmer Aug 14, 2026
3fe1520
fix(quotes): prevent wrong quote length when moderator edits quote (@…
HmonWutt Aug 14, 2026
041d5dd
fix(friends page): Fixed a loader position which is position next to …
self-yash Aug 14, 2026
685d4ad
chore(deps-dev): bump @vitest/browser from 4.1.8 to 4.1.10 (#8269)
dependabot[bot] Aug 14, 2026
5303ce7
fix(languages): disable lazy mode for chinese simplified (@EnormousMu…
EnormousMush Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/fix-formatting.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Fix formatting

env:
PNPM_VERSION: "10.28.1"
PNPM_VERSION: "11.21.0"
NODE_VERSION: "24.11.0"

permissions:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/monkey-ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Monkey CI

env:
PNPM_VERSION: "10.28.1"
PNPM_VERSION: "11.21.0"
NODE_VERSION: "24.11.0"
RECAPTCHA_SITE_KEY: "6Lc-V8McAAAAAJ7s6LGNe7MBZnRiwbsbiWts87aj"

Expand Down
3 changes: 0 additions & 3 deletions .npmrc

This file was deleted.

2 changes: 1 addition & 1 deletion backend/docker/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ services:
build:
dockerfile_inline: |
FROM node:24.11.0
RUN npm i -g pnpm@10.28.1
RUN npm i -g pnpm@11.21.0
RUN mkdir /pnpm-store && chown -R 1000:1000 /pnpm-store
user: "node" ##this works as long as your local user has uid=1000
restart: on-failure
Expand Down
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"string-similarity": "4.0.4",
"swagger-stats": "0.99.7",
"ua-parser-js": "0.7.33",
"uuid": "14.0.0",
"uuid": "14.0.1",
"winston": "3.6.0",
"zod": "3.23.8"
},
Expand Down
6 changes: 4 additions & 2 deletions backend/src/dal/new-quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,12 @@ export async function approve(
);
}
const language = targetQuote.language;

const approvedText = editQuote ?? targetQuote.text;
const quote: ApproveQuote = {
text: editQuote ?? targetQuote.text,
text: approvedText,
source: editSource ?? targetQuote.source,
length: targetQuote.text.length,
length: approvedText.length,
approvedBy: name,
id: -1,
};
Expand Down
2 changes: 1 addition & 1 deletion docker/backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ COPY packages packages
COPY backend backend

#gimme pnpm + build
RUN npm i -g pnpm@10.28.1 && \
RUN npm i -g pnpm@11.21.0 && \
pnpm i --frozen-lockfile && \
npm run build

Expand Down
106 changes: 92 additions & 14 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,58 @@
version: "3.8"

services:
monkeytype-frontend:
container_name: monkeytype-frontend
image: monkeytype/monkeytype-frontend:latest
ports:
- "${HTTP_PORT:-8080}:80"
monkeytype-proxy:
container_name: monkeytype-proxy
image: traefik:v3.7
restart: on-failure
depends_on:
monkeytype-backend:
condition: service_healthy
monkeytype-frontend:
condition: service_healthy
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
#Dashboard disabled for security (uncomment only for debugging behind VPN)
#- "8080:8080"
networks:
- public
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./letsencrypt:/letsencrypt
command:
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"

# enable for HTTPS
#- "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}"
#- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
#- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
#- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
#- "--entrypoints.web.http.redirections.entrypoint.scheme=https"

# for debugging only
#- "--api.insecure=true"
#- "--log.level=DEBUG"

monkeytype-frontend:
container_name: monkeytype-frontend
image: monkeytype/monkeytype-frontend:latest
#uncomment if you want to expose the frontend server directly (not recommended)
#ports:
# - "${FRONTEND_PORT:-8080}:80"
restart: on-failure
networks:
- public
labels:
- "traefik.http.routers.frontend.rule=PathPrefix(`/`)"
- "traefik.http.routers.frontend.entrypoints=web,websecure"
- "traefik.http.routers.frontend.priority=25"
- "traefik.http.services.frontend.loadbalancer.server.port=80"
# enable for HTTPS
#- "traefik.http.routers.frontend.rule=Host(`${DOMAIN}`)"
#- "traefik.http.routers.frontend.tls=true"
#- "traefik.http.routers.frontend.tls.certresolver=letsencrypt"
#- "traefik.http.routers.frontend.tls.domains[0].main=${DOMAIN}"
env_file:
- path: ./.env
required: true
Expand All @@ -20,20 +63,30 @@ services:
- FIREBASE_STORAGEBUCKET=${FIREBASE_STORAGEBUCKET:-}
- FIREBASE_MESSAGINGSENDERID=${FIREBASE_MESSAGINGSENDERID:-}
- FIREBASE_APPID=${FIREBASE_APPID:-}
- MONKEYTYPE_BACKENDURL=${MONKEYTYPE_BACKENDURL}
- MONKEYTYPE_BACKENDURL=${BASE_URL:-http://localhost}/api
- RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY:-}
healthcheck:
test: ["CMD", "nc", "-z", "localhost", "80"]
interval: 5s
timeout: 10s
retries: 15
start_period: 5s

monkeytype-backend:
container_name: monkeytype-backend
image: monkeytype/monkeytype-backend:latest
ports:
- "${BACKEND_PORT:-5005}:5005"
#uncomment if you want to expose the backend server (not recommended)
#ports:
# - "${BACKEND_PORT:-5005}:5005"
networks:
- public # access to the internet, e.g. google firebase
- isolated # access to the databases
restart: on-failure
environment:
- DB_NAME=monkeytype
- DB_URI=mongodb://monkeytype-mongodb:27017
- REDIS_URI=redis://monkeytype-redis:6379
- FRONTEND_URL=${MONKEYTYPE_FRONTENDURL}
- FRONTEND_URL=${BASE_URL:-http://localhost}
- RECAPTCHA_SECRET=${RECAPTCHA_SECRET:-}
- EMAIL_HOST=${EMAIL_HOST:-}
- EMAIL_PORT=${EMAIL_PORT:-}
Expand All @@ -56,18 +109,32 @@ services:
monkeytype-mongodb:
condition: service_healthy
healthcheck:
test: ["CMD", "nc", "-z", "-v", "localhost", "5005"]
test: ["CMD", "nc", "-z", "localhost", "5005"]
interval: 5s
timeout: 10s
retries: 15
start_period: 5s
labels:
- "traefik.http.middlewares.backend-prefix.stripprefix.prefixes=/api"
- "traefik.http.middlewares.backend-headers.headers.customrequestheaders.CF-Connecting-IP="
- "traefik.http.routers.backend.middlewares=backend-prefix,backend-headers"
- "traefik.http.routers.backend.rule=PathPrefix(`/api`)"
- "traefik.http.routers.backend.priority=100"
- "traefik.http.routers.backend.entrypoints=web"
- "traefik.http.services.backend.loadbalancer.server.port=5005"
- "traefik.enable=true"
- "traefik.docker.network=monkeytype-public"

monkeytype-redis:
container_name: monkeytype-redis
image: redis:6.2.6
#uncomment if you want to expose the redis server
#uncomment if you want to expose the redis server (not recommended)
#ports:
# - "${REDIS_PORT:-6379}:6379"
networks:
- isolated
#uncomment if you want to expose the redis server (not recommended)
#- public
restart: on-failure
volumes:
- redis-data:/data
Expand All @@ -84,9 +151,13 @@ services:
restart: on-failure
volumes:
- mongo-data:/data/db
#uncomment if you want to expose the mongodb server
#uncomment if you want to expose the mongodb server (not recommended)
#ports:
# - "${MONGO_PORT:-27017}:27017"
networks:
- isolated
#uncomment if you want to expose the mongodb server (not recommended)
#- public
healthcheck:
test: echo 'db.stats().ok' | mongo localhost:27017/test --quiet
interval: 5s
Expand All @@ -99,3 +170,10 @@ volumes:
name: monkeytype_mongo_data
redis-data:
name: monkeytype_redis_data

networks:
isolated:
name: monkeytype-isolated
internal: true
public:
name: monkeytype-public
16 changes: 10 additions & 6 deletions docker/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

### === Required Config ===

# URL of the frontend (accessible by browser)
MONKEYTYPE_FRONTENDURL=http://myserver:8080

# URL of the backend (accessible by browser)
MONKEYTYPE_BACKENDURL=http://myserver:5005
# By default your installation is available on http://localhost
# If you want to expose the installation on a network (private or public)
# you need to have a domain and enable the HTTPS marked entries in the docker-compose.yml
# and uncomment and set the following
#DOMAIN= # example: mydomain.tld
#BASE_URL= # example: https://mydomain.tld
#ACME_EMAIL= # example: certmanager@mydomain.tld

### === Optional: Google reCAPTCHA ===

Expand Down Expand Up @@ -38,7 +40,9 @@ RECAPTCHA_SECRET=

### === Optional: Custom Ports ===

# HTTP_PORT=8080
# HTTP_PORT=80
# HTTPS_PORT=443
# FRONTEND_PORT=8080
# BACKEND_PORT=5005
# REDIS_PORT=6379
# MONGO_PORT=27017
2 changes: 1 addition & 1 deletion docker/frontend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ COPY docker/frontend/firebase-config-live.ts frontend/src/ts/constants/firebase-
COPY docker/frontend/firebase-config-live.ts frontend/src/ts/constants/firebase-config-live.ts

#gimme pnpm + build
RUN npm i -g pnpm && \
RUN npm i -g pnpm@11.21.0 && \
pnpm i --frozen-lockfile && \
npm run build

Expand Down
2 changes: 1 addition & 1 deletion docs/CONTRIBUTING_ADVANCED.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ If you use `nvm` (if you use Windows, use [nvm-windows](https://github.com/corey

Alternatively, you can navigate to the NodeJS [website](https://nodejs.org/en/) to download it from there.

For package management, we use `pnpm` instead of `npm` or `yarn`. You can install it by running `npm i -g pnpm@10.28.1`. This will install `pnpm` globally on your machine.
For package management, we use `pnpm` instead of `npm` or `yarn`. You can install it by running `npm i -g pnpm@11.21.0`. This will install `pnpm` globally on your machine.

### Docker (Recommended but Optional)

Expand Down
20 changes: 16 additions & 4 deletions docs/SELF_HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,34 @@
- create an `.env` file, you can copy the content from the [example.env](https://github.com/monkeytypegame/monkeytype/tree/master/docker/example.env).
- download the [backend-configuration.json](https://github.com/monkeytypegame/monkeytype/tree/master/docker/backend-configuration.json)
- run `docker compose up -d`
- after the command exits successfully you can access [http://localhost:8080](http://localhost:8080)
- after the command exits successfully you can access [http://localhost](http://localhost)

### Hosting over the network (HTTPS)

If you plan to access your self-hosted Monkeytype instance over a local network or the internet (not using `localhost`), **you must serve it over HTTPS**. Modern browsers restrict key web features, such as `crypto.randomUUID`, to secure contexts. Accessing the site via HTTP over a network will cause the frontend to crash with errors like `Uncaught TypeError: crypto.randomUUID is not a function`.

To solve this, you need to place a reverse proxy (like Nginx, Caddy, or Traefik) in front of your containers to handle HTTPS/TLS termination.
#### Enable HTTPS

Update the `.env` file and uncomment these lines and set the values based on your domain.

```
DOMAIN=mydomain.com
BASE_URL=https://mydomain.com
ACME_EMAIL=certmanager@mydomain.com
```

Update the `docker-compose.yml` and uncomment all lines marked with `# enable for HTTPS`.



#### Troubleshooting Frontend Connection Issues

If your reverse proxy is up but you see errors like `Looks like the server is experiencing unexpected down time` or network errors when fetching resources, your frontend is likely trying to communicate with the backend over unsecure HTTP, causing a **Mixed Content** block in the browser.

Ensure you configure the frontend to talk to your secure backend URL by following these rules in your `.env` file:

1. **Update the frontend and backend URL:** Set `MONKEYTYPE_FRONTENDURL` and `MONKEYTYPE_BACKENDURL` to your full HTTPS backend domain.
2. **Do not include a trailing slash:** Ensure the URL does not end with a `/` (e.g., use `https://api.yourdomain.com`, **not** `https://api.yourdomain.com/`). A trailing slash will cause `404 Not Found` errors due to double slashes in the API calls (like `//configuration`).
1. **Update the frontend and backend URL:** Set `DOMAIN` and `BASE_URL` correctly, usually `BASE_URL` is `https://DOMAIN`.
2. **Do not include a trailing slash:** Ensure the URL does not end with a `/` (e.g., use `https://yourdomain.com`, **not** `https://yourdomain.com/`). A trailing slash will cause `404 Not Found` errors due to double slashes in the API calls (like `//configuration`).
3. **Force container recreation:** Monkeytype is a Single Page Application (SPA), meaning environment variables are baked into the static JavaScript files during startup. If you change your `.env`, you must completely recreate the container for the changes to apply:

```bash
Expand Down
66 changes: 65 additions & 1 deletion frontend/__tests__/components/ui/form/InputField.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { render, screen, fireEvent } from "@solidjs/testing-library";
import { AnyFieldApi } from "@tanstack/solid-form";
import { describe, it, expect, vi } from "vitest";
import { z } from "zod";

import { InputField } from "../../../../src/ts/components/ui/form/InputField";

function makeField(
name: string,
value?: string | number | boolean,
value?: string | number | boolean | Date,
): AnyFieldApi {
let current = value;
const meta = {
Expand Down Expand Up @@ -173,4 +174,67 @@ describe("InputField", () => {
fireEvent.input(input, { target: { value: "6" } });
expect(field.handleChange).toHaveBeenCalledWith(6);
});

it("keeps empty string for string values", async () => {
const field = makeField("age", "test");
render(() => <InputField field={() => field} />);
const input = screen.getByRole("textbox");

fireEvent.input(input, { target: { value: "" } });
fireEvent.blur(input);

expect(field.handleChange).toHaveBeenCalledWith("");
});

it("handles empty string for numeric fields", async () => {
const field = makeField("age", 5);
render(() => <InputField field={() => field} type="number" />);
const input = screen.getByRole("spinbutton");

fireEvent.input(input, { target: { value: "" } });
fireEvent.blur(input);

expect(field.handleChange).toHaveBeenCalledWith(undefined);
});

it("applies number schema constraints", async () => {
const field = makeField("value", 10);
const schema = z.number().min(1).max(100).int();
render(() => (
<InputField field={() => field} type="number" schema={schema} />
));

const input = screen.getByRole("spinbutton");
expect(input).toHaveAttribute("min", "1");
expect(input).toHaveAttribute("max", "100");
expect(input).toHaveAttribute("step", "1");
});

it("applies float number schema constraints", async () => {
const field = makeField("value", 1.5);
const schema = z.number().min(0.5).max(10.5);
render(() => (
<InputField field={() => field} type="number" schema={schema} />
));

const input = screen.getByRole("spinbutton");
expect(input).toHaveAttribute("min", "0.5");
expect(input).toHaveAttribute("max", "10.5");
expect(input).toHaveAttribute("step", "any");
});

it("applies date schema constraints", async () => {
const field = makeField("date", new Date("2024-01-15"));
const schema = z
.date()
.min(new Date("2024-01-01"))
.max(new Date("2024-12-31"));
const { container } = render(() => (
<InputField field={() => field} type="date" schema={schema} />
));

const input = container.querySelector("input") as HTMLInputElement;
expect(input).toHaveAttribute("min", "2024-01-01");
expect(input).toHaveAttribute("max", "2024-12-31");
});
});
Loading
Loading