Claude 4c16040373 Fix #6: larger RTE toolbar buttons + bigger, both-axis-scrolling editor
The shared rich-text editor's toolbar buttons (especially the link and
image icons) were too small, and the editor body was short and only
scrolled vertically. These styles are shared by every RichTextEditor on
the site, so the fix applies to the wiki editor, the post editor, and any
future ones.

- Toolbar buttons: 30px -> 38px, base font 0.85rem -> 1rem, with roomier
  toolbar padding and gap.
- Icon (glyph) buttons (Link, Insert image, wiki-page, Quote, Divider,
  Undo, Redo) bumped to 1.25rem so they read clearly.
- Editor body: max-height 460px -> min(640px, 65vh); ProseMirror
  min-height 220px -> 320px.
- Body now scrolls both ways: overflow-y:auto -> overflow:auto (wide
  images, code blocks, tables can scroll sideways).
- Nudged the internal-link popover offset to match the taller toolbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:47:15 -05:00
2026-06-26 21:51:27 -05:00

UOMysticmoon Website

Public site, wiki, and protected admin panel for the UOMysticmoon private Ultima Online shard — a full-stack app in one repo:

  • Backend — Node.js + Express REST API (layered router → controller → model → db), MariaDB, JWT-in-cookie auth.
  • Frontend — React + Vite single-page app (public site, wiki, and the admin panel), dark "gothic" theme (Cinzel + Georgia).
  • Deploy — Docker Compose (app + MariaDB) behind a Pangolin reverse proxy. Express serves the built SPA in production.

The design reference is BACKEND_DESIGN.md (API contract, schema, security).


Contents


Tech stack

Layer Tech
Backend Node.js 20+, Express 4, mariadb driver (parameterized SQL, no ORM)
Auth JWT in an httpOnly cookie, bcrypt password hashing
Database MariaDB 11 (own container)
Frontend React 18, Vite 5, React Router 6
Email Nodemailer (SMTP) with a mailto: fallback
Deploy Docker Compose, Pangolin reverse proxy

Project structure

UOMSITE/
├─ server/                     Express API
│  ├─ src/
│  │  ├─ server.js             bootstrap: ensure schema → seed → listen (0.0.0.0)
│  │  ├─ app.js                middleware + static SPA + routes
│  │  ├─ router/v1/            auth / public / admin route groups
│  │  ├─ model/                users · posts · wiki · settings · activity (.model + .db)
│  │  ├─ middleware/           siteMode · noindex · rateLimit · validate
│  │  └─ utils/                auth (JWT/cookies) · db (pool) · mailer · logger
│  ├─ db/                      schema.sql + seed.js
│  └─ .env.example
├─ client/                     React + Vite SPA
│  ├─ src/
│  │  ├─ routes/public/        Portal, Website, News, Screenshots, FiveOnFriday, Newsletter(+Issue), Status, About, Maintenance
│  │  ├─ routes/wiki/          Wiki landing + WikiArticle
│  │  ├─ routes/admin/         AdminLogin, AdminLayout, views/ (Dashboard, Posts, Wiki, Settings, Activity, Users) + editors
│  │  ├─ components/           SiteHeader, SiteFooter, layout, guards, Modal, …
│  │  ├─ contexts/             AuthContext, SiteContext
│  │  ├─ api/client.js         fetch wrapper (sends cookies)
│  │  └─ styles/theme.css      design tokens
│  └─ public/assets/img/       hero image
├─ Dockerfile                  builds client → serves via Express
├─ docker-compose.yml          app + MariaDB
├─ .env.example                root env (used by Compose)
└─ package.json                workspace scripts

Prerequisites

  • Node.js 20+ and npm (Node 22/24 are fine).
  • Docker Desktop (for MariaDB, and for the full Compose deploy).

Setup & run

Option A — Docker Compose (full stack)

The simplest way to run everything. The image installs server deps, builds the React client, and Express serves it; MariaDB runs in its own container; tables + defaults + the first admin are created automatically on first boot.

cp .env.example .env
# Edit .env and set at least:
#   DB_PASSWORD, DB_ROOT_PASSWORD   (any strong values)
#   JWT_SECRET                      (a long random string)
#   ADMIN_USERNAME, ADMIN_PASSWORD  (your first admin login)

docker compose up -d --build
  • App: http://localhost:3000 (binds 0.0.0.0)
  • Health check: GET http://localhost:3000/api/health{ "status": "ok" }
  • Logs: docker compose logs -f app (and ./logs/app.log on the host)
  • Stop: docker compose down (add -v to also wipe the database + uploads volumes)

Option B — Local development (hot reload)

Run the API and the Vite dev server separately. The Vite server proxies /api and /uploads to the backend, so the SPA stays same-origin (cookies work).

1. Start a MariaDB the backend can reach (published on localhost:3306):

docker run -d --name uomm-db -p 3306:3306 -e MARIADB_DATABASE=uomysticmoon -e MARIADB_USER=uomm -e MARIADB_PASSWORD=devpass -e MARIADB_ROOT_PASSWORD=rootpass mariadb:11

2. Configure + start the backend (terminal 1):

cp server/.env.example server/.env
# Set DB_HOST=127.0.0.1, DB_PORT=3306, DB_USER=uomm, DB_PASSWORD=devpass,
#     JWT_SECRET=<anything>, ADMIN_USERNAME=admin, ADMIN_PASSWORD=<your password>
npm run install-server
npm run server          # nodemon → http://localhost:3000

3. Start the frontend (terminal 2):

npm run install-client
npm run client          # Vite → http://localhost:5173

Develop at http://localhost:5173 (hot reload). On Windows, the Vite proxy targets 127.0.0.1:3000 to avoid the IPv6-localhost pitfall.

Tip: npm run install-all installs both server and client deps in one go.

Option C — Production build without Docker

Build the SPA and let Express serve it on a single port (still needs a MariaDB + server/.env):

npm run install-all
npm run build           # → client/dist
npm start               # node server → serves API + SPA at http://localhost:3000

First admin & site mode

  • On first boot, if the users table is empty and ADMIN_USERNAME / ADMIN_PASSWORD are set, the first admin is created automatically. You can also run npm run seed. After it exists you may blank those env vars.
  • The site starts in maintenance mode: public visitors see the polished "coming soon" page; the admin login and panel are always reachable.
  • Sign in at /admin/login, then flip Maintenance → Live from the Dashboard. A logged-in admin can preview the live site even while it's in maintenance.

Pages & routes

Public (gated by site mode):

Route Page
/ Portal landing (hero + destinations)
/site Website index (section cards)
/site/news News feed
/site/screenshots Screenshot gallery
/site/five-on-friday Five on Friday
/site/newsletter · /site/newsletter/:id Newsletter list + issue
/site/about · /site/status About · Shard status
/wiki · /wiki/:slug Wiki landing + article (auto table-of-contents)

Admin (cookie auth, noindex):

Route View
/admin/login Sign in
/admin Dashboard (mode toggle, stats, recent activity)
/admin/posts Posts CRUD + publish + image upload
/admin/wiki Wiki pages CRUD
/admin/settings Site settings
/admin/activity Activity log
/admin/users User management

API endpoints

Group Base Auth
Auth /api/v1/auth (login, logout, me) cookie
Public /api/v1/public (settings, status, posts/:category, posts/:category/:idOrSlug, wiki, wiki/:slug, contact) none
Admin /api/v1/admin (dashboard, site-mode, posts, posts/upload, wiki, settings, activity, users) cookie (admin)

Post categories (URL form): news, five-on-friday, newsletter, screenshots. See BACKEND_DESIGN.md §4 for the full contract.


Environment variables

Copy .env.example (Compose) or server/.env.example (local) and fill in. .env is git-ignored.

Var Default Notes
NODE_ENV production
PORT 3000 server listens on 0.0.0.0:PORT
DB_HOST / DB_PORT db / 3306 db in Compose; 127.0.0.1 for local dev
DB_NAME / DB_USER / DB_PASSWORD uomysticmoon / uomm / — app database credentials
DB_ROOT_PASSWORD MariaDB root (Compose only)
JWT_SECRET required — long random string
JWT_EXPIRES_IN 1d token + cookie lifetime
COOKIE_SECURE auto auto = Secure only over HTTPS (works on LAN HTTP + Pangolin HTTPS)
COOKIE_NAME uomm_token
ADMIN_USERNAME / ADMIN_PASSWORD first-admin bootstrap (first boot only)
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS optional; blank → contact form uses mailto:
CONTACT_TO UOMysticmoon@gmail.com contact recipient
CLIENT_ORIGIN http://localhost:5173 enables CORS in dev only
LOG_LEVEL / FILE_LOG_LEVEL info / debug console / file verbosity
LOG_TO_FILE / LOG_DIR / LOG_FILE true / <server>/logs / app.log log file (bind-mounted to ./logs in Docker)

Security

JWT in an httpOnly, SameSite=Lax cookie (Secure auto-detected) · bcrypt hashing · login & contact rate limiting · express-validator on writes · helmet · admin routes noindex + robots.txt disallow · trust proxy for correct client IPs behind Pangolin · first admin seeded from env (no hardcoded credentials) · .env git-ignored. Passwords and request bodies are never logged. SMTP is optional — the contact form falls back to a mailto: link when unconfigured.


Logging

Every log line goes to both the console and a log file, timestamped and leveled (error / warn / info / debug):

2026-06-26T18:55:01.123Z INFO  [server] listening on http://0.0.0.0:3000 ...
2026-06-26T18:55:09.880Z INFO  [http] 192.168.1.40 admin POST /api/v1/auth/login 200 12 ms - 48 bytes
2026-06-26T18:55:14.402Z WARN  [auth] login failed {"username":"root","ip":"192.168.1.40"}
2026-06-26T18:55:20.110Z ERROR [error] GET /api/v1/public/wiki -> 500 ... {"stack":"..."}

Captured: startup config banner, schema/seed steps, HTTP access logs (real client IP via trust proxy, the authenticated admin, method/URL/status/time/size), login success/failure, rate-limit hits, site-mode changes, all errors with stack traces, and graceful shutdown. Console verbosity is LOG_LEVEL; the file keeps the fuller FILE_LOG_LEVEL record. In Docker the file is bind-mounted to ./logs/app.log and docker compose logs -f app shows the console stream.


Deployment behind Pangolin

docker compose up -d --build exposes the app container on 0.0.0.0:3000 (no 127.0.0.1 binding) so Pangolin can reach it. Point a Pangolin resource at app:3000. Because COOKIE_SECURE defaults to auto, the admin login works both directly via the LAN IP over HTTP and through Pangolin over HTTPS — no config change needed. MariaDB stays on the private Compose network (no published port by default); data persists in the dbdata volume, uploads in uploads.

Description
No description provided
Readme 9.8 MiB
Languages
JavaScript 98.7%
CSS 0.9%
C# 0.3%