# Mathew — a wiki for mathematics A collaborative wiki of mathematical concepts. Articles are written in **Markdown** with **LaTeX** (KaTeX), stored in MongoDB behind an Express API, and rendered by a Nuxt 4 app as an open book with a table of contents, a table of topics, a link graph, and a review-then-publish flow for edits. The interface speaks five languages — English, Spanish, Catalan, French, German — chosen per account, and articles themselves are multilingual: one article can exist in several languages, each with its own address. ``` mathew/ ├── dev.sh # start/stop all three services together (lean via docker compose) ├── docker-compose.yml # nginx + frontend + backend (prebuilt images) ├── nginx.conf # reverse proxy: / → frontend, /api/ → backend ├── backend/ # Express 5 + Mongoose API (port 5000) ├── frontend/ # Nuxt 4 / Vue 3 wiki UI (port 3000) └── lean/ # Lean editor server (lean4web) — its own Docker image (port 8080) ``` ## Quick start Requires **MongoDB** on `localhost:27017` (database `mathew`) and **Node 20+**. ```bash # 1. API cd backend npm install cp .env.example .env # then set JWT_SECRET npm run seed # optional: sample articles with LaTeX npm run dev # → http://localhost:5000 # 2. UI cd ../frontend npm install npm run dev # → http://localhost:3000 ``` Or run everything together from the repo root: ```bash ./dev.sh # all three services in the foreground (Ctrl+C stops all) ./dev.sh start|stop|restart|status ./dev.sh logs [all|backend|frontend|lean] ``` The third service is the **Lean editor server** (`lean/`), run through docker compose alongside the two npm dev servers — the image's first build fetches Mathlib, so give it time; without docker, `dev.sh` warns and starts backend + frontend anyway. ### Scripts | Where | Command | What it does | | --- | --- | --- | | root | `./dev.sh [dev]` | All three services, foreground, tagged output, one Ctrl+C stops them all | | `backend/` | `npm run dev` | API with `node --watch` (no build step) | | `backend/` | `npm start` | API without watch | | `backend/` | `npm run seed` | A handful of sample articles | | `backend/` | `npm run seed:bulk` | 100 cross-linked articles for load-testing the UI and graph (idempotent) | | `frontend/` | `npm run dev` / `build` / `preview` | Nuxt dev server, production build, preview of the build | There is **no test suite, linter, or typecheck script** configured. ### Configuration | Where | Variable | Default | Meaning | | --- | --- | --- | --- | | `backend/.env` | `PORT` | `5000` | API port | | `backend/.env` | `MONGO_URI` | `mongodb://127.0.0.1:27017/mathew` | Mongo connection | | `backend/.env` | `JWT_SECRET` | *(insecure dev key)* | Signs session tokens — set this in production | | `backend/.env` | `AUTH_TOKEN_TTL` | `7d` | How long a session lasts | | `frontend` env | `NUXT_PUBLIC_API_BASE` | `http://localhost:5000` | Where the UI calls the API | | `frontend` env | `NUXT_PUBLIC_LEAN_URL` | `http://localhost:8080` | The Lean server (`lean/`) the editor talks to | ## How it works **Reading is open; writing takes an account.** Anyone can register, and the **first account ever created becomes the admin** (on a database whose accounts all predate roles, the oldest one gets it at startup). The admin hands the role on or takes it back from `/admin`; nobody may change their own role, so the dashboard always has a door. - An **admin**'s save applies immediately. - A **member**'s save is answered `202` and filed as an **edit proposal** instead — the live article does not move. Saving again while a proposal waits *revises* it, and each proposal keeps a snapshot of the article as it read when the request was sent, so the desk can show a diff and warn when the page has moved since. - Proposals include **new articles**: a request for a page the wiki does not have yet keeps the address it asks for and appears only when approved. - Approving or rejecting happens at `/admin/proposals`, and approval runs the same code an admin's direct write runs — so a retitle re-slugs, and a language the article already has in another version is refused. - **Deleting is admin-only**, and it settles that article's waiting proposals with it. Articles name no author, so they outlive the account that wrote them. ### Languages Two language ideas, deliberately apart: - The **interface language** (`en`, `es`, `ca`, `fr`, `de`) is a preference and never part of a URL. Precedence: the signed-in account's choice → the `mathew-locale` cookie → the browser → English. Pick it in the account popup. - The **article language** appears in the address: `/wiki//`. Every version of an article has its own address, English included — `/wiki/en/bayes-theorem` and `/wiki/fr/bayes-theorem` are one article two ways. The short form `/wiki/` redirects onto the version the reader should see: their own language if the article speaks it, English failing that. Switching an article's language turns the page of the open book rather than opening a second one, and "recently viewed" remembers article + language as a single entry. ### Themes Light and dark, plus eight accent colours, chosen in the settings popup and applied before first paint so there is no flash. Colours are CSS custom properties swapped by `data-theme` / `data-accent` on ``. ## Pages | Route | What it is | | --- | --- | | `/` | Search, recently viewed, a way into the shelf | | `/articles` | The shelf, filed by topic — one folder per topic, opening onto a batch-at-a-time grid of cards; language and topic filters are applied by the API | | `/graph` | The wiki's link map: one node per article, one edge per linked pair | | `/lean` | The Lean 4 editor — Monaco with a live Goals/Messages infoview, talking to its own Lean server (`lean/`) | | `/wiki//` | One language version of an article, drawn as a page of the book with its table of contents | | `/wiki/` | The short form, naming no language — turned onto the version the slug speaks | | `/wiki//edit` | Markdown editor with live preview (Write / Split / Preview) | | `/new` | Create a new article | | `/admin` | Accounts and roles (admin only) | | `/admin/proposals` | The edit-request desk: diff, approve, reject (admin only) | ## Writing articles GitHub-ish Markdown — headings, tables, lists, blockquotes, fenced code with syntax highlighting — plus LaTeX: `$E = mc^2$` inline, `$$ … $$` in display mode. Number-set macros `\R`, `\N`, `\Z`, `\Q`, `\C` are available. Raw HTML in an article is escaped, and everything rendered is sanitized with DOMPurify. Link between articles with wiki links — `[[Bayes' theorem]]`, `[[Fourier series#definition]]`, `[[Zeno's paradoxes|Zeno]]` — or with a normal Markdown link to `/wiki/` or `/wiki//`. Both feed the graph view. Shortcuts: `/` focuses search on the home page, `Ctrl/Cmd+K` opens the search popup, `Ctrl/Cmd+S` saves in the editor. Changing an article's title can change its slug — old links then 404, as no redirects are kept. ## API `GET /health` answers `{ status, mongo }`. Everything below lives under `/api`. Reads are public; writes need `Authorization: Bearer ` from `/auth/login` or `/auth/register`. All errors answer `{ message }`. ### Accounts | Method | Endpoint | Notes | | --- | --- | --- | | POST | `/auth/register` | Creates the account and returns its session; the first account ever is an admin | | POST | `/auth/login` | A wrong name and a wrong password answer the same way | | GET | `/auth/me` | Who a token belongs to | | PUT | `/auth/me` | Rename | | PUT | `/auth/password` | Needs the current password | | PUT | `/auth/locale` | Interface language — one of `en es ca fr de` | | PUT | `/auth/avatar` | A small data URL; `""` clears it | ### Articles Articles are addressed by **slug**, not by Mongo id. | Method | Endpoint | Notes | | --- | --- | --- | | GET | `/articles` | `{ articles, total, hasMore }`, paged **by article** — `?q=`, `?tag=`, `?lang=`, `?limit=`, `?offset=`; `?prefer=` picks which version stands for an article, `?preferFirst` ranks those versions first; no `limit` means the whole shelf | | GET | `/articles/facets` | `{ total, topics, languages }` counted over the whole shelf | | GET | `/articles/random` | One random article | | GET | `/articles/graph` | `{ nodes, edges }` — the link map built from wiki and Markdown links | | GET | `/articles/slug/:slug` | One article | | POST | `/articles` | Create (`title` required; the slug is generated and uniquified). Admin applies; a member is answered `202 { proposal }` | | PUT | `/articles/slug/:slug` | Update. Admin applies; a member is answered `202 { proposal }` | | DELETE | `/articles/slug/:slug` | Admin only | ### Administration Everything behind `requireAuth, requireAdmin`. | Method | Endpoint | Notes | | --- | --- | --- | | GET | `/admin/users` | Every account, newest first | | PUT | `/admin/users/:id/role` | `role` is `admin` or `member`; never your own | | DELETE | `/admin/users/:id` | Close an account — never your own; its articles stay | | GET | `/admin/proposals` | Every request, decided ones included, with its diff material | | GET | `/admin/proposals/count` | Just `{ pending }` — what the sidebar's badge wears | | PUT | `/admin/proposals/:id/approve` | Writes the change; a conflict refuses it and the request keeps waiting | | PUT | `/admin/proposals/:id/reject` | Refuses; the page stays put | `/api/items` is a small demo resource (CRUD over one schema) left in as a reference for how a router, model and error path fit together. ## Deploy `docker-compose.yml` runs nginx in front of prebuilt frontend and backend images: nginx listens on `3000` and routes `/api/` to the backend and everything else to the Nuxt server, passing the standard forwarded headers. Point `NUXT_PUBLIC_API_BASE` and `MONGO_URI` at the deployment's real addresses and set a real `JWT_SECRET` — the fallback key is a development convenience only, and anyone holding it can mint a session. The stack also runs `lean` — the Lean server built from [`lean/`](lean/README.md), listening on `8080` with its websocket on that one port; the wiki's editor speaks the Lean language protocol to it directly. It keeps no state of the wiki's, but it runs real Lean for every session, so plan the machine accordingly; point `NUXT_PUBLIC_LEAN_URL` at the address the browser reaches (https if the site is https — the `wss://` is derived from it, and mixed content is blocked). ## Notes for contributors [`AGENTS.md`](AGENTS.md) is the working map of the codebase: file-by-file layout, the backend and frontend conventions, and the gotchas that are easy to get wrong (article identity vs. document slug, versioned addresses, MongoDB's reserved `language` field, localization rules). ## License MIT.