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
@ -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
|
||||
|
||||
@ -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:**
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
@ -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"
|
||||
},
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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
@ -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"
|
||||
},
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
50
api/src/modules/auth/gitea-sso.routes.ts
Normal 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 };
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -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); }
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 61 KiB |
@ -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",
|
||||
|
||||
@ -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` → `2024–2026` 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 1–2 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 `` 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)
|
||||
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 176 KiB |
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
12
mkdocs/docs/blog/posts/2026-03-27-test-blog-post.md
Normal 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.
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
### 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
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
!!! 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
|
||||
|
||||
@ -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
@ -0,0 +1,3 @@
|
||||
# test
|
||||
|
||||
Hello!
|
||||
@ -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:
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 74 KiB |
BIN
mkdocs/site/assets/images/social/blog/category/testing.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
mkdocs/site/assets/images/social/test.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
226
mkdocs/site/assets/js/straw-poll-widget.js
Normal 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 →</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 += ' · ' + 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;">✓ 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); });
|
||||
}
|
||||
})();
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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">¶</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">¶</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>
|
||||
|
||||
1842
mkdocs/site/blog/2026/03/27/test-blog-post---version-7/index.html
Normal 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
1894
mkdocs/site/blog/category/testing/index.html
Normal 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||