Compare commits

...

3 Commits

Author SHA1 Message Date
3de1d3fca5 Rewrite README as visual explainer with screenshots and docs link
Bunker Admin
2026-03-30 11:44:25 -06:00
a436c494fd Add duplication guard in collab onChange to detect and auto-fix doubled content
Y.js CRDT merges can duplicate content when a client reconnects after
external file modifications (e.g., API PUT while collab is active).
The guard detects when content is exactly doubled and auto-trims it.

Bunker Admin
2026-03-27 13:46:35 -06:00
078bb6e313 Fix collab preview refresh: use src reassignment with cache-buster instead of contentWindow.reload
Cross-origin iframes may silently fail on contentWindow.location.reload().
Use src reassignment with cache-buster query param to force fresh load.
Increased debounce to 2.5s to give MkDocs more time to rebuild.

Bunker Admin
2026-03-27 13:41:02 -06:00
3 changed files with 156 additions and 62 deletions

186
README.md
View File

@ -1,84 +1,156 @@
# Changemaker Lite <p align="center">
<img src="mkdocs/docs/assets/logo.png" alt="Changemaker Lite" width="120" />
</p>
A self-hosted political campaign platform that consolidates advocacy email campaigns, geographic mapping, volunteer canvassing, media management, and administration into a single TypeScript stack. Built for organizers who want to own their data. <h1 align="center">Changemaker Lite</h1>
## What Is This? <p align="center">
A self-hosted campaign platform for community organizers who want to own their data.
</p>
Changemaker Lite gives community organizers the tools they need to: <p align="center">
<a href="https://cmlite.org/docs/getting-started/">Documentation</a> &middot;
<a href="https://cmlite.org">Website</a> &middot;
<a href="https://opensource.org/license/apache-2-0">Apache 2.0 License</a>
</p>
- **Run advocacy campaigns** — let supporters look up their elected representatives by postal code and send emails in a few clicks ---
- **Manage canvassing** — map locations, draw canvassing areas, schedule volunteer shifts, and track door-to-door visits with GPS
- **Host media** — upload campaign videos, share them publicly, and track engagement analytics
- **Build landing pages** — drag-and-drop page builder for campaign microsites
- **Send newsletters** — integrated with Listmonk for opt-in mailing lists
- **Monitor everything** — Prometheus + Grafana observability stack included
The entire platform runs on Docker Compose with a single `.env` file for configuration. Changemaker Lite consolidates advocacy campaigns, geographic mapping, volunteer canvassing, media management, newsletters, and administration into a single Docker Compose stack. One `.env` file, one command to start, everything under your control.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/admin-dashboard.png" alt="Admin Dashboard" width="800" />
</p>
## Why Changemaker Lite?
Most campaign tools are SaaS platforms that lock you into monthly subscriptions, hold your data hostage, and disappear when funding dries up. Changemaker Lite is different:
- **Self-hosted** -- runs on any machine with Docker. Your server, your data.
- **All-in-one** -- replaces 5-10 separate tools with a single integrated platform.
- **Free and open source** -- Apache 2.0 licensed. Fork it, modify it, make it yours.
- **Privacy-first** -- no telemetry, no third-party analytics, no data leaving your server.
## What's Inside
### Advocacy Campaigns
Let supporters look up their elected representatives by postal code and send advocacy emails in a few clicks. Track responses, moderate a public response wall, and monitor email delivery.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/public-campaigns.png" alt="Public Campaign Page" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/influence-campaigns.png" alt="Campaign Management" width="800" />
</p>
### Interactive Map & Canvassing
Import thousands of addresses, draw canvassing areas, schedule volunteer shifts, and track door-to-door visits with GPS. Volunteers get a full-screen mobile map with real-time location tracking and visit recording.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/public-map.png" alt="Public Map" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/canvass-dashboard.png" alt="Canvass Dashboard" width="800" />
</p>
### Volunteer Portal
Volunteers get their own portal with shift sign-ups, canvassing assignments, activity tracking, a social calendar, and a friends system to stay connected with their team.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/volunteer-dashboard.png" alt="Volunteer Map" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/volunteer-calendar.png" alt="Volunteer Calendar" width="800" />
</p>
### Media Library & Public Gallery
Upload campaign videos, manage metadata, schedule publishing, and share them through a public gallery. Includes GDPR-compliant analytics.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/media-library.png" alt="Media Library" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/public-gallery.png" alt="Public Gallery" width="800" />
</p>
### Landing Pages & Email Templates
Build campaign microsites with a drag-and-drop GrapesJS editor. Design email templates for consistent campaign communications.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/landing-pages.png" alt="Landing Page Builder" width="800" />
</p>
### SMS Campaigns, Newsletters & More
Send SMS campaigns via an Android bridge, sync subscribers to Listmonk for newsletters, recognize volunteers on a Wall of Fame leaderboard, and monitor everything with built-in Prometheus + Grafana observability.
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/sms-dashboard.png" alt="SMS Dashboard" width="800" />
</p>
<p align="center">
<img src="mkdocs/docs/assets/images/screenshots/features/public-wall-of-fame.png" alt="Wall of Fame" width="800" />
</p>
## Quick Start ## Quick Start
```bash ```bash
# Clone and switch to the v2 branch # 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
cd ~/changemaker.lite
docker compose up -d
```
Or clone and build from source:
```bash
git clone <repo-url> changemaker.lite git clone <repo-url> changemaker.lite
cd changemaker.lite cd changemaker.lite && git checkout v2
git checkout v2
# Create your environment file
cp .env.example .env cp .env.example .env
# Edit .env — at minimum set: # Edit .env -- set passwords, JWT secrets, admin credentials
# V2_POSTGRES_PASSWORD, REDIS_PASSWORD,
# JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY
# INITIAL_ADMIN_EMAIL, INITIAL_ADMIN_PASSWORD
# Start core services
docker compose up -d v2-postgres redis api admin docker compose up -d v2-postgres redis api admin
# Run database migrations and seed
docker compose exec api npx prisma migrate deploy docker compose exec api npx prisma migrate deploy
docker compose exec api npx prisma db seed docker compose exec api npx prisma db seed
``` ```
Then open **http://localhost:3000** and log in with the admin credentials from your `.env`. Then open **http://localhost:3000** and log in with the admin credentials from your `.env`.
## Architecture
| Component | Technology | Port |
|-----------|-----------|------|
| **API** | Express.js + Prisma + PostgreSQL | 4000 |
| **Media API** | Fastify + Prisma (shared DB) | 4100 |
| **Admin GUI** | React + Vite + Ant Design + Zustand | 3000 |
| **Reverse Proxy** | Nginx (subdomain routing) | 80 |
| **Database** | PostgreSQL 16 | 5433 |
| **Cache / Queue** | Redis + BullMQ | 6379 |
| **Newsletter** | Listmonk | 9001 |
| **Monitoring** | Prometheus + Grafana + Alertmanager | 9090, 3001 |
See `CLAUDE.md` for comprehensive architecture documentation, module reference, and troubleshooting.
## Feature Flags
Enable optional modules in `.env`:
```bash
ENABLE_MEDIA_FEATURES=true # Video library + gallery
LISTMONK_SYNC_ENABLED=true # Newsletter subscriber sync
EMAIL_TEST_MODE=true # Route emails to MailHog (dev)
```
## Production Deployment
Changemaker Lite uses [Pangolin](https://github.com/fosrl/pangolin) tunnels for production access (Cloudflare alternative). See the Tunnel page in the admin panel (`/app/tunnel`) for setup instructions.
## Documentation ## Documentation
- **`CLAUDE.md`** — Full project reference (architecture, modules, ports, patterns) **Full documentation is available at [cmlite.org/docs/getting-started](https://cmlite.org/docs/getting-started/).**
- **`V2_PLAN.md`** — Development roadmap (Phases 1-14 complete)
- **`SECURITY_AUDIT_2025-02-11.md`** — Security audit findings and remediations
- **`.env.example`** — All 100+ environment variables with descriptions
## Licensing The docs site covers installation, configuration, all features, architecture details, production deployment with Pangolin tunnels, and troubleshooting. It is the authoritative and up-to-date reference for Changemaker Lite.
This project is licensed under the [Apache License 2.0](https://opensource.org/license/apache-2-0). ## Architecture at a Glance
| Layer | Technology |
|-------|-----------|
| API | Express.js + Prisma + PostgreSQL 16 |
| Media API | Fastify + Prisma (shared DB) |
| Frontend | React + Vite + Ant Design + Zustand |
| Reverse Proxy | Nginx (subdomain routing) |
| Cache & Queue | Redis + BullMQ |
| Newsletter | Listmonk |
| Monitoring | Prometheus + Grafana + Alertmanager |
| Tunneling | Pangolin (self-hosted Cloudflare alternative) |
The entire stack runs on Docker Compose. Enable optional modules (media, newsletters, SMS, monitoring) with feature flags in `.env`.
## License
[Apache License 2.0](https://opensource.org/license/apache-2-0)
## AI Disclaimer ## AI Disclaimer

View File

@ -938,20 +938,27 @@ export default function DocsPage() {
// Auto-refresh preview when remote changes arrive in collab mode // Auto-refresh preview when remote changes arrive in collab mode
useEffect(() => { useEffect(() => {
if (!collab.active || !collab.yText) return; if (!collab.active || !collab.yText || !selectedFile) return;
let refreshTimer: ReturnType<typeof setTimeout>; let refreshTimer: ReturnType<typeof setTimeout>;
const observer = () => { const observer = () => {
clearTimeout(refreshTimer); clearTimeout(refreshTimer);
refreshTimer = setTimeout(() => { refreshTimer = setTimeout(() => {
previewIframeRef.current?.contentWindow?.location.reload(); // Re-set src to force reload (avoids cross-origin issues with contentWindow.reload)
}, 2000); if (previewIframeRef.current && selectedFile) {
const url = filePathToMkDocsUrl(selectedFile);
// Append cache-buster to force fresh load
const buster = `_t=${Date.now()}`;
const sep = url.includes('?') ? '&' : '?';
previewIframeRef.current.src = url + sep + buster;
}
}, 2500);
}; };
collab.yText.observe(observer); collab.yText.observe(observer);
return () => { return () => {
collab.yText?.unobserve(observer); collab.yText?.unobserve(observer);
clearTimeout(refreshTimer); clearTimeout(refreshTimer);
}; };
}, [collab.active, collab.yText]); }, [collab.active, collab.yText, selectedFile]);
const handleToolbarSnippet = useCallback((snippetId: string) => { const handleToolbarSnippet = useCallback((snippetId: string) => {
if (snippetId === 'video-card') { if (snippetId === 'video-card') {

View File

@ -257,8 +257,23 @@ const docsExtension: Extension = {
return; return;
} }
// Write plaintext to disk (debounced by Hocuspocus's built-in debounce) // Duplication guard: detect and fix content that appears to be doubled
const content = yText.toString(); const content = yText.toString();
if (content.length > 20) {
const half = Math.floor(content.length / 2);
const firstHalf = content.substring(0, half);
const secondHalf = content.substring(half);
if (firstHalf === secondHalf) {
logger.warn(`Docs collab: detected duplicated content in ${documentName} (${content.length} chars), fixing`);
// Replace the Y.Text with just the first half
document.transact(() => {
yText.delete(half, content.length - half);
});
return; // The deletion triggers another onChange with the fixed content
}
}
// Write plaintext to disk (debounced by Hocuspocus's built-in debounce)
try { try {
await docsFilesService.writeFileContent(documentName, content); await docsFilesService.writeFileContent(documentName, content);
// Invalidate Redis file cache // Invalidate Redis file cache