Files
mathew/README.md
Aran Roig 580a160300
Some checks failed
Build and Deploy Nuxt / build (push) Failing after 30s
Pipeline test
2026-10-01 21:34:56 +02:00

210 lines
9.7 KiB
Markdown

# 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 both dev servers together
├── 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)
```
## 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 both together from the repo root:
```bash
./dev.sh # both dev servers in the foreground (Ctrl+C stops both)
./dev.sh start|stop|restart|status
./dev.sh logs [all|backend|frontend]
```
### Scripts
| Where | Command | What it does |
| --- | --- | --- |
| root | `./dev.sh [dev]` | Both dev servers, foreground, tagged output, one Ctrl+C stops both |
| `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 |
## 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/<lang>/<identity>`. 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/<slug>` 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 `<html>`.
## 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 |
| `/wiki/<lang>/<slug>` | One language version of an article, drawn as a page of the book with its table of contents |
| `/wiki/<slug>` | The short form, naming no language — turned onto the version the slug speaks |
| `/wiki/<slug>/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/<slug>` or
`/wiki/<lang>/<slug>`. 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 <token>` 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.
## 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.