Add Gitea SSO, fix security audit findings, harden production defaults

Gitea SSO: cookie-based single sign-on via nginx auth_request — sets
cml_session cookie on login/refresh, validates via /api/auth/gitea-sso-validate,
injects X-WEBAUTH-USER header for reverse proxy auth. Dedicated GITEA_SSO_SECRET
and SERVICE_PASSWORD_SALT env vars isolate secret rotation.

Security fixes from March 30 audit: IDOR on ticketed events (requireEventOwnership
middleware), IDOR on action items (admin/assignee/creator check), path traversal
on photos (resolve-based validation), CSV upload size limit (5MB), shared calendar
email exposure removed.

Gitea provisioner: auto-sync docs repo collaborator access based on role
(CONTENT_ROLES get write, SUPER_ADMIN gets admin). Gitea client extended
with collaborator management API methods.

Production hardening: NODE_ENV defaults to production in docker-compose.prod.yml,
Grafana anonymous auth disabled, install.sh branch ref updated to main.

Admin UI: moved docs reset from toolbar to MkDocs Settings danger zone,
improved collab Ctrl+S to explicitly save + cache-bust preview.

MkDocs site rebuild with updated repo data, upgrade screenshots, and content.

Bunker Admin
This commit is contained in:
bunker-admin 2026-03-31 11:20:01 -06:00
parent 9321aeb263
commit 91db29402c
175 changed files with 9683 additions and 1184 deletions

View File

@ -53,6 +53,14 @@ JWT_REFRESH_EXPIRY=7d
# Generate with: openssl rand -hex 32
ENCRYPTION_KEY=GENERATE_WITH_openssl_rand_hex_32
# Gitea SSO cookie signing secret (separate from JWT — falls back to JWT_ACCESS_SECRET if empty)
# Generate with: openssl rand -hex 32
GITEA_SSO_SECRET=
# Salt for deriving deterministic service passwords (Gitea, Rocket.Chat)
# Falls back to JWT_ACCESS_SECRET if empty — set a dedicated value to isolate secret rotation
# Generate with: openssl rand -hex 32
SERVICE_PASSWORD_SALT=
# --- Initial Super Admin User (auto-created during database seeding) ---
# These credentials are used to create the initial super admin account
# Change these before running the seed script in production

View File

@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
Changemaker Lite is a self-hosted political campaign platform built with Docker Compose. It consolidates advocacy email campaigns, geographic mapping, volunteer management, and administration into a single TypeScript stack. The primary domain is `cmlite.org`.
**Current state:** V2 rebuild substantially complete on the `v2` branch. Core platform operational with Phases 1-14 complete. See `V2_PLAN.md` for the full roadmap.
**Current state:** V2 rebuild substantially complete (merged to `main`). Core platform operational with Phases 1-14 complete. See `V2_PLAN.md` for the full roadmap.
**Status Summary:**
- ✅ Phases 1-14 Complete (Foundation through Monitoring + DevOps)
@ -160,7 +160,7 @@ changemaker.lite/
The fastest way to deploy. No source code, no compilation:
```bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
```
This downloads a ~9MB release tarball, runs the config wizard, and sets `IMAGE_TAG=latest`. Then:
@ -173,11 +173,10 @@ Pre-built images are pulled from `gitea.bnkops.com/admin` (~2 min). Database mig
### Source Install (Development)
1. **Clone repository and checkout v2 branch:**
1. **Clone repository:**
```bash
git clone <repo-url> changemaker.lite
cd changemaker.lite
git checkout v2
```
2. **Create environment file:**
@ -321,7 +320,7 @@ docker compose down
./scripts/upgrade.sh --use-registry --force --skip-backup
# Install from tarball (end-user one-liner)
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
```
**Two compose files:**

View File

@ -174,7 +174,7 @@ The tarball contains:
```bash
# One-liner
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
# Or manual
curl -LO https://gitea.bnkops.com/admin/changemaker.lite/releases/latest/download/changemaker-lite-latest.tar.gz

View File

@ -105,7 +105,7 @@ Send SMS campaigns via an Android bridge, sync subscribers to Listmonk for newsl
```bash
# One-command install (downloads pre-built images, runs config wizard)
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
cd ~/changemaker.lite
docker compose up -d
@ -115,7 +115,7 @@ Or clone and build from source:
```bash
git clone <repo-url> changemaker.lite
cd changemaker.lite && git checkout v2
cd changemaker.lite
cp .env.example .env
# Edit .env -- set passwords, JWT secrets, admin credentials

225
admin/package-lock.json generated
View File

@ -1154,9 +1154,9 @@
"dev": true
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
"integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
"cpu": [
"arm"
],
@ -1167,9 +1167,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
"integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
"cpu": [
"arm64"
],
@ -1180,9 +1180,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
"integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
"cpu": [
"arm64"
],
@ -1193,9 +1193,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
"integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
"cpu": [
"x64"
],
@ -1206,9 +1206,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
"integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
"cpu": [
"arm64"
],
@ -1219,9 +1219,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
"integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
"cpu": [
"x64"
],
@ -1232,9 +1232,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
"integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
"cpu": [
"arm"
],
@ -1245,9 +1245,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
"integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
"cpu": [
"arm"
],
@ -1258,9 +1258,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
"integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
"cpu": [
"arm64"
],
@ -1271,9 +1271,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
"integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
"cpu": [
"arm64"
],
@ -1284,9 +1284,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
"integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
"cpu": [
"loong64"
],
@ -1297,9 +1297,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
"integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
"cpu": [
"loong64"
],
@ -1310,9 +1310,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
"integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
"cpu": [
"ppc64"
],
@ -1323,9 +1323,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
"integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
"cpu": [
"ppc64"
],
@ -1336,9 +1336,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
"integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
"cpu": [
"riscv64"
],
@ -1349,9 +1349,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
"integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
"cpu": [
"riscv64"
],
@ -1362,9 +1362,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
"integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
"cpu": [
"s390x"
],
@ -1375,9 +1375,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
"integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
"cpu": [
"x64"
],
@ -1388,9 +1388,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
"integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
"cpu": [
"x64"
],
@ -1401,9 +1401,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
"integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
"cpu": [
"x64"
],
@ -1414,9 +1414,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
"integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
"cpu": [
"arm64"
],
@ -1427,9 +1427,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
"integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
"cpu": [
"arm64"
],
@ -1440,9 +1440,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
"integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
"cpu": [
"ia32"
],
@ -1453,9 +1453,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
"integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
"cpu": [
"x64"
],
@ -1466,9 +1466,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
"integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
"cpu": [
"x64"
],
@ -2261,9 +2261,9 @@
}
},
"node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
"integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@ -2860,9 +2860,9 @@
"dev": true
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"engines": {
"node": ">=12"
@ -3651,9 +3651,9 @@
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
},
"node_modules/rollup": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
"integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.8"
@ -3666,31 +3666,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.1",
"@rollup/rollup-android-arm64": "4.57.1",
"@rollup/rollup-darwin-arm64": "4.57.1",
"@rollup/rollup-darwin-x64": "4.57.1",
"@rollup/rollup-freebsd-arm64": "4.57.1",
"@rollup/rollup-freebsd-x64": "4.57.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
"@rollup/rollup-linux-arm64-musl": "4.57.1",
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
"@rollup/rollup-linux-loong64-musl": "4.57.1",
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
"@rollup/rollup-linux-x64-gnu": "4.57.1",
"@rollup/rollup-linux-x64-musl": "4.57.1",
"@rollup/rollup-openbsd-x64": "4.57.1",
"@rollup/rollup-openharmony-arm64": "4.57.1",
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
"@rollup/rollup-win32-x64-gnu": "4.57.1",
"@rollup/rollup-win32-x64-msvc": "4.57.1",
"@rollup/rollup-android-arm-eabi": "4.60.1",
"@rollup/rollup-android-arm64": "4.60.1",
"@rollup/rollup-darwin-arm64": "4.60.1",
"@rollup/rollup-darwin-x64": "4.60.1",
"@rollup/rollup-freebsd-arm64": "4.60.1",
"@rollup/rollup-freebsd-x64": "4.60.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
"@rollup/rollup-linux-arm-musleabihf": "4.60.1",
"@rollup/rollup-linux-arm64-gnu": "4.60.1",
"@rollup/rollup-linux-arm64-musl": "4.60.1",
"@rollup/rollup-linux-loong64-gnu": "4.60.1",
"@rollup/rollup-linux-loong64-musl": "4.60.1",
"@rollup/rollup-linux-ppc64-gnu": "4.60.1",
"@rollup/rollup-linux-ppc64-musl": "4.60.1",
"@rollup/rollup-linux-riscv64-gnu": "4.60.1",
"@rollup/rollup-linux-riscv64-musl": "4.60.1",
"@rollup/rollup-linux-s390x-gnu": "4.60.1",
"@rollup/rollup-linux-x64-gnu": "4.60.1",
"@rollup/rollup-linux-x64-musl": "4.60.1",
"@rollup/rollup-openbsd-x64": "4.60.1",
"@rollup/rollup-openharmony-arm64": "4.60.1",
"@rollup/rollup-win32-arm64-msvc": "4.60.1",
"@rollup/rollup-win32-ia32-msvc": "4.60.1",
"@rollup/rollup-win32-x64-gnu": "4.60.1",
"@rollup/rollup-win32-x64-msvc": "4.60.1",
"fsevents": "~2.3.2"
}
},
@ -3993,10 +3993,9 @@
"dev": true
},
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"license": "ISC",
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"bin": {
"yaml": "bin.mjs"
},

View File

@ -59,7 +59,6 @@ import {
MobileOutlined,
DesktopOutlined,
CalendarOutlined,
ClearOutlined,
FormOutlined,
ShareAltOutlined,
LockOutlined,
@ -591,40 +590,6 @@ export default function DocsPage() {
const isMobile = !screens.md;
const { token } = theme.useToken();
const { building, confirmAndBuild, isSuperAdmin } = useMkDocsBuild();
const [resetting, setResetting] = useState(false);
const confirmAndReset = useCallback(() => {
if (!isSuperAdmin) return;
Modal.confirm({
title: 'Reset Documentation Site',
content: (
<div>
<p>This will reset all documentation content to a baseline template.</p>
<p><strong>Preserved:</strong> header config, analytics tracking, hooks, assets, stylesheets, blog.</p>
<p><strong>Deleted:</strong> all custom content pages.</p>
<p>A backup will be created automatically.</p>
</div>
),
okText: 'Reset Site',
okButtonProps: { danger: true },
onOk: async () => {
setResetting(true);
try {
const { data } = await api.post('/docs/reset');
message.success(`Site reset complete. ${data.filesReset} files reset, ${data.filesPreserved} preserved.`);
// Refresh file tree
const treeRes = await api.get('/docs/files');
setFileTree(treeRes.data.tree || []);
setSelectedFile(null);
setFileContent('');
} catch {
message.error('Failed to reset documentation site');
} finally {
setResetting(false);
}
},
});
}, [isSuperAdmin]);
const [fileTree, setFileTree] = useState<FileNode[]>(() => getCachedTree() || []);
const [config, setConfig] = useState<ServicesConfig | null>(null);
@ -800,9 +765,10 @@ export default function DocsPage() {
}
}, [fileContentCache, messageApi]);
// Handle navigation state from command palette — auto-select a file
// Handle navigation state from command palette or metadata page — auto-select a file
useEffect(() => {
const selectFile = (location.state as { selectFile?: string } | null)?.selectFile;
const state = location.state as { selectFile?: string; openFile?: string } | null;
const selectFile = state?.selectFile || state?.openFile;
if (!selectFile || loading) return;
// Expand parent directories so the file is visible in the tree
@ -855,8 +821,22 @@ export default function DocsPage() {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
if (collab.active) {
// In collab mode, auto-save handles persistence — just refresh preview
previewIframeRef.current?.contentWindow?.location.reload();
// In collab mode, explicitly save current content + refresh preview
if (selectedFile && collab.yText) {
const content = collab.yText.toString();
api.put(`/docs/files/${selectedFile}`, { content })
.then(() => messageApi.success('Saved'))
.catch(() => messageApi.error('Save failed'));
}
// Refresh preview with cache-buster
if (previewIframeRef.current && selectedFile) {
const url = filePathToMkDocsUrl(selectedFile);
setTimeout(() => {
if (previewIframeRef.current) {
previewIframeRef.current.src = url + '?_t=' + Date.now();
}
}, 1500);
}
} else {
saveFile();
}
@ -1603,13 +1583,10 @@ export default function DocsPage() {
<Tooltip title="Build static site">
<Button type="text" icon={<BuildOutlined />} onClick={confirmAndBuild} loading={building} size="middle" />
</Tooltip>
<Tooltip title="Reset site to baseline">
<Button type="text" danger icon={<ClearOutlined />} onClick={confirmAndReset} loading={resetting} size="middle" />
</Tooltip>
</>
)}
</Space>
), [layout, dirty, saving, saveFile, refreshPreview, mkdocsDirectUrl, token.colorBorderSecondary, isSuperAdmin, building, confirmAndBuild, resetting, confirmAndReset]);
), [layout, dirty, saving, saveFile, refreshPreview, mkdocsDirectUrl, token.colorBorderSecondary, isSuperAdmin, building, confirmAndBuild]);
// Inject header
useEffect(() => {

View File

@ -912,6 +912,49 @@ export default function MkDocsSettingsPage() {
))}
</Card>
{isSuperAdmin && (
<Card
title={<span style={{ color: token.colorError }}>Danger Zone</span>}
style={{ marginTop: 24, borderColor: token.colorError }}
size="small"
>
<Space direction="vertical" style={{ width: '100%' }}>
<Typography.Text type="secondary">
Reset all documentation content to a baseline template. A backup is created automatically.
Preserved: header config, analytics tracking, hooks, assets, stylesheets, blog.
</Typography.Text>
<Button
danger
onClick={() => {
Modal.confirm({
title: 'Reset Documentation Site',
content: (
<div>
<p>This will reset all documentation content to a baseline template.</p>
<p><strong>Preserved:</strong> header config, analytics, hooks, assets, stylesheets, blog.</p>
<p><strong>Deleted:</strong> all custom content pages.</p>
<p>A backup will be created automatically.</p>
</div>
),
okText: 'Reset Site',
okButtonProps: { danger: true },
onOk: async () => {
try {
const { data } = await api.post('/docs/reset');
message.success(`Site reset complete. ${data.filesReset} files reset, ${data.filesPreserved} preserved.`);
} catch {
message.error('Failed to reset documentation site');
}
},
});
}}
>
Reset Site to Baseline
</Button>
</Space>
</Card>
)}
</div>
),
},

135
api/package-lock.json generated
View File

@ -1656,14 +1656,6 @@
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz",
"integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow=="
},
"node_modules/@isaacs/cliui": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
"engines": {
"node": ">=18"
}
},
"node_modules/@js-temporal/polyfill": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz",
@ -1901,6 +1893,7 @@
"resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
"integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/express": "*"
}
@ -2102,9 +2095,9 @@
}
},
"node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@ -2216,14 +2209,11 @@
}
},
"node_modules/balanced-match": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz",
"integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==",
"dependencies": {
"jackspeak": "^4.2.3"
},
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
}
},
"node_modules/bcryptjs": {
@ -2260,14 +2250,14 @@
}
},
"node_modules/brace-expansion": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
}
},
"node_modules/buffer-equal-constant-time": {
@ -2548,6 +2538,7 @@
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
@ -2700,15 +2691,15 @@
}
},
"node_modules/drizzle-kit": {
"version": "0.31.9",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.9.tgz",
"integrity": "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==",
"version": "0.31.10",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz",
"integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==",
"dev": true,
"dependencies": {
"@drizzle-team/brocli": "^0.10.2",
"@esbuild-kit/esm-loader": "^2.5.5",
"esbuild": "^0.25.4",
"esbuild-register": "^3.5.0"
"tsx": "^4.21.0"
},
"bin": {
"drizzle-kit": "bin.cjs"
@ -3426,41 +3417,6 @@
"@esbuild/win32-x64": "0.27.3"
}
},
"node_modules/esbuild-register": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"dev": true,
"dependencies": {
"debug": "^4.3.4"
},
"peerDependencies": {
"esbuild": ">=0.12 <1"
}
},
"node_modules/esbuild-register/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/esbuild-register/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -3623,9 +3579,9 @@
]
},
"node_modules/fastify": {
"version": "5.7.4",
"resolved": "https://registry.npmjs.org/fastify/-/fastify-5.7.4.tgz",
"integrity": "sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==",
"version": "5.8.4",
"resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.4.tgz",
"integrity": "sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==",
"funding": [
{
"type": "github",
@ -3646,7 +3602,7 @@
"fast-json-stringify": "^6.0.0",
"find-my-way": "^9.0.0",
"light-my-request": "^6.0.0",
"pino": "^10.1.0",
"pino": "^9.14.0 || ^10.1.0",
"process-warning": "^5.0.0",
"rfdc": "^1.3.1",
"secure-json-parse": "^4.0.0",
@ -4066,20 +4022,6 @@
"url": "https://github.com/sponsors/dmonad"
}
},
"node_modules/jackspeak": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
"dependencies": {
"@isaacs/cliui": "^9.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@ -4407,14 +4349,14 @@
}
},
"node_modules/minimatch": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz",
"integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==",
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dependencies": {
"brace-expansion": "^5.0.2"
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "20 || >=22"
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@ -4542,9 +4484,9 @@
}
},
"node_modules/nodemailer": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
"integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
"engines": {
"node": ">=6.0.0"
}
@ -4705,9 +4647,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA=="
},
"node_modules/pathe": {
"version": "2.0.3",
@ -5000,9 +4942,9 @@
}
},
"node_modules/qs": {
"version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"dependencies": {
"side-channel": "^1.1.0"
},
@ -5848,10 +5790,9 @@
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="
},
"node_modules/yaml": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"license": "ISC",
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"bin": {
"yaml": "bin.mjs"
},

View File

@ -38,6 +38,11 @@ const envSchema = z.object({
// Encryption (for DB-stored secrets like SMTP password — required for all environments)
ENCRYPTION_KEY: z.string().min(32, 'ENCRYPTION_KEY must be at least 32 characters'),
// Gitea SSO cookie signing secret (falls back to JWT_ACCESS_SECRET if empty)
GITEA_SSO_SECRET: z.string().default(''),
// Salt for deriving deterministic service passwords (Gitea, Rocket.Chat — falls back to JWT_ACCESS_SECRET if empty)
SERVICE_PASSWORD_SALT: z.string().default(''),
// Initial Super Admin (auto-created during database seeding)
INITIAL_ADMIN_EMAIL: z.string().email().default('admin@cmlite.org'),
INITIAL_ADMIN_PASSWORD: z.string().min(12).default('REQUIRED_STRONG_PASSWORD_CHANGE_THIS')

View File

@ -1,6 +1,7 @@
import { Router, Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import { UserRole, UserStatus } from '@prisma/client';
import { authService } from './auth.service';
import { loginSchema, registerSchema, refreshSchema } from './auth.schemas';
@ -22,6 +23,9 @@ const router = Router();
const REFRESH_COOKIE_NAME = 'cml_refresh';
const REFRESH_COOKIE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days in ms
const SESSION_COOKIE_NAME = 'cml_session';
const SESSION_COOKIE_MAX_AGE = 30 * 60 * 1000; // 30 min buffer (JWT inside enforces 15min expiry)
/** Set the refresh token as an httpOnly cookie.
* Uses req.secure (respects trust proxy + X-Forwarded-Proto) to determine
* the Secure flag, so it works correctly over both HTTP (dev) and HTTPS (tunnel). */
@ -45,6 +49,53 @@ function clearRefreshCookie(req: Request, res: Response) {
});
}
/** Cookie options for the SSO session cookie (domain-wide for Gitea reverse proxy auth) */
function sessionCookieOptions(req: Request) {
const isSecure = req.secure;
const domain = env.DOMAIN;
// Use domain-wide cookie for production (subdomains); omit for localhost dev
const hasDomain = domain && !domain.includes('localhost') && !domain.match(/^\d/);
return {
httpOnly: true,
secure: isSecure,
sameSite: 'lax' as const,
path: '/',
maxAge: SESSION_COOKIE_MAX_AGE,
...(hasDomain ? { domain: `.${domain}` } : {}),
};
}
/** Set the SSO session cookie for Gitea reverse proxy auth (fire-and-forget).
* Only sets the cookie if the user has a provisioned Gitea account. */
async function setSessionCookie(req: Request, res: Response, userId: string) {
try {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { permissions: true },
});
const permissions = (user?.permissions as Record<string, unknown>) || {};
const giteaUser = permissions._giteaUsername as string | undefined;
if (!giteaUser) return; // Not provisioned — skip
const ssoSecret = env.GITEA_SSO_SECRET || env.JWT_ACCESS_SECRET;
const token = jwt.sign(
{ sub: userId, giteaUser },
ssoSecret,
{ algorithm: 'HS256', expiresIn: '15m' },
);
res.cookie(SESSION_COOKIE_NAME, token, sessionCookieOptions(req));
} catch (err) {
logger.debug('Failed to set SSO session cookie:', err);
}
}
/** Clear the SSO session cookie */
function clearSessionCookie(req: Request, res: Response) {
const opts = sessionCookieOptions(req);
delete (opts as Record<string, unknown>).maxAge;
res.clearCookie(SESSION_COOKIE_NAME, opts);
}
// POST /api/auth/login
router.post(
'/login',
@ -55,6 +106,8 @@ router.post(
const result = await authService.login(req.body.email, req.body.password);
// Set refresh token as httpOnly cookie (not in response body)
setRefreshCookie(req, res, result.refreshToken);
// Set SSO session cookie for Gitea reverse proxy auth
await setSessionCookie(req, res, result.user.id);
const { refreshToken: _, ...responseWithoutRefresh } = result;
res.json(responseWithoutRefresh);
} catch (err) {
@ -281,6 +334,10 @@ router.post(
const result = await authService.refreshTokens(refreshToken);
// Set new refresh token as httpOnly cookie
setRefreshCookie(req, res, result.refreshToken);
// Renew SSO session cookie for Gitea reverse proxy auth
if (result.user?.id) {
await setSessionCookie(req, res, result.user.id);
}
const { refreshToken: _, ...responseWithoutRefresh } = result;
res.json(responseWithoutRefresh);
} catch (err) {
@ -302,9 +359,11 @@ router.post(
await authService.logout(refreshToken);
}
clearRefreshCookie(req, res);
clearSessionCookie(req, res);
res.json({ message: 'Logged out' });
} catch (err) {
clearRefreshCookie(req, res);
clearSessionCookie(req, res);
next(err);
}
}

View File

@ -0,0 +1,50 @@
import { Router, Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import { env } from '../../config/env';
import { logger } from '../../utils/logger';
const router = Router();
interface SsoPayload {
sub: string;
giteaUser: string;
}
/**
* GET /api/auth/gitea-sso-validate
*
* Called by nginx auth_request to validate the cml_session cookie.
* Always returns 200 never blocks requests.
* Sets X-Gitea-User header when the session is valid and the user
* has a provisioned Gitea account. Empty header = no SSO (Gitea
* shows its own login page).
*/
router.get('/gitea-sso-validate', (req: Request, res: Response) => {
const token = req.cookies?.cml_session;
if (!token) {
res.setHeader('X-Gitea-User', '');
res.status(200).end();
return;
}
try {
const ssoSecret = env.GITEA_SSO_SECRET || env.JWT_ACCESS_SECRET;
const payload = jwt.verify(token, ssoSecret, {
algorithms: ['HS256'],
}) as SsoPayload;
if (payload.giteaUser) {
res.setHeader('X-Gitea-User', payload.giteaUser);
} else {
res.setHeader('X-Gitea-User', '');
}
} catch {
// Expired or invalid JWT — no SSO, graceful fallback
res.setHeader('X-Gitea-User', '');
}
res.status(200).end();
});
export { router as giteaSsoRouter };

View File

@ -925,7 +925,7 @@ export const sharedCalendarService = {
include: {
members: {
where: { status: SharedViewMemberStatus.ACCEPTED },
include: { user: { select: { id: true, name: true, email: true } } },
include: { user: { select: { id: true, name: true } } },
},
},
});
@ -972,7 +972,7 @@ export const sharedCalendarService = {
orderBy: [{ date: 'asc' }, { startTime: 'asc' }],
});
const memberName = member.user.name || member.user.email;
const memberName = member.user.name || 'Member';
for (const item of items) {
if (item.visibility === CalendarVisibility.PRIVATE || item.visibility === CalendarVisibility.FRIENDS) continue;
allItems.push({
@ -999,7 +999,7 @@ export const sharedCalendarService = {
const sysLayers = filteredLayers.filter(l => l.layerType === CalendarLayerType.SYSTEM);
for (const sysLayer of sysLayers) {
const sysItems = await this.getSystemLayerItems(member.userId, sysLayer, start, end);
const memberName = member.user.name || member.user.email;
const memberName = member.user.name || 'Member';
for (const si of sysItems) {
allItems.push({
...si,

View File

@ -1,6 +1,7 @@
import { FastifyInstance } from 'fastify';
import { createReadStream } from 'fs';
import { access } from 'fs/promises';
import { resolve } from 'path';
import { prisma } from '../../../config/database';
import { optionalAuth } from '../middleware/auth';
import { logger } from '../../../utils/logger';
@ -181,19 +182,25 @@ export async function photosPublicRoutes(fastify: FastifyInstance) {
}
}
if (!filePath || filePath.includes('..')) {
if (!filePath) {
return reply.code(404).send({ message: 'Image variant not found' });
}
const PHOTOS_BASE = '/media/local/photos';
const resolvedPath = resolve(filePath);
if (!resolvedPath.startsWith(resolve(PHOTOS_BASE) + '/')) {
logger.warn(`Photo path traversal attempt blocked: ${filePath}`);
return reply.code(403).send({ message: 'Access denied' });
}
try {
await access(filePath);
await access(resolvedPath);
} catch {
return reply.code(404).send({ message: 'Image file not found' });
}
reply.header('Content-Type', contentType);
reply.header('Cache-Control', 'public, max-age=604800, immutable');
return reply.send(createReadStream(filePath));
return reply.send(createReadStream(resolvedPath));
}
);
@ -208,19 +215,25 @@ export async function photosPublicRoutes(fastify: FastifyInstance) {
select: { thumbnailPath: true },
});
if (!photo?.thumbnailPath || photo.thumbnailPath.includes('..')) {
if (!photo?.thumbnailPath) {
return reply.code(404).send({ message: 'Thumbnail not found' });
}
const PHOTOS_BASE = '/media/local/photos';
const resolvedThumb = resolve(photo.thumbnailPath);
if (!resolvedThumb.startsWith(resolve(PHOTOS_BASE) + '/')) {
logger.warn(`Thumbnail path traversal attempt blocked: ${photo.thumbnailPath}`);
return reply.code(403).send({ message: 'Access denied' });
}
try {
await access(photo.thumbnailPath);
await access(resolvedThumb);
} catch {
return reply.code(404).send({ message: 'Thumbnail file not found' });
}
reply.header('Content-Type', 'image/jpeg');
reply.header('Cache-Control', 'public, max-age=604800, immutable');
return reply.send(createReadStream(photo.thumbnailPath));
return reply.send(createReadStream(resolvedThumb));
}
);

View File

@ -8,7 +8,8 @@ import {
import { validate } from '../../middleware/validate';
import { authenticate } from '../../middleware/auth.middleware';
import { requireRole } from '../../middleware/rbac.middleware';
import { EVENTS_ROLES } from '../../utils/roles';
import { EVENTS_ROLES, hasAnyRole } from '../../utils/roles';
import { AppError } from '../../middleware/error-handler';
const router = Router();
@ -44,6 +45,14 @@ router.get('/:id', authenticate, async (req: Request, res: Response, next: NextF
try {
const id = req.params.id as string;
const item = await actionItemsService.findById(id);
const isAdmin = hasAnyRole(req.user!, EVENTS_ROLES);
const isAssignee = item.assigneeUserId === req.user!.id;
const isCreator = item.createdByUserId === req.user!.id;
if (!isAdmin && !isAssignee && !isCreator) {
throw new AppError(403, 'Insufficient permissions', 'FORBIDDEN');
}
res.json(item);
} catch (err) { next(err); }
});
@ -56,10 +65,19 @@ router.post('/', authenticate, requireRole(...EVENTS_ROLES), validate(createActi
} catch (err) { next(err); }
});
// Update action item (authenticate only - assignees can update their own)
// Update action item — admins, assignees, or creators can update
router.put('/:id', authenticate, validate(updateActionItemSchema), async (req: Request, res: Response, next: NextFunction) => {
try {
const id = req.params.id as string;
const existing = await actionItemsService.findById(id);
const isAdmin = hasAnyRole(req.user!, EVENTS_ROLES);
const isAssignee = existing.assigneeUserId === req.user!.id;
const isCreator = existing.createdByUserId === req.user!.id;
if (!isAdmin && !isAssignee && !isCreator) {
throw new AppError(403, 'Insufficient permissions', 'FORBIDDEN');
}
const item = await actionItemsService.update(id, req.body);
res.json(item);
} catch (err) { next(err); }

View File

@ -171,6 +171,11 @@ router.post('/:id/import-csv', async (req, res, next) => {
res.status(400).json({ error: 'CSV text is required in the "csv" field' });
return;
}
const MAX_CSV_SIZE = 5 * 1024 * 1024; // 5MB
if (csv.length > MAX_CSV_SIZE) {
res.status(400).json({ error: { message: 'CSV too large (max 5MB)', code: 'CSV_TOO_LARGE' } });
return;
}
const result = await smsContactsService.importCsv(req.params.id as string, csv, filename);
res.json(result);
} catch (err) { next(err); }

View File

@ -39,6 +39,30 @@ async function requireEventPermission(req: Request, _res: Response, next: NextFu
return next({ status: 403, message: 'Insufficient permissions' });
}
/** Middleware: for :id routes, verify non-admin users own the event */
async function requireEventOwnership(req: Request, res: Response, next: NextFunction) {
const eventId = req.params.id as string;
if (!eventId) return next();
const userRoles = req.user!.roles || [req.user!.role];
const isAdmin = userRoles.some(r => EVENTS_ROLES.includes(r as UserRole));
if (isAdmin) return next();
const event = await prisma.ticketedEvent.findUnique({
where: { id: eventId },
select: { createdByUserId: true },
});
if (!event) {
res.status(404).json({ error: { message: 'Event not found', code: 'NOT_FOUND' } });
return;
}
if (event.createdByUserId !== req.user!.id) {
res.status(403).json({ error: { message: 'Forbidden', code: 'FORBIDDEN' } });
return;
}
next();
}
// All routes require auth + event permission
router.use(authenticate, requireEventPermission);
@ -73,7 +97,7 @@ router.post('/', validate(createEventSchema), async (req: Request, res: Response
});
// GET /:id — event detail
router.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
router.get('/:id', requireEventOwnership, async (req: Request, res: Response, next: NextFunction) => {
try {
const event = await ticketedEventsService.findById(req.params.id as string);
res.json(event);
@ -144,7 +168,7 @@ router.post('/:id/complete', requireRole(...EVENTS_ROLES), async (req: Request,
// --- Meeting ---
// POST /:id/meeting-token — generate moderator JWT for Jitsi
router.post('/:id/meeting-token', async (req: Request, res: Response, next: NextFunction) => {
router.post('/:id/meeting-token', requireEventOwnership, async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await prisma.user.findUnique({
where: { id: req.user!.id },
@ -159,7 +183,7 @@ router.post('/:id/meeting-token', async (req: Request, res: Response, next: Next
// --- Tiers ---
// POST /:id/tiers
router.post('/:id/tiers', validate(createTierSchema), async (req: Request, res: Response, next: NextFunction) => {
router.post('/:id/tiers', requireEventOwnership, validate(createTierSchema), async (req: Request, res: Response, next: NextFunction) => {
try {
const tier = await ticketedEventsService.addTier(req.params.id as string, req.body);
res.status(201).json(tier);
@ -167,7 +191,7 @@ router.post('/:id/tiers', validate(createTierSchema), async (req: Request, res:
});
// PUT /:id/tiers/:tierId
router.put('/:id/tiers/:tierId', validate(updateTierSchema), async (req: Request, res: Response, next: NextFunction) => {
router.put('/:id/tiers/:tierId', requireEventOwnership, validate(updateTierSchema), async (req: Request, res: Response, next: NextFunction) => {
try {
const tier = await ticketedEventsService.updateTier(
req.params.tierId as string,
@ -179,7 +203,7 @@ router.put('/:id/tiers/:tierId', validate(updateTierSchema), async (req: Request
});
// DELETE /:id/tiers/:tierId
router.delete('/:id/tiers/:tierId', async (req: Request, res: Response, next: NextFunction) => {
router.delete('/:id/tiers/:tierId', requireEventOwnership, async (req: Request, res: Response, next: NextFunction) => {
try {
await ticketedEventsService.deleteTier(req.params.tierId as string, req.params.id as string);
res.json({ success: true });
@ -189,7 +213,7 @@ router.delete('/:id/tiers/:tierId', async (req: Request, res: Response, next: Ne
// --- Tickets ---
// GET /:id/tickets
router.get('/:id/tickets', async (req: Request, res: Response, next: NextFunction) => {
router.get('/:id/tickets', requireEventOwnership, async (req: Request, res: Response, next: NextFunction) => {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
@ -201,7 +225,7 @@ router.get('/:id/tickets', async (req: Request, res: Response, next: NextFunctio
});
// GET /:id/checkins
router.get('/:id/checkins', async (req: Request, res: Response, next: NextFunction) => {
router.get('/:id/checkins', requireEventOwnership, async (req: Request, res: Response, next: NextFunction) => {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
@ -211,7 +235,7 @@ router.get('/:id/checkins', async (req: Request, res: Response, next: NextFuncti
});
// GET /:id/stats
router.get('/:id/stats', async (req: Request, res: Response, next: NextFunction) => {
router.get('/:id/stats', requireEventOwnership, async (req: Request, res: Response, next: NextFunction) => {
try {
const stats = await ticketedEventsService.getEventStats(req.params.id as string);
res.json(stats);
@ -219,7 +243,7 @@ router.get('/:id/stats', async (req: Request, res: Response, next: NextFunction)
});
// POST /:id/resend-ticket/:ticketId
router.post('/:id/resend-ticket/:ticketId', async (req: Request, res: Response, next: NextFunction) => {
router.post('/:id/resend-ticket/:ticketId', requireEventOwnership, async (req: Request, res: Response, next: NextFunction) => {
try {
const ticket = await prisma.ticket.findUnique({
where: { id: req.params.ticketId as string },
@ -273,7 +297,7 @@ router.post('/:id/resend-ticket/:ticketId', async (req: Request, res: Response,
});
// POST /:id/tickets/:ticketId/cancel
router.post('/:id/tickets/:ticketId/cancel', async (req: Request, res: Response, next: NextFunction) => {
router.post('/:id/tickets/:ticketId/cancel', requireEventOwnership, async (req: Request, res: Response, next: NextFunction) => {
try {
await ticketsService.cancelTicket(req.params.ticketId as string);
res.json({ success: true });

View File

@ -599,6 +599,51 @@ class GiteaClient {
{ name: tokenName, scopes: ['read', 'write'] as unknown as Record<string, unknown> } as unknown as Record<string, unknown>,
);
}
// --- Repository Collaborator Management ---
/**
* Add a collaborator to a repository with the specified permission level.
* @param permission - "read", "write", or "admin"
*/
async addCollaborator(
owner: string,
repo: string,
username: string,
permission: 'read' | 'write' | 'admin' = 'write',
): Promise<void> {
await this.request(
'PUT',
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
{ permission },
);
}
/**
* Remove a collaborator from a repository.
*/
async removeCollaborator(owner: string, repo: string, username: string): Promise<void> {
await this.request(
'DELETE',
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
);
}
/**
* Check if a user is a collaborator on a repository.
* Returns true if they are, false otherwise.
*/
async isCollaborator(owner: string, repo: string, username: string): Promise<boolean> {
try {
await this.request(
'GET',
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/collaborators/${encodeURIComponent(username)}`,
);
return true;
} catch {
return false;
}
}
}
export const giteaClient = new GiteaClient();

View File

@ -1,9 +1,10 @@
import { createHmac } from 'crypto';
import { Prisma } from '@prisma/client';
import { Prisma, UserRole } from '@prisma/client';
import { prisma } from '../../config/database';
import { env } from '../../config/env';
import { logger } from '../../utils/logger';
import { giteaClient } from '../gitea.client';
import { CONTENT_ROLES } from '../../utils/roles';
import type { ServiceProvisioner, ProvisionerConfig, ProvisionResult, CMUser } from './provisioner.interface';
const ROLE_MAP: Record<string, string[]> = {
@ -14,9 +15,13 @@ const ROLE_MAP: Record<string, string[]> = {
TEMP: [],
};
/** The private docs repo name created by gitea-setup */
const DOCS_REPO_NAME = 'changemaker.lite';
/** Deterministic password — never exposed to users */
function generateGiteaPassword(userId: string): string {
return createHmac('sha256', env.JWT_ACCESS_SECRET)
const salt = env.SERVICE_PASSWORD_SALT || env.JWT_ACCESS_SECRET;
return createHmac('sha256', salt)
.update(`gitea:${userId}`)
.digest('hex');
}
@ -94,6 +99,9 @@ class GiteaProvisioner implements ServiceProvisioner {
},
});
// Grant docs repo access based on role
await this.syncDocsRepoAccess(giteaUser.login, user.role);
return {
success: true,
serviceUserId: String(giteaUser.id),
@ -121,6 +129,9 @@ class GiteaProvisioner implements ServiceProvisioner {
admin: isAdmin,
active: user.status === 'ACTIVE',
});
// Re-evaluate docs repo access based on current role
await this.syncDocsRepoAccess(username, user.role);
}
async deactivate(serviceUserId: string): Promise<void> {
@ -140,13 +151,57 @@ class GiteaProvisioner implements ServiceProvisioner {
}
await giteaClient.adminUpdateUser(username, { active: false });
// Remove docs repo collaborator access
try {
const config = await giteaClient.getConfig();
if (config.repoOwner) {
await giteaClient.removeCollaborator(config.repoOwner, DOCS_REPO_NAME, username);
}
} catch {
// Ignore — user may not have been a collaborator
}
logger.info(`Gitea provisioner: deactivated user ${username}`);
}
async getAuthToken(_user: CMUser, _serviceUserId: string): Promise<string | null> {
// Gitea SSO via API tokens could be implemented here if needed for iframe embedding
return null;
}
/**
* Ensure the user's collaborator status on the private docs repo matches
* their role. CONTENT_ROLES (SUPER_ADMIN, CONTENT_ADMIN) get write access;
* SUPER_ADMIN users already have admin access via the Gitea admin flag,
* but we also add them as explicit collaborators for consistency.
* Users without CONTENT_ROLES are removed as collaborators.
*/
private async syncDocsRepoAccess(username: string, role: string): Promise<void> {
try {
const config = await giteaClient.getConfig();
const repoOwner = config.repoOwner;
if (!repoOwner) return; // Setup not complete — no repo owner yet
const hasDocsAccess = CONTENT_ROLES.includes(role as UserRole);
if (hasDocsAccess) {
// SUPER_ADMIN → admin, CONTENT_ADMIN → write
const permission = role === 'SUPER_ADMIN' ? 'admin' : 'write';
await giteaClient.addCollaborator(repoOwner, DOCS_REPO_NAME, username, permission);
logger.debug(`Gitea provisioner: granted ${permission} access to ${repoOwner}/${DOCS_REPO_NAME} for ${username}`);
} else {
// Remove access if user no longer has CONTENT_ROLES
const isCollab = await giteaClient.isCollaborator(repoOwner, DOCS_REPO_NAME, username);
if (isCollab) {
await giteaClient.removeCollaborator(repoOwner, DOCS_REPO_NAME, username);
logger.debug(`Gitea provisioner: removed ${username} from ${repoOwner}/${DOCS_REPO_NAME}`);
}
}
} catch (err) {
// Non-fatal — don't block provisioning if repo access sync fails
logger.warn(`Gitea provisioner: docs repo access sync failed for ${username}:`, err instanceof Error ? err.message : err);
}
}
}
export const giteaProvisioner = new GiteaProvisioner();

View File

@ -16,7 +16,8 @@ const ROLE_MAP: Record<string, string[]> = {
/** Deterministic password — never exposed to users, only used for RC internal auth */
function generateRCPassword(userId: string): string {
return createHmac('sha256', env.JWT_ACCESS_SECRET)
const salt = env.SERVICE_PASSWORD_SALT || env.JWT_ACCESS_SECRET;
return createHmac('sha256', salt)
.update(`rc:${userId}`)
.digest('hex');
}

View File

@ -746,6 +746,12 @@ services:
GITEA__attachment__MAX_SIZE: "1024"
GITEA__repository__MAX_CREATION_LIMIT: "-1"
GITEA__server__LFS_START_SERVER: "true"
# Reverse proxy auth — nginx injects X-WEBAUTH-USER for SSO
GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION: "true"
GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION: "false"
GITEA__service__ENABLE_REVERSE_PROXY_EMAIL: "false"
GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER: "X-WEBAUTH-USER"
GITEA__service__REQUIRE_SIGNIN_VIEW: "true"
volumes:
- {{containerPrefix}}-gitea-data:/data
networks:

View File

@ -35,19 +35,36 @@ server {
}
}
# Gitea embed proxy (internal 8883)
# Gitea embed proxy (internal 8883) — SSO via auth_request
server {
listen 8883;
client_max_body_size 2048M;
# Internal: validate SSO session cookie via API
location = /_auth {
internal;
set $upstream_api http://{{containerPrefix}}-api:4000;
proxy_pass $upstream_api/api/auth/gitea-sso-validate;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Cookie $http_cookie;
}
location / {
auth_request /_auth;
auth_request_set $gitea_user $upstream_http_x_gitea_user;
set $upstream_gitea http://{{containerPrefix}}-gitea:3000;
proxy_pass $upstream_gitea;
proxy_hide_header X-Frame-Options;
proxy_hide_header Content-Security-Policy;
add_header Content-Security-Policy "frame-ancestors 'self' localhost 127.0.0.1" always;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSO header — empty string if not authenticated (Gitea ignores it)
proxy_set_header X-WEBAUTH-USER $gitea_user;
}
}

View File

@ -32,7 +32,7 @@ services:
retries: 3
start_period: 30s
environment:
- NODE_ENV=${NODE_ENV:-development}
- NODE_ENV=${NODE_ENV:-production}
- PORT=4000
- LOG_DIR=/app/logs
- DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2}
@ -155,7 +155,7 @@ services:
retries: 3
start_period: 30s
environment:
- NODE_ENV=${NODE_ENV:-development}
- NODE_ENV=${NODE_ENV:-production}
- MEDIA_API_PORT=${MEDIA_API_PORT:-4100}
- DATABASE_URL=postgresql://${V2_POSTGRES_USER:-changemaker}:${V2_POSTGRES_PASSWORD:?V2_POSTGRES_PASSWORD must be set in .env}@changemaker-v2-postgres:5432/${V2_POSTGRES_DB:-changemaker_v2}
- REDIS_URL=redis://:${REDIS_PASSWORD}@redis-changemaker:6379
@ -204,7 +204,7 @@ services:
start_period: 20s
environment:
- DOMAIN=${DOMAIN:-cmlite.org}
- NODE_ENV=${NODE_ENV:-development}
- NODE_ENV=${NODE_ENV:-production}
- VITE_API_URL=http://changemaker-v2-api:4000
- VITE_MEDIA_API_URL=${VITE_MEDIA_API_URL:-http://changemaker-media-api:4100}
- VITE_MKDOCS_URL=http://mkdocs-changemaker:8000
@ -665,6 +665,12 @@ services:
- GITEA__server__LFS_MAX_FILE_SIZE=1024
- GITEA__repository__upload__FILE_MAX_SIZE=1024
- GITEA__repository__upload__MAX_FILES=1000
# Reverse proxy auth — nginx injects X-WEBAUTH-USER for SSO
- GITEA__service__ENABLE_REVERSE_PROXY_AUTHENTICATION=true
- GITEA__service__ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=false
- GITEA__service__ENABLE_REVERSE_PROXY_EMAIL=false
- GITEA__service__REVERSE_PROXY_AUTHENTICATION_HEADER=X-WEBAUTH-USER
- GITEA__service__REQUIRE_SIGNIN_VIEW=true
restart: unless-stopped
volumes:
- gitea-data:/data
@ -1191,7 +1197,7 @@ services:
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=${GRAFANA_ROOT_URL:-http://localhost:3001}
- GF_SECURITY_ALLOW_EMBEDDING=true
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ENABLED=false
- GF_AUTH_ANONYMOUS_ORG_ROLE=Viewer
volumes:
- grafana-data:/var/lib/grafana

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -9,10 +9,17 @@
"assets/images/social/blog/2025/08/01/3.png": "332b80224e75bda48c92439ad6354e7ffcab52e1",
"assets/images/social/blog/2025/09/24/4.png": "840234a707ac182ce6b89203c658b312d03df58e",
"assets/images/social/blog/2026/03/22/introducing-changemaker-lite-v2.png": "c79afe23671a3a829e74debe1f3dc23f21d7100f",
"assets/images/social/blog/2026/03/27/test-blog-post---version-3.png": "7fce7e67d83b63b507be00e7ded540dfa7bd0ed5",
"assets/images/social/blog/2026/03/27/test-blog-post---version-4.png": "98cffc754c5be0c9b153444df46ed5b8b4350290",
"assets/images/social/blog/2026/03/27/test-blog-post---version-5.png": "74ac268b9766c50b397e5b02513661d6abe8ebaf",
"assets/images/social/blog/2026/03/27/test-blog-post---version-6.png": "c6d68bac2cdcd6b8c52256df35d542be64de7e0e",
"assets/images/social/blog/2026/03/27/test-blog-post---version-7.png": "f6cc4e78350bbf53560dbc4f119596592fe597c1",
"assets/images/social/blog/2026/03/27/test-blog-post.png": "fd4dfc0ab942b7d648409d37b344d50e348150c7",
"assets/images/social/blog/archive/2025.png": "08cbed159d450158ab4d79807f37adcda08bce39",
"assets/images/social/blog/archive/2026.png": "e13e604d5c2f61d08e8d14b4442ac65c268424b8",
"assets/images/social/blog/category/announcements.png": "096d6368b08dfd41a79a91e7e0fbfdc3bfd32a1b",
"assets/images/social/blog/category/platform.png": "036ba6414ed31d34d59423289658a3accefa76c0",
"assets/images/social/blog/category/testing.png": "53d1c9283a3557e3213d9c8cc28a2aa6d6824ce5",
"assets/images/social/blog/index.png": "b6fe388cb026572dfa0f42b9a6833e9fa9f95b9e",
"assets/images/social/build/index.png": "30e75040da55694ca6485d51a35fb4d19a26b408",
"assets/images/social/build/influence.png": "2961db1abb5f36d53086bf2bde9dfe19f1487809",
@ -151,7 +158,7 @@
"assets/images/social/services/static-server.png": "f36d527c80adba4bcb7778784683f429acb4ce74",
"assets/images/social/test-2.png": "a6ae43d52d7c58fc106a562777e03b7da2263f83",
"assets/images/social/test-page.png": "c9d5751a1f0a4c1341336bb7d00c9bc743d33ef4",
"assets/images/social/test.png": "a6ae43d52d7c58fc106a562777e03b7da2263f83",
"assets/images/social/test.png": "18d63169d742e6321dc4bb2988b8c5de61e79a28",
"assets/images/social/testing.png": "f7aaf394b71cbe7084a6afa0e75a324ca59e23d8",
"assets/images/social/v1/adv/ansible.png": "cb542ad9a3cc9a869258b3b1353966e1b9616a2b",
"assets/images/social/v1/adv/index.png": "faa3ec092003114c031995ba6258c4d43f4262a4",

View File

@ -1,305 +0,0 @@
# Documentation Next Steps — Editorial & Material Theme Enhancement Plan
**Date:** 2026-03-22
**Branch:** v2
**Status:** Planning → Execution
---
## Executive Summary
The MkDocs documentation site is comprehensive (70+ pages, all with proper frontmatter, no stubs) and already uses many Material theme features well (grid cards, admonitions, code copy, mermaid diagrams, social cards, blog plugin, dark/light toggle). This plan focuses on **activating dormant Material theme capabilities** and **editorial polish** to take the docs from "complete" to "professional-grade."
---
## Step 1: mkdocs.yml Configuration Hardening
**Goal:** Enable Material theme features that are available but not configured.
### 1a. Add missing theme features
```yaml
features:
# Already enabled (keep):
- announce.dismiss
- content.action.edit
- content.action.view
- content.code.annotate
- content.code.copy
- content.tooltips
- navigation.footer
- navigation.indexes
- navigation.path
- navigation.prune
- navigation.tabs
- navigation.tabs.sticky
- navigation.top
- navigation.tracking
- search.highlight
- search.share
- search.suggest
- toc.follow
# ADD these:
- navigation.instant # SPA-like navigation (no full page reload)
- navigation.instant.prefetch # Prefetch pages on hover
- navigation.instant.progress # Show loading progress bar
- content.code.select # Line selection in code blocks
- content.tabs.link # Linked content tabs (sync across page)
```
### 1b. Fix consent banner
The copyright references `#__consent` but no consent config exists. Add:
```yaml
extra:
consent:
title: Cookie consent
description: >
We use cookies to recognize your repeated visits and preferences,
as well as to measure the effectiveness of our documentation.
With your consent, you help us improve.
actions:
- accept
- reject
- manage
```
### 1c. Fix copyright year
Change `2024``20242026` in the copyright line.
### 1d. Fix edit_uri branch
Change `edit_uri: src/branch/main/mkdocs/docs``edit_uri: src/branch/v2/mkdocs/docs`
### 1e. Add abbreviations snippets
Create `docs/includes/abbreviations.md` and reference via snippets:
```yaml
markdown_extensions:
- pymdownx.snippets:
auto_append:
- includes/abbreviations.md
```
Common abbreviations: API, JWT, RBAC, CSV, ORM, SMTP, CORS, SSL, TLS, DNS, CRUD, SSO, SPA, CLI, GUI, QR, GPS, GDPR, CDN, VPS, CGNAT, NAR, CRM, OG, DDoS, SSE, UUID, FOSS, HSTS, CSP
### 1f. Add privacy plugin (optional, external resource proxying)
```yaml
plugins:
- privacy
```
---
## Step 2: Page-Level Metadata Enhancement
**Goal:** Add `tags`, `status`, and `search.boost` metadata to every page.
### 2a. Tags
The tags plugin is loaded but zero pages use tags. Add contextually appropriate tags to every page. Example tag taxonomy:
- **Audience:** `admin`, `volunteer`, `user`, `operator`, `developer`
- **Module:** `influence`, `map`, `media`, `payments`, `broadcast`, `social`
- **Type:** `guide`, `reference`, `tutorial`, `concept`, `troubleshooting`
- **Feature:** `campaigns`, `canvassing`, `shifts`, `gallery`, `landing-pages`, `newsletter`, `sms`, `chat`, `events`
### 2b. Status badges
Material supports `status: new` and `status: deprecated` on pages, shown as badges in the nav sidebar. Apply:
- `status: new` — Recent features (Gallery Ads, People CRM, Achievements, Social Calendar, CrowdSec, SMS, Docs Comments, Payments)
- `status: deprecated` — Legacy/archival content if any
Requires adding to `extra:` in mkdocs.yml:
```yaml
extra:
status:
new: Recently added
deprecated: Legacy
```
### 2c. Search boost
Boost important entry-point pages so they rank higher:
```yaml
---
search:
boost: 2
---
```
Apply to: Getting Started index, Installation, First Steps, Features at a Glance, FAQ/Troubleshooting.
---
## Step 3: Abbreviations Glossary
**Goal:** Create a shared abbreviations file so that hovering over acronyms shows their meaning.
Create `mkdocs/docs/includes/abbreviations.md`:
```markdown
*[API]: Application Programming Interface
*[JWT]: JSON Web Token
*[RBAC]: Role-Based Access Control
*[CORS]: Cross-Origin Resource Sharing
*[SMTP]: Simple Mail Transfer Protocol
*[CSV]: Comma-Separated Values
*[ORM]: Object-Relational Mapping
*[SSL]: Secure Sockets Layer
*[TLS]: Transport Layer Security
*[DNS]: Domain Name System
*[CRUD]: Create, Read, Update, Delete
*[SSO]: Single Sign-On
*[SPA]: Single Page Application
*[CLI]: Command Line Interface
*[GUI]: Graphical User Interface
*[QR]: Quick Response (code)
*[GPS]: Global Positioning System
*[GDPR]: General Data Protection Regulation
*[CDN]: Content Delivery Network
*[VPS]: Virtual Private Server
*[CGNAT]: Carrier-Grade Network Address Translation
*[NAR]: National Address Register
*[CRM]: Customer Relationship Management
*[OG]: Open Graph
*[DDoS]: Distributed Denial of Service
*[SSE]: Server-Sent Events
*[UUID]: Universally Unique Identifier
*[FOSS]: Free and Open Source Software
*[HSTS]: HTTP Strict Transport Security
*[CSP]: Content Security Policy
*[BullMQ]: Bull Message Queue
*[FFprobe]: FFmpeg Probe (media metadata tool)
*[FFmpeg]: Fast Forward Moving Picture Experts Group
*[XMPP]: Extensible Messaging and Presence Protocol
```
---
## Step 4: Custom 404 Page
**Goal:** Branded 404 page instead of browser default.
Create `mkdocs/docs/404.md`:
```markdown
---
template: main.html
title: Page Not Found
hide:
- navigation
- toc
- footer
search:
exclude: true
---
# Page Not Found
The page you're looking for doesn't exist or has been moved.
[Go to Documentation Home](docs/index.md){ .md-button .md-button--primary }
[Search](docs/index.md){ .md-button }
```
---
## Step 5: Content Fixes — Broken Links & Stale Warnings
**Goal:** Fix all broken links, placeholder content, and stale warnings.
### 5a. Fix broken "Coming soon" links
- `docs/index.md`: Monitoring card links to `../blog/index.md` → link to `admin/services/monitoring.md`
- `docs/index.md`: Contributing card links to `../blog/index.md` → create a contributing stub or remove placeholder
### 5b. Fix Architecture page
Remove "Under Construction" admonition. Flesh out with:
- Mermaid system diagram
- Database entity relationship summary
- Authentication flow (already started)
- Request lifecycle
### 5c. Fix Troubleshooting page
Remove "Under Construction" admonition. Add more entries from CLAUDE.md and production experience.
### 5d. Fix Admin Guide roles table
Only lists 5 roles but there are 11. Add missing roles:
BROADCAST_ADMIN, CONTENT_ADMIN, MEDIA_ADMIN, PAYMENTS_ADMIN, EVENTS_ADMIN, SOCIAL_ADMIN
### 5e. Social icons semantic fix
GitHub icon links to Gitea — change icon to `fontawesome/brands/gitea` or `simple/gitea` if available, or use a generic `fontawesome/solid/code-branch`.
---
## Step 6: Cleanup Test/Orphan Files
**Goal:** Remove files that shouldn't be in the docs directory.
- `docs/test.md` (41KB test file)
- `docs/test-page.md`
- `docs/testing.md`
- `docs/lander.md` (override template pointer — keep if used by index.md)
- `docs/main.md` (override template pointer — keep if used)
Verify `lander.md` and `main.md` are still needed by checking if any page uses `template: lander.html` or `template: main.html`.
---
## Step 7: Announcement Bar
**Goal:** Use Material's announcement bar for version/status info.
Create `mkdocs/docs/overrides/main.html` addition (or modify existing) with announcement block:
```html
{% block announce %}
Changemaker Lite v2 — <a href="docs/getting-started/index.md">Get started in 5 minutes</a>
{% endblock %}
```
Note: The existing `main.html` override is 35KB — check if it already has an announce block before modifying.
---
## Step 8: Blog Seeding
**Goal:** Create 12 initial blog posts so the blog section isn't empty.
Suggested posts:
1. **"Introducing Changemaker Lite v2"** — Overview of the rebuild, what's new, philosophy
2. **"Why Self-Hosted Campaign Tools Matter in 2026"** — Draws from the Philosophy page
---
## Step 9: Screenshot Audit
**Goal:** Verify all `![...](../../assets/images/screenshots/...)` references resolve to actual files. Take new screenshots with Playwright where missing or outdated.
Pages referencing screenshots:
- Getting Started index (dashboard.png)
- First Steps (login.png, dashboard.png, settings-organization.png, campaigns.png, locations.png, shifts.png)
- Dashboard (dashboard.png)
- People & Access (users.png)
- Settings (settings.png)
- Campaigns (campaigns.png)
---
## Priority Order
| Priority | Step | Impact | Effort |
|----------|------|--------|--------|
| **P0** | Step 1 (mkdocs.yml fixes) | High — broken consent, wrong branch, missing SPA nav | Low |
| **P0** | Step 5 (broken links/content) | High — broken UX | Medium |
| **P0** | Step 6 (cleanup test files) | Medium — professional appearance | Low |
| **P1** | Step 3 (abbreviations) | Medium — reader comprehension | Low |
| **P1** | Step 4 (404 page) | Medium — UX polish | Low |
| **P1** | Step 2 (tags/status/boost) | Medium — discoverability | Medium-High |
| **P2** | Step 7 (announcement bar) | Low-Medium — branding | Low |
| **P2** | Step 8 (blog seeding) | Low — completeness | Medium |
| **P2** | Step 9 (screenshot audit) | Medium — visual credibility | High |
---
## Material Theme Reference
Key Material docs for implementers:
- [Setting up tags](https://squidfunk.github.io/mkdocs-material/setup/setting-up-tags/)
- [Setting up navigation](https://squidfunk.github.io/mkdocs-material/setup/setting-up-navigation/)
- [Page status](https://squidfunk.github.io/mkdocs-material/reference/index/#setting-the-page-status)
- [Search boosting](https://squidfunk.github.io/mkdocs-material/setup/setting-up-site-search/#search-boosting)
- [Abbreviations](https://squidfunk.github.io/mkdocs-material/reference/tooltips/#adding-abbreviations)
- [Cookie consent](https://squidfunk.github.io/mkdocs-material/setup/ensuring-data-privacy/#cookie-consent)
- [Custom 404](https://www.mkdocs.org/user-guide/custom-themes/#custom-theme)
- [Announcement bar](https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-header/#announcement-bar)

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

View File

@ -7,10 +7,10 @@
"stars_count": 0,
"forks_count": 0,
"open_issues_count": 0,
"updated_at": "2026-03-25T20:11:01-06:00",
"updated_at": "2026-03-30T11:54:37-06:00",
"created_at": "2025-05-28T14:54:59-06:00",
"clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git",
"ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git",
"default_branch": "main",
"last_build_update": "2026-03-25T20:11:01-06:00"
"last_build_update": "2026-03-30T11:54:37-06:00"
}

View File

@ -4,13 +4,13 @@
"description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.",
"html_url": "https://github.com/anthropics/claude-code",
"language": "Shell",
"stars_count": 82863,
"forks_count": 6945,
"open_issues_count": 7902,
"updated_at": "2026-03-26T05:46:26Z",
"stars_count": 88332,
"forks_count": 9745,
"open_issues_count": 8343,
"updated_at": "2026-03-31T15:50:05Z",
"created_at": "2025-02-22T17:41:21Z",
"clone_url": "https://github.com/anthropics/claude-code.git",
"ssh_url": "git@github.com:anthropics/claude-code.git",
"default_branch": "main",
"last_build_update": "2026-03-26T00:31:05Z"
"last_build_update": "2026-03-31T14:35:57Z"
}

View File

@ -4,13 +4,13 @@
"description": "VS Code in the browser",
"html_url": "https://github.com/coder/code-server",
"language": "TypeScript",
"stars_count": 76836,
"forks_count": 6567,
"open_issues_count": 166,
"updated_at": "2026-03-26T04:51:01Z",
"stars_count": 76910,
"forks_count": 6575,
"open_issues_count": 172,
"updated_at": "2026-03-31T14:49:15Z",
"created_at": "2019-02-27T16:50:41Z",
"clone_url": "https://github.com/coder/code-server.git",
"ssh_url": "git@github.com:coder/code-server.git",
"default_branch": "main",
"last_build_update": "2026-03-25T23:42:46Z"
"last_build_update": "2026-03-31T00:00:24Z"
}

View File

@ -4,13 +4,13 @@
"description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.",
"html_url": "https://github.com/gethomepage/homepage",
"language": "JavaScript",
"stars_count": 29142,
"forks_count": 1831,
"open_issues_count": 2,
"updated_at": "2026-03-26T04:32:12Z",
"stars_count": 29249,
"forks_count": 1833,
"open_issues_count": 0,
"updated_at": "2026-03-31T14:45:22Z",
"created_at": "2022-08-24T07:29:42Z",
"clone_url": "https://github.com/gethomepage/homepage.git",
"ssh_url": "git@github.com:gethomepage/homepage.git",
"default_branch": "dev",
"last_build_update": "2026-03-26T04:09:25Z"
"last_build_update": "2026-03-31T14:36:10Z"
}

View File

@ -4,13 +4,13 @@
"description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD",
"html_url": "https://github.com/go-gitea/gitea",
"language": "Go",
"stars_count": 54497,
"forks_count": 6492,
"open_issues_count": 2870,
"updated_at": "2026-03-26T05:41:32Z",
"stars_count": 54629,
"forks_count": 6518,
"open_issues_count": 2866,
"updated_at": "2026-03-31T15:12:57Z",
"created_at": "2016-11-01T02:13:26Z",
"clone_url": "https://github.com/go-gitea/gitea.git",
"ssh_url": "git@github.com:go-gitea/gitea.git",
"default_branch": "main",
"last_build_update": "2026-03-26T00:53:32Z"
"last_build_update": "2026-03-31T15:40:43Z"
}

View File

@ -4,13 +4,13 @@
"description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.",
"html_url": "https://github.com/knadh/listmonk",
"language": "Go",
"stars_count": 19343,
"forks_count": 1965,
"open_issues_count": 103,
"updated_at": "2026-03-26T04:23:08Z",
"stars_count": 19396,
"forks_count": 1977,
"open_issues_count": 91,
"updated_at": "2026-03-31T14:23:10Z",
"created_at": "2019-06-26T05:08:39Z",
"clone_url": "https://github.com/knadh/listmonk.git",
"ssh_url": "git@github.com:knadh/listmonk.git",
"default_branch": "master",
"last_build_update": "2026-03-26T04:23:38Z"
"last_build_update": "2026-03-31T05:18:48Z"
}

View File

@ -4,13 +4,13 @@
"description": "Create & scan cute qr codes easily \ud83d\udc7e",
"html_url": "https://github.com/lyqht/mini-qr",
"language": "Vue",
"stars_count": 1931,
"forks_count": 244,
"open_issues_count": 21,
"updated_at": "2026-03-26T02:38:23Z",
"stars_count": 1938,
"forks_count": 245,
"open_issues_count": 23,
"updated_at": "2026-03-31T12:32:17Z",
"created_at": "2023-04-21T14:20:14Z",
"clone_url": "https://github.com/lyqht/mini-qr.git",
"ssh_url": "git@github.com:lyqht/mini-qr.git",
"default_branch": "main",
"last_build_update": "2026-03-13T12:48:04Z"
"last_build_update": "2026-03-31T12:43:07Z"
}

View File

@ -4,13 +4,13 @@
"description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.",
"html_url": "https://github.com/n8n-io/n8n",
"language": "TypeScript",
"stars_count": 181103,
"forks_count": 56170,
"open_issues_count": 1416,
"updated_at": "2026-03-26T05:48:22Z",
"stars_count": 181869,
"forks_count": 56348,
"open_issues_count": 1447,
"updated_at": "2026-03-31T15:46:32Z",
"created_at": "2019-06-22T09:24:21Z",
"clone_url": "https://github.com/n8n-io/n8n.git",
"ssh_url": "git@github.com:n8n-io/n8n.git",
"default_branch": "master",
"last_build_update": "2026-03-26T05:30:58Z"
"last_build_update": "2026-03-31T15:47:22Z"
}

View File

@ -4,13 +4,13 @@
"description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative",
"html_url": "https://github.com/nocodb/nocodb",
"language": "TypeScript",
"stars_count": 62543,
"forks_count": 4681,
"open_issues_count": 658,
"updated_at": "2026-03-26T05:48:04Z",
"stars_count": 62566,
"forks_count": 4703,
"open_issues_count": 665,
"updated_at": "2026-03-31T15:24:55Z",
"created_at": "2017-10-29T18:51:48Z",
"clone_url": "https://github.com/nocodb/nocodb.git",
"ssh_url": "git@github.com:nocodb/nocodb.git",
"default_branch": "develop",
"last_build_update": "2026-03-26T05:48:41Z"
"last_build_update": "2026-03-31T15:24:48Z"
}

View File

@ -4,13 +4,13 @@
"description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.",
"html_url": "https://github.com/ollama/ollama",
"language": "Go",
"stars_count": 166179,
"forks_count": 15178,
"open_issues_count": 2726,
"updated_at": "2026-03-26T05:38:58Z",
"stars_count": 166587,
"forks_count": 15255,
"open_issues_count": 2778,
"updated_at": "2026-03-31T15:34:55Z",
"created_at": "2023-06-26T19:39:32Z",
"clone_url": "https://github.com/ollama/ollama.git",
"ssh_url": "git@github.com:ollama/ollama.git",
"default_branch": "main",
"last_build_update": "2026-03-26T02:01:29Z"
"last_build_update": "2026-03-31T15:11:36Z"
}

View File

@ -4,13 +4,13 @@
"description": "Documentation that simply works",
"html_url": "https://github.com/squidfunk/mkdocs-material",
"language": "Python",
"stars_count": 26394,
"forks_count": 4060,
"open_issues_count": 2,
"updated_at": "2026-03-26T02:34:14Z",
"stars_count": 26430,
"forks_count": 4062,
"open_issues_count": 1,
"updated_at": "2026-03-31T14:42:16Z",
"created_at": "2016-01-28T22:09:23Z",
"clone_url": "https://github.com/squidfunk/mkdocs-material.git",
"ssh_url": "git@github.com:squidfunk/mkdocs-material.git",
"default_branch": "master",
"last_build_update": "2026-03-25T22:14:34Z"
"last_build_update": "2026-03-27T10:24:49Z"
}

View File

@ -0,0 +1,12 @@
---
date: 2026-03-27
authors:
- admin
categories:
- Testing
draft: false
---
# Test Blog Post - Version 7
This version uses the auto-setup token.

View File

@ -55,7 +55,7 @@ Read more in our [Philosophy](../../docs/phil.md) page.
## Get Started
```bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
```
Or follow the [Getting Started guide](../../docs/getting-started/index.md) for a walkthrough.

View File

@ -530,7 +530,7 @@ Pre-configured alerts in `configs/prometheus/alerts.yml`:
```bash
# Pull latest code
git pull origin v2
git pull origin main
# Rebuild and restart containers
docker compose build api admin

View File

@ -30,7 +30,7 @@ This guide walks you through installing Changemaker Lite, running your first dep
The fastest way to deploy — no source code, no compilation:
```bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
```
This downloads a lightweight release package (~2 MB), runs the configuration wizard, and pulls pre-built Docker images. First startup takes ~2 minutes. See [Installation](installation.md#pre-built-image-installation) for details.
@ -42,7 +42,6 @@ For development or customization, clone the full repository:
```bash
git clone https://gitea.bnkops.com/admin/changemaker.lite
cd changemaker.lite
git checkout v2
```
```bash

View File

@ -34,7 +34,6 @@ Clone the repository:
```bash
git clone https://gitea.bnkops.com/admin/changemaker.lite
cd changemaker.lite
git checkout v2
```
Run the configuration wizard:
@ -63,7 +62,7 @@ For production deployments, you can skip cloning the source repository entirely.
### One-Line Install
```bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh | bash
curl -fsSL https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh | bash
```
This script:

View File

@ -61,6 +61,10 @@ sudo systemctl status changemaker-upgrade.path
2. Click the **System** tab
3. Click **Check for Updates**
The System tab shows your current version, last commit message, and auto-upgrade settings:
![System tab initial state](../../assets/images/screenshots/getting-started/upgrade-01-system-tab-initial.png)
The system fetches from the git remote and shows:
- Current commit hash and message
@ -68,6 +72,10 @@ The system fetches from the git remote and shows:
- Number of commits behind
- Changelog of incoming changes
When updates are available, the panel highlights how many commits are behind and lists the incoming changes:
![Update available notification](../../assets/images/screenshots/getting-started/upgrade-02-update-available.png)
### Starting an Upgrade
1. Review the changelog to understand what's changing
@ -79,8 +87,19 @@ The system fetches from the git remote and shows:
- **Dry run** — preview what would happen without making changes
4. Monitor the 6-phase progress indicator
![Confirm System Upgrade dialog](../../assets/images/screenshots/getting-started/upgrade-03-confirm-dialog.png)
The GUI polls for progress updates and displays the current phase, percentage, and status message in real time.
### Upgrade Results
After the upgrade completes, the System tab shows the result — including the new version, health check status, and any warnings:
![Upgrade success result](../../assets/images/screenshots/getting-started/upgrade-04-success-result.png)
!!! tip
If health checks show warnings immediately after an upgrade, wait 1-2 minutes for services to fully start before investigating.
---
## The 6 Upgrade Phases

View File

@ -5,3 +5,9 @@ hide:
- toc
title: "Test Page"
---
Testing
testing testing one two
hello is this content going to show?

3
mkdocs/docs/test.md Normal file
View File

@ -0,0 +1,3 @@
# test
Hello!

View File

@ -9,7 +9,7 @@ use_directory_urls: true
# Repository
repo_url: https://gitea.bnkops.com/admin/changemaker.lite
repo_name: changemaker.lite
edit_uri: src/branch/v2/mkdocs/docs
edit_uri: src/branch/main/mkdocs/docs
# Theme
theme:

View File

@ -1522,6 +1522,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="/assets/js/scheduling-poll.js"></script>
<script src="/assets/js/straw-poll-widget.js"></script>
<script src="/javascripts/ad-widgets.js"></script>
<script src="/javascripts/docs-comments.js"></script>

View File

@ -1397,7 +1397,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/404.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/404.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -1406,7 +1406,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/404.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/404.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -1598,6 +1598,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../assets/js/scheduling-poll.js"></script>
<script src="../assets/js/straw-poll-widget.js"></script>
<script src="../javascripts/ad-widgets.js"></script>
<script src="../javascripts/docs-comments.js"></script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -0,0 +1,226 @@
/**
* Straw Poll Widget Hydration for MkDocs
*
* Supports two modes:
* .straw-poll-inline Full voting UI embedded in docs page
* .straw-poll-card Preview card linking to the full poll lander
*
* Uses the lightweight /api/straw-polls/widget/:slug endpoint (cached).
* Follows the scheduling-poll.js hydration pattern.
*/
(function () {
'use strict';
function getApiUrl() {
if (window.PAYMENT_API_URL) return window.PAYMENT_API_URL;
if (window.API_URL) return window.API_URL;
var host = window.location.hostname;
if (host !== 'localhost' && host.indexOf('.') !== -1) {
var parts = host.split('.');
var base = parts.slice(-2).join('.');
return window.location.protocol + '//api.' + base;
}
return 'http://localhost:4000';
}
function getAppUrl() {
if (window.APP_URL) return window.APP_URL;
var host = window.location.hostname;
if (host !== 'localhost' && host.indexOf('.') !== -1) {
var parts = host.split('.');
var base = parts.slice(-2).join('.');
return window.location.protocol + '//app.' + base;
}
return 'http://localhost:3000';
}
var COLORS = ['#1890ff', '#52c41a', '#faad14', '#ff4d4f', '#722ed1', '#13c2c2', '#eb2f96'];
var YNA_COLORS = { Yes: '#52c41a', No: '#ff4d4f', Abstain: '#8c8c8c' };
function tokenKey(slug) {
return 'straw_poll_voter_token_' + slug;
}
// ===== Card Mode =====
function renderCard(block, poll, appUrl) {
var html = '<div style="border:1px solid rgba(255,255,255,0.15); border-radius:8px; padding:16px; max-width:400px; margin:12px auto;">';
html += '<div style="font-size:11px; text-transform:uppercase; opacity:0.5; margin-bottom:6px;">';
html += (poll.type === 'YES_NO_ABSTAIN' ? 'Yes / No / Abstain' : 'Single Choice') + ' Poll</div>';
html += '<h3 style="margin:0 0 8px; font-size:1.1rem;">' + poll.title + '</h3>';
html += '<div style="font-size:13px; opacity:0.65; margin-bottom:12px;">' + poll.totalVotes + ' vote' + (poll.totalVotes !== 1 ? 's' : '') + '</div>';
if (poll.status === 'ACTIVE') {
html += '<a href="' + appUrl + '/straw-poll/' + encodeURIComponent(poll.slug) + '" target="_blank" rel="noopener noreferrer" ';
html += 'style="display:inline-block; padding:10px 24px; background:#1890ff; color:#fff; text-decoration:none; border-radius:6px; font-weight:600; font-size:14px;">';
html += 'Vote Now &rarr;</a>';
} else {
html += '<span style="padding:3px 10px; border-radius:4px; font-size:12px; background:rgba(250,140,22,0.15); color:#fa8c16;">Closed</span>';
}
html += '</div>';
block.innerHTML = html;
}
// ===== Inline Mode =====
function renderInline(block, poll, apiUrl) {
var slug = poll.slug;
var storedToken = localStorage.getItem(tokenKey(slug));
var hasVoted = !!storedToken;
var showResults = poll.totalVotes > 0;
var html = '<div style="max-width:500px; margin:12px auto; border:1px solid rgba(255,255,255,0.15); border-radius:8px; padding:20px;">';
// Title
html += '<h3 style="margin:0 0 4px; font-size:1.1rem;">' + poll.title + '</h3>';
html += '<div style="font-size:11px; opacity:0.5; margin-bottom:14px;">';
html += (poll.type === 'YES_NO_ABSTAIN' ? 'Yes / No / Abstain' : 'Single Choice');
html += ' &middot; ' + poll.totalVotes + ' vote' + (poll.totalVotes !== 1 ? 's' : '') + '</div>';
if (poll.status === 'ACTIVE' && !hasVoted) {
// Vote form
html += '<div id="sp-vote-form-' + slug + '">';
poll.options.forEach(function (opt, i) {
var isYNA = poll.type === 'YES_NO_ABSTAIN';
var color = isYNA ? (YNA_COLORS[opt.label] || COLORS[i]) : COLORS[i % COLORS.length];
html += '<button class="sp-opt-btn" data-option-id="' + opt.id + '" style="display:block; width:100%; padding:10px 14px; margin-bottom:6px; ';
html += 'border:2px solid ' + color + '44; border-radius:6px; background:transparent; color:inherit; cursor:pointer; text-align:left; font-size:14px; transition:all 0.2s;" ';
html += 'onmouseover="this.style.background=\'' + color + '22\'" onmouseout="this.style.background=\'transparent\'">';
html += opt.label + '</button>';
});
html += '<input type="text" id="sp-voter-name-' + slug + '" placeholder="Your name (optional)" style="width:100%; padding:8px 12px; margin:8px 0; border:1px solid rgba(255,255,255,0.2); border-radius:4px; background:transparent; color:inherit; font-size:13px;" />';
html += '<button id="sp-submit-' + slug + '" disabled style="display:block; width:100%; padding:10px; border:none; border-radius:6px; background:#1890ff; color:#fff; font-weight:600; font-size:14px; cursor:pointer; opacity:0.5;">';
html += 'Submit Vote</button>';
html += '</div>';
}
// Results
if (showResults && (hasVoted || poll.status !== 'ACTIVE')) {
html += renderResultsHtml(poll);
}
if (hasVoted && poll.status === 'ACTIVE') {
html += '<div style="text-align:center; padding:8px; margin-top:8px; font-size:13px; color:#52c41a;">&#10003; You\'ve voted</div>';
}
html += '</div>';
block.innerHTML = html;
// Wire up vote form
if (poll.status === 'ACTIVE' && !hasVoted) {
wireVoteForm(block, poll, apiUrl, slug);
}
}
function renderResultsHtml(poll) {
var html = '<div style="margin-top:12px;">';
poll.options.forEach(function (opt, i) {
var pct = poll.totalVotes > 0 ? Math.round((opt.voteCount / poll.totalVotes) * 100) : 0;
var color = poll.type === 'YES_NO_ABSTAIN' ? (YNA_COLORS[opt.label] || COLORS[i]) : COLORS[i % COLORS.length];
html += '<div style="margin-bottom:8px;">';
html += '<div style="display:flex; justify-content:space-between; font-size:13px; margin-bottom:2px;">';
html += '<span>' + opt.label + '</span><span style="opacity:0.65;">' + opt.voteCount + ' (' + pct + '%)</span></div>';
html += '<div style="height:6px; background:rgba(255,255,255,0.1); border-radius:3px; overflow:hidden;">';
html += '<div style="height:100%; width:' + pct + '%; background:' + color + '; border-radius:3px; transition:width 0.3s;"></div>';
html += '</div></div>';
});
html += '</div>';
return html;
}
function wireVoteForm(block, poll, apiUrl, slug) {
var selectedId = null;
var buttons = block.querySelectorAll('.sp-opt-btn');
var submitBtn = block.querySelector('#sp-submit-' + slug);
buttons.forEach(function (btn) {
btn.addEventListener('click', function () {
selectedId = btn.getAttribute('data-option-id');
buttons.forEach(function (b) { b.style.borderWidth = '2px'; b.style.fontWeight = 'normal'; });
btn.style.borderWidth = '3px';
btn.style.fontWeight = '600';
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
});
});
submitBtn.addEventListener('click', function () {
if (!selectedId) return;
submitBtn.disabled = true;
submitBtn.textContent = 'Submitting...';
var voterName = (block.querySelector('#sp-voter-name-' + slug) || {}).value || '';
var body = { optionId: selectedId };
if (voterName) body.voterName = voterName;
var storedToken = localStorage.getItem(tokenKey(slug));
if (storedToken) body.voterToken = storedToken;
fetch(apiUrl + '/api/straw-polls/public/' + encodeURIComponent(slug) + '/vote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
.then(function (res) { return res.json(); })
.then(function (data) {
if (data.voterToken) localStorage.setItem(tokenKey(slug), data.voterToken);
// Re-fetch and re-render with results
return fetch(apiUrl + '/api/straw-polls/widget/' + encodeURIComponent(slug)).then(function (r) { return r.json(); });
})
.then(function (updated) {
renderInline(block, updated, apiUrl);
})
.catch(function () {
submitBtn.textContent = 'Error — try again';
submitBtn.disabled = false;
submitBtn.style.opacity = '1';
});
});
}
// ===== Hydration =====
function hydrateBlocks() {
var apiUrl = getApiUrl();
var appUrl = getAppUrl();
// Inline embeds
document.querySelectorAll('.straw-poll-inline').forEach(function (block) {
if (block.getAttribute('data-hydrated') === 'true') return;
var slug = block.getAttribute('data-poll-slug');
if (!slug) return;
block.setAttribute('data-hydrated', 'true');
block.innerHTML = '<div style="text-align:center; padding:16px; opacity:0.5;">Loading poll...</div>';
fetch(apiUrl + '/api/straw-polls/widget/' + encodeURIComponent(slug))
.then(function (res) { if (!res.ok) throw new Error(); return res.json(); })
.then(function (poll) { renderInline(block, poll, apiUrl); })
.catch(function () {
block.innerHTML = '<div style="text-align:center; padding:16px; opacity:0.5;">Poll unavailable</div>';
});
});
// Card links
document.querySelectorAll('.straw-poll-card').forEach(function (block) {
if (block.getAttribute('data-hydrated') === 'true') return;
var slug = block.getAttribute('data-poll-slug');
if (!slug) return;
block.setAttribute('data-hydrated', 'true');
block.innerHTML = '<div style="text-align:center; padding:16px; opacity:0.5;">Loading...</div>';
fetch(apiUrl + '/api/straw-polls/widget/' + encodeURIComponent(slug))
.then(function (res) { if (!res.ok) throw new Error(); return res.json(); })
.then(function (poll) { renderCard(block, poll, appUrl); })
.catch(function () {
block.innerHTML = '<div style="text-align:center; padding:16px; opacity:0.5;">Poll unavailable</div>';
});
});
}
// Initial hydration
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', hydrateBlocks);
} else {
hydrateBlocks();
}
// Re-hydrate on MkDocs SPA navigation
if (typeof document$ !== 'undefined') {
document$.subscribe(function () { setTimeout(hydrateBlocks, 100); });
}
})();

View File

@ -7,10 +7,10 @@
"stars_count": 0,
"forks_count": 0,
"open_issues_count": 0,
"updated_at": "2026-03-23T15:48:06-06:00",
"updated_at": "2026-03-30T11:54:37-06:00",
"created_at": "2025-05-28T14:54:59-06:00",
"clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git",
"ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git",
"default_branch": "main",
"last_build_update": "2026-03-23T15:48:06-06:00"
"last_build_update": "2026-03-30T11:54:37-06:00"
}

View File

@ -4,13 +4,13 @@
"description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.",
"html_url": "https://github.com/anthropics/claude-code",
"language": "Shell",
"stars_count": 81725,
"forks_count": 6816,
"open_issues_count": 7445,
"updated_at": "2026-03-23T23:45:06Z",
"stars_count": 88332,
"forks_count": 9745,
"open_issues_count": 8343,
"updated_at": "2026-03-31T15:50:05Z",
"created_at": "2025-02-22T17:41:21Z",
"clone_url": "https://github.com/anthropics/claude-code.git",
"ssh_url": "git@github.com:anthropics/claude-code.git",
"default_branch": "main",
"last_build_update": "2026-03-20T22:24:50Z"
"last_build_update": "2026-03-31T14:35:57Z"
}

View File

@ -4,13 +4,13 @@
"description": "VS Code in the browser",
"html_url": "https://github.com/coder/code-server",
"language": "TypeScript",
"stars_count": 76802,
"forks_count": 6567,
"open_issues_count": 167,
"updated_at": "2026-03-23T23:30:09Z",
"stars_count": 76910,
"forks_count": 6575,
"open_issues_count": 172,
"updated_at": "2026-03-31T14:49:15Z",
"created_at": "2019-02-27T16:50:41Z",
"clone_url": "https://github.com/coder/code-server.git",
"ssh_url": "git@github.com:coder/code-server.git",
"default_branch": "main",
"last_build_update": "2026-03-23T18:50:37Z"
"last_build_update": "2026-03-31T00:00:24Z"
}

View File

@ -4,13 +4,13 @@
"description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.",
"html_url": "https://github.com/gethomepage/homepage",
"language": "JavaScript",
"stars_count": 29108,
"forks_count": 1825,
"open_issues_count": 1,
"updated_at": "2026-03-23T22:44:02Z",
"stars_count": 29249,
"forks_count": 1833,
"open_issues_count": 0,
"updated_at": "2026-03-31T14:45:22Z",
"created_at": "2022-08-24T07:29:42Z",
"clone_url": "https://github.com/gethomepage/homepage.git",
"ssh_url": "git@github.com:gethomepage/homepage.git",
"default_branch": "dev",
"last_build_update": "2026-03-23T12:27:34Z"
"last_build_update": "2026-03-31T14:36:10Z"
}

View File

@ -4,13 +4,13 @@
"description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD",
"html_url": "https://github.com/go-gitea/gitea",
"language": "Go",
"stars_count": 54439,
"forks_count": 6485,
"open_issues_count": 2870,
"updated_at": "2026-03-23T23:19:17Z",
"stars_count": 54629,
"forks_count": 6518,
"open_issues_count": 2866,
"updated_at": "2026-03-31T15:12:57Z",
"created_at": "2016-11-01T02:13:26Z",
"clone_url": "https://github.com/go-gitea/gitea.git",
"ssh_url": "git@github.com:go-gitea/gitea.git",
"default_branch": "main",
"last_build_update": "2026-03-23T23:20:24Z"
"last_build_update": "2026-03-31T15:40:43Z"
}

View File

@ -4,13 +4,13 @@
"description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.",
"html_url": "https://github.com/knadh/listmonk",
"language": "Go",
"stars_count": 19329,
"forks_count": 1958,
"open_issues_count": 105,
"updated_at": "2026-03-23T23:36:16Z",
"stars_count": 19396,
"forks_count": 1977,
"open_issues_count": 91,
"updated_at": "2026-03-31T14:23:10Z",
"created_at": "2019-06-26T05:08:39Z",
"clone_url": "https://github.com/knadh/listmonk.git",
"ssh_url": "git@github.com:knadh/listmonk.git",
"default_branch": "master",
"last_build_update": "2026-03-23T11:44:19Z"
"last_build_update": "2026-03-31T05:18:48Z"
}

View File

@ -4,13 +4,13 @@
"description": "Create & scan cute qr codes easily \ud83d\udc7e",
"html_url": "https://github.com/lyqht/mini-qr",
"language": "Vue",
"stars_count": 1928,
"forks_count": 243,
"open_issues_count": 20,
"updated_at": "2026-03-23T14:42:50Z",
"stars_count": 1938,
"forks_count": 245,
"open_issues_count": 23,
"updated_at": "2026-03-31T12:32:17Z",
"created_at": "2023-04-21T14:20:14Z",
"clone_url": "https://github.com/lyqht/mini-qr.git",
"ssh_url": "git@github.com:lyqht/mini-qr.git",
"default_branch": "main",
"last_build_update": "2026-03-13T12:48:04Z"
"last_build_update": "2026-03-31T12:43:07Z"
}

View File

@ -4,13 +4,13 @@
"description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.",
"html_url": "https://github.com/n8n-io/n8n",
"language": "TypeScript",
"stars_count": 180706,
"forks_count": 56089,
"open_issues_count": 1433,
"updated_at": "2026-03-23T23:47:25Z",
"stars_count": 181869,
"forks_count": 56348,
"open_issues_count": 1447,
"updated_at": "2026-03-31T15:46:32Z",
"created_at": "2019-06-22T09:24:21Z",
"clone_url": "https://github.com/n8n-io/n8n.git",
"ssh_url": "git@github.com:n8n-io/n8n.git",
"default_branch": "master",
"last_build_update": "2026-03-23T22:55:38Z"
"last_build_update": "2026-03-31T15:47:22Z"
}

View File

@ -4,13 +4,13 @@
"description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative",
"html_url": "https://github.com/nocodb/nocodb",
"language": "TypeScript",
"stars_count": 62544,
"forks_count": 4679,
"open_issues_count": 647,
"updated_at": "2026-03-23T22:41:15Z",
"stars_count": 62566,
"forks_count": 4703,
"open_issues_count": 665,
"updated_at": "2026-03-31T15:24:55Z",
"created_at": "2017-10-29T18:51:48Z",
"clone_url": "https://github.com/nocodb/nocodb.git",
"ssh_url": "git@github.com:nocodb/nocodb.git",
"default_branch": "develop",
"last_build_update": "2026-03-23T19:39:55Z"
"last_build_update": "2026-03-31T15:24:48Z"
}

View File

@ -4,13 +4,13 @@
"description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.",
"html_url": "https://github.com/ollama/ollama",
"language": "Go",
"stars_count": 165968,
"forks_count": 15121,
"open_issues_count": 2708,
"updated_at": "2026-03-23T23:47:27Z",
"stars_count": 166587,
"forks_count": 15255,
"open_issues_count": 2778,
"updated_at": "2026-03-31T15:34:55Z",
"created_at": "2023-06-26T19:39:32Z",
"clone_url": "https://github.com/ollama/ollama.git",
"ssh_url": "git@github.com:ollama/ollama.git",
"default_branch": "main",
"last_build_update": "2026-03-23T23:31:24Z"
"last_build_update": "2026-03-31T15:11:36Z"
}

View File

@ -4,13 +4,13 @@
"description": "Documentation that simply works",
"html_url": "https://github.com/squidfunk/mkdocs-material",
"language": "Python",
"stars_count": 26370,
"forks_count": 4058,
"stars_count": 26430,
"forks_count": 4062,
"open_issues_count": 1,
"updated_at": "2026-03-23T21:42:58Z",
"updated_at": "2026-03-31T14:42:16Z",
"created_at": "2016-01-28T22:09:23Z",
"clone_url": "https://github.com/squidfunk/mkdocs-material.git",
"ssh_url": "git@github.com:squidfunk/mkdocs-material.git",
"default_branch": "master",
"last_build_update": "2026-03-22T15:57:47Z"
"last_build_update": "2026-03-27T10:24:49Z"
}

View File

@ -17,6 +17,8 @@
<link rel="next" href="../../27/test-blog-post---version-7/">
@ -1432,6 +1434,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
@ -1704,7 +1708,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/blog/posts/introducing-changemaker-lite-v2.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/blog/posts/introducing-changemaker-lite-v2.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -1713,7 +1717,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/blog/posts/introducing-changemaker-lite-v2.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/blog/posts/introducing-changemaker-lite-v2.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -1752,7 +1756,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<p>Changemaker Lite costs roughly the price of a VPS — often under $50/month for the full stack. But the real value isn't cost savings. It's <strong>control.</strong> No vendor can cut off your access. No acquisition can change your terms.</p>
<p>Read more in our <a href="../../../../../docs/phil/">Philosophy</a> page.</p>
<h2 id="get-started">Get Started<a class="headerlink" href="#get-started" title="Permanent link">&para;</a></h2>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>curl<span class="w"> </span>-fsSL<span class="w"> </span>https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/v2/scripts/install.sh<span class="w"> </span><span class="p">|</span><span class="w"> </span>bash
<div class="language-bash highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>curl<span class="w"> </span>-fsSL<span class="w"> </span>https://gitea.bnkops.com/admin/changemaker.lite/raw/branch/main/scripts/install.sh<span class="w"> </span><span class="p">|</span><span class="w"> </span>bash
</span></code></pre></div>
<p>Or follow the <a href="../../../../../docs/getting-started/">Getting Started guide</a> for a walkthrough.</p>
<h2 id="whats-next">What's Next<a class="headerlink" href="#whats-next" title="Permanent link">&para;</a></h2>
@ -1803,6 +1807,30 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<footer class="md-footer">
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../../27/test-blog-post---version-7/" class="md-footer__link md-footer__link--next" aria-label="Next: Test Blog Post - Version 7">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
Test Blog Post - Version 7
</div>
</div>
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 11v2h12l-5.5 5.5 1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5 16 11z"/></svg>
</div>
</a>
</nav>
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
@ -1940,6 +1968,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../../../assets/js/scheduling-poll.js"></script>
<script src="../../../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../../../javascripts/ad-widgets.js"></script>
<script src="../../../../../javascripts/docs-comments.js"></script>

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
<link rel="canonical" href="https://cmlite.org/blog/archive/2026/">
<link rel="prev" href="../../category/platform/">
<link rel="prev" href="../../category/testing/">
@ -1484,6 +1484,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
@ -1582,6 +1584,48 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
</nav>
<div class="md-post__meta md-meta">
<ul class="md-meta__list">
<li class="md-meta__item">
<time datetime="2026-03-27 00:00:00+00:00">Mar 27, 2026</time></li>
<li class="md-meta__item">
in
<a href="../../category/testing/" class="md-meta__link">Testing</a></li>
<li class="md-meta__item">
1 min read
</li>
</ul>
</div>
</header>
<div class="md-post__content md-typeset">
<h2 id="test-blog-post-version-7"><a class="toclink" href="../../2026/03/27/test-blog-post---version-7/">Test Blog Post - Version 7</a></h2>
<p>This version uses the auto-setup token.</p>
</div>
</article>
<article class="md-post md-post--excerpt">
<header class="md-post__header">
<nav class="md-post__authors md-typeset">
<span class="md-author">
<img src="https://gitea.bnkops.com/avatars/1" alt="Bunker Operations">
</span>
</nav>
<div class="md-post__meta md-meta">
<ul class="md-meta__list">
<li class="md-meta__item">
@ -1656,7 +1700,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../../category/platform/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Platform">
<a href="../../category/testing/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Testing">
<div class="md-footer__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
@ -1666,7 +1710,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
Previous
</span>
<div class="md-ellipsis">
Platform
Testing
</div>
</div>
</a>
@ -1811,6 +1855,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../assets/js/scheduling-poll.js"></script>
<script src="../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../javascripts/ad-widgets.js"></script>
<script src="../../../javascripts/docs-comments.js"></script>

View File

@ -1435,6 +1435,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
@ -1553,6 +1555,36 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<li class="md-nav__item">
<a href="../testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing
</span>
</a>
</li>
</ul>
</nav>
@ -1859,6 +1891,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../assets/js/scheduling-poll.js"></script>
<script src="../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../javascripts/ad-widgets.js"></script>
<script src="../../../javascripts/docs-comments.js"></script>

View File

@ -19,7 +19,7 @@
<link rel="prev" href="../announcements/">
<link rel="next" href="../../archive/2026/">
<link rel="next" href="../testing/">
@ -1435,6 +1435,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
@ -1553,6 +1555,36 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<li class="md-nav__item">
<a href="../testing/" class="md-nav__link">
<span class="md-ellipsis">
Testing
</span>
</a>
</li>
</ul>
</nav>
@ -1705,13 +1737,13 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="../../archive/2026/" class="md-footer__link md-footer__link--next" aria-label="Next: 2026">
<a href="../testing/" class="md-footer__link md-footer__link--next" aria-label="Next: Testing">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
2026
Testing
</div>
</div>
<div class="md-footer__button md-icon">
@ -1859,6 +1891,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../assets/js/scheduling-poll.js"></script>
<script src="../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../javascripts/ad-widgets.js"></script>
<script src="../../../javascripts/docs-comments.js"></script>

File diff suppressed because it is too large Load Diff

View File

@ -1434,6 +1434,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
@ -1532,6 +1534,48 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
</nav>
<div class="md-post__meta md-meta">
<ul class="md-meta__list">
<li class="md-meta__item">
<time datetime="2026-03-27 00:00:00+00:00">Mar 27, 2026</time></li>
<li class="md-meta__item">
in
<a href="category/testing/" class="md-meta__link">Testing</a></li>
<li class="md-meta__item">
1 min read
</li>
</ul>
</div>
</header>
<div class="md-post__content md-typeset">
<h2 id="test-blog-post-version-7"><a class="toclink" href="2026/03/27/test-blog-post---version-7/">Test Blog Post - Version 7</a></h2>
<p>This version uses the auto-setup token.</p>
</div>
</article>
<article class="md-post md-post--excerpt">
<header class="md-post__header">
<nav class="md-post__authors md-typeset">
<span class="md-author">
<img src="https://gitea.bnkops.com/avatars/1" alt="Bunker Operations">
</span>
</nav>
<div class="md-post__meta md-meta">
<ul class="md-meta__list">
<li class="md-meta__item">
@ -1777,6 +1821,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../assets/js/scheduling-poll.js"></script>
<script src="../assets/js/straw-poll-widget.js"></script>
<script src="../javascripts/ad-widgets.js"></script>
<script src="../javascripts/docs-comments.js"></script>

View File

@ -1390,7 +1390,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/comments/callback.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/comments/callback.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -1399,7 +1399,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/comments/callback.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/comments/callback.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -1601,6 +1601,8 @@ setTimeout(function() {
<script src="../../assets/js/scheduling-poll.js"></script>
<script src="../../assets/js/straw-poll-widget.js"></script>
<script src="../../javascripts/ad-widgets.js"></script>
<script src="../../javascripts/docs-comments.js"></script>

View File

@ -3076,7 +3076,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/campaigns.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/campaigns.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -3085,7 +3085,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/campaigns.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/campaigns.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3382,6 +3382,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../../assets/js/scheduling-poll.js"></script>
<script src="../../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../../javascripts/ad-widgets.js"></script>
<script src="../../../../javascripts/docs-comments.js"></script>

View File

@ -2966,7 +2966,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/email-queue.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/email-queue.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -2975,7 +2975,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/email-queue.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/email-queue.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3212,6 +3212,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../../assets/js/scheduling-poll.js"></script>
<script src="../../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../../javascripts/ad-widgets.js"></script>
<script src="../../../../javascripts/docs-comments.js"></script>

View File

@ -2875,7 +2875,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/index.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/index.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -2884,7 +2884,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/index.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/index.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3114,6 +3114,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../assets/js/scheduling-poll.js"></script>
<script src="../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../javascripts/ad-widgets.js"></script>
<script src="../../../javascripts/docs-comments.js"></script>

View File

@ -2959,7 +2959,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/representatives.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/representatives.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -2968,7 +2968,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/representatives.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/representatives.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3204,6 +3204,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../../assets/js/scheduling-poll.js"></script>
<script src="../../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../../javascripts/ad-widgets.js"></script>
<script src="../../../../javascripts/docs-comments.js"></script>

View File

@ -2966,7 +2966,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/responses.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/responses.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -2975,7 +2975,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/advocacy/responses.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/advocacy/responses.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3212,6 +3212,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../../assets/js/scheduling-poll.js"></script>
<script src="../../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../../javascripts/ad-widgets.js"></script>
<script src="../../../../javascripts/docs-comments.js"></script>

View File

@ -3032,7 +3032,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/broadcast/email-templates.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/email-templates.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -3041,7 +3041,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/broadcast/email-templates.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/email-templates.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3309,6 +3309,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../../assets/js/scheduling-poll.js"></script>
<script src="../../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../../javascripts/ad-widgets.js"></script>
<script src="../../../../javascripts/docs-comments.js"></script>

View File

@ -2853,7 +2853,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/broadcast/index.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/index.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -2862,7 +2862,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/broadcast/index.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/index.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3091,6 +3091,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../assets/js/scheduling-poll.js"></script>
<script src="../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../javascripts/ad-widgets.js"></script>
<script src="../../../javascripts/docs-comments.js"></script>

View File

@ -2988,7 +2988,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/broadcast/newsletter.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/newsletter.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -2997,7 +2997,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/broadcast/newsletter.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/newsletter.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3314,6 +3314,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../../assets/js/scheduling-poll.js"></script>
<script src="../../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../../javascripts/ad-widgets.js"></script>
<script src="../../../../javascripts/docs-comments.js"></script>

View File

@ -3784,7 +3784,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/broadcast/sms.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/sms.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -3793,7 +3793,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/broadcast/sms.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/sms.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -4537,6 +4537,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../../assets/js/scheduling-poll.js"></script>
<script src="../../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../../javascripts/ad-widgets.js"></script>
<script src="../../../../javascripts/docs-comments.js"></script>

View File

@ -2771,7 +2771,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/dashboard.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/dashboard.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -2780,7 +2780,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/dashboard.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/dashboard.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3014,6 +3014,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../../assets/js/scheduling-poll.js"></script>
<script src="../../../assets/js/straw-poll-widget.js"></script>
<script src="../../../javascripts/ad-widgets.js"></script>
<script src="../../../javascripts/docs-comments.js"></script>

View File

@ -2720,7 +2720,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/index.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/index.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
@ -2729,7 +2729,7 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/v2/mkdocs/docs/docs/admin/index.md" title="View source of this page" class="md-content__button md-icon">
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/index.md" title="View source of this page" class="md-content__button md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
</a>
@ -3063,6 +3063,8 @@ body.cm-search-active .md-header--cm-hidden .md-search__output {
<script src="../../assets/js/scheduling-poll.js"></script>
<script src="../../assets/js/straw-poll-widget.js"></script>
<script src="../../javascripts/ad-widgets.js"></script>
<script src="../../javascripts/docs-comments.js"></script>

Some files were not shown because too many files have changed in this diff Show More