Tonnes of updates to the site documentation and general processes

This commit is contained in:
2025-06-30 15:49:39 -06:00
parent 438e9678d1
commit d607d3a4b2
93 changed files with 14823 additions and 6347 deletions

View File

@@ -0,0 +1,16 @@
{
"full_name": "admin/changemaker.lite",
"name": "changemaker.lite",
"description": "Changemaker-lite is the current active development branch of Changemaker, focused on streamlining core services. These improvements will be merged into the master branch once ready.",
"html_url": "https://gitea.bnkops.com/admin/changemaker.lite",
"language": "HTML",
"stars_count": 0,
"forks_count": 0,
"open_issues_count": 0,
"updated_at": "2025-06-30T10:01:47-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": "2025-06-30T10:01:47-06:00"
}

View File

@@ -0,0 +1,16 @@
{
"full_name": "coder/code-server",
"name": "code-server",
"description": "VS Code in the browser",
"html_url": "https://github.com/coder/code-server",
"language": "TypeScript",
"stars_count": 72491,
"forks_count": 6060,
"open_issues_count": 132,
"updated_at": "2025-06-30T21:31:29Z",
"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": "2025-06-25T21:18:52Z"
}

View File

@@ -0,0 +1,16 @@
{
"full_name": "gethomepage/homepage",
"name": "homepage",
"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": 24565,
"forks_count": 1519,
"open_issues_count": 1,
"updated_at": "2025-06-30T20:44:09Z",
"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": "2025-06-30T12:13:58Z"
}

View File

@@ -0,0 +1,16 @@
{
"full_name": "knadh/listmonk",
"name": "listmonk",
"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": 17238,
"forks_count": 1656,
"open_issues_count": 103,
"updated_at": "2025-06-30T20:57:12Z",
"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": "2025-06-30T10:45:51Z"
}

View File

@@ -0,0 +1,16 @@
{
"full_name": "n8n-io/n8n",
"name": "n8n",
"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": 113623,
"forks_count": 33244,
"open_issues_count": 1061,
"updated_at": "2025-06-30T21:38:07Z",
"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": "2025-06-30T20:59:49Z"
}

View File

@@ -0,0 +1,16 @@
{
"full_name": "nocodb/nocodb",
"name": "nocodb",
"description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 Open Source Airtable Alternative",
"html_url": "https://github.com/nocodb/nocodb",
"language": "TypeScript",
"stars_count": 55465,
"forks_count": 3980,
"open_issues_count": 714,
"updated_at": "2025-06-30T21:39:01Z",
"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": "2025-06-30T20:21:14Z"
}

View File

@@ -0,0 +1,16 @@
{
"full_name": "squidfunk/mkdocs-material",
"name": "mkdocs-material",
"description": "Documentation that simply works",
"html_url": "https://github.com/squidfunk/mkdocs-material",
"language": "Python",
"stars_count": 23757,
"forks_count": 3783,
"open_issues_count": 4,
"updated_at": "2025-06-30T21:37:52Z",
"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": "2025-06-29T16:43:26Z"
}

147
mkdocs/docs/build/build-server.md vendored Normal file
View File

@@ -0,0 +1,147 @@
# BNKops Server Build
Purpose: a Ubuntu server build-out for general application
---
This documentation is a overview of the full build out for a server OS and baseline for running Changemaker-lite. It is a manual to re-install this server on any machine.
All of the following systems are free and the majority are open source.
## [Ubuntu](https://ubuntu.com/) OS
_Ubuntu_ is a Linux distribution derived from Debian and composed mostly of free and open-source software.
### [Install Ubuntu](https://ubuntu.com/tutorials/install-ubuntu-desktop#1-overview)
### Post Install
Post installation, run update:
```
sudo apt update
```
```
sudo apt upgrade
```
### Configuration
Further configurations:
- User profile was updated to Automatically Login
- Remote Desktop, Sharing, and Login have all been enabled.
- Default system settings have been set to dark mode.
## [VSCode Insiders](https://code.visualstudio.com/insiders/)
Visual Studio Code is a new choice of tool that combines the simplicity of a code editor with what developers need for the core edit-build-debug cycle.
### Install Using App Centre
## [Obsidian](https://obsidian.md/)
The free and flexible app for your private thoughts.
### Install Using App Center
## [Curl](https://curl.se/)
command line tool and library for transferring data with URLs (since 1998)
### Install
```
sudo apt install curl
```
## [Glances](https://github.com/nicolargo/glances)
Glances an Eye on your system. A top/htop alternative for GNU/Linux, BSD, Mac OS and Windows operating systems.
### Install
```
sudo snap install glances
```
## [Syncthing](https://syncthing.net/)
Syncthing is a continuous file synchronization program. It synchronizes files between two or more computers in real time, safely protected from prying eyes. Your data is your data alone and you deserve to choose where it is stored, whether it is shared with some third party, and how its transmitted over the internet.
### Install
```
# Add the release PGP keys:
sudo mkdir -p /etc/apt/keyrings
sudo curl -L -o /etc/apt/keyrings/syncthing-archive-keyring.gpg https://syncthing.net/release-key.gpg
```
```
# Add the "stable" channel to your APT sources:
echo "deb [signed-by=/etc/apt/keyrings/syncthing-archive-keyring.gpg] https://apt.syncthing.net/ syncthing stable" | sudo tee /etc/apt/sources.list.d/syncthing.list
```
```
# Update and install syncthing:
sudo apt-get update
sudo apt-get install syncthing
```
### Post Install
Run syncthing as a system service.
```
sudo systemctl start syncthing@yourusername
```
```
sudo systemctl enable syncthing@yourusername
```
## [Docker](https://www.docker.com/)
Docker helps developers build, share, run, and verify applications anywhere — without tedious environment configuration or management.
```
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
```
```
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
### Update Users
```
sudo groupadd docker
```
```
sudo usermod -aG docker $USER
```
```
newgrp docker
```
### Enable on Boot
```
sudo systemctl enable docker.service
sudo systemctl enable containerd.service
```
## [Cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)
Connect, protect, and build everywhere. We make websites, apps, and networks faster and more secure. Our developer platform is the best place to build modern apps and deliver AI initiatives.
```
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
```
```
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
```
```
sudo apt-get update && sudo apt-get install cloudflared
```
### Post Install
Login to Cloudflare
```
cloudflared login
```
### Configuration
- Setting up the [[service.cloudflared]] enables your cloudflared tunnel to automatically run on reboot
## [Pandoc](https://pandoc.org/)
If you need to convert files from one markup format into another, pandoc is your swiss-army knife.
```
sudo apt install pandoc
```
## [Changemaker (V3.9.9.1)](https://changemaker.bnkops.com)

View File

@@ -1,6 +1,42 @@
# Getting Started with Digital Liberation 🏳️‍⚧️
# Getting Started with Digital Liberation
Welcome to Changemaker-Lite! You're about to reclaim your digital sovereignty and stop feeding your secrets to corporations. This guide will help you set up your own political infrastructure that you actually own and control.
## Quick Start
### Build Changemkaer-Lite
```bash
# Clone the repository
git clone https://gitea.bnkops.com/admin/changemaker.lite
cd changemaker.lite
```
!!! warning "Cloudflare Credentials"
The config.sh script will ask you for your Cloudflare credentials to get started. You can find more information on how to find this in the [Cloudlflare Configuration](./config/cloudflare-config.md)
```
# Configure environment (creates .env file)
./config.sh
```
```
# Start all services
docker compose up -d
```
### Deploy
!!! note "Cloudflare"
Right now, we suggest deploying using Cloudflare for simplicity and protections against 99% of surface level attacks to digital infrastructure. If you want to avoid using this service, we recommend checking out [Pagolin](https://github.com/fosrl/pangolin) as a drop in replacement.
For secure public access, use the production deployment script:
```bash
./start-production.sh
```
Welcome to Changemaker Lite! You're about to reclaim your digital sovereignty and stop feeding your secrets to corporations. This guide will help you set up your own political infrastructure that you actually own and control.
## Why Changemaker Lite?
@@ -30,8 +66,13 @@ Before we dive into the technical setup, let's be clear about what you're doing
### Operating System
- **Ubuntu 24.04 LTS (Noble Numbat)** - Recommended and tested
!!! note "Getting Started on Ubunut"
Want some help getting started with a baseline buildout for a Ubunut server? You can use our [BNKops Server Build Script](./build-server.md)
- Other Linux distributions with systemd support
- WSL2 on Windows (limited functionality)
- Mac OS
!!! tip "New to Linux?"
Consider [Linux Mint](https://www.linuxmint.com/) - it looks like Windows but opens the door to true digital freedom.
@@ -44,10 +85,13 @@ Before we dive into the technical setup, let's be clear about what you're doing
- **Network**: Stable internet connection
!!! info "Cloud Hosting"
You can run this on a VPS from providers like Hetzner, DigitalOcean, or Linode for $20-50/month.
You can run this on a VPS from providers like Hetzner, DigitalOcean, or Linode for ~$20/month.
### Software Prerequisites
!!! tip "Getting Started on Docker"
Want some help getting started with a baseline buildout for a Ubunutu server? You can use our [BNKops Server Build Script](./build-server.md) to roll out a configured server in about 20 mins!
1. **Docker Engine** (24.0+)
```bash
@@ -159,34 +203,29 @@ Expected output should show all services as "Up":
Once services are running, access them locally:
### 🏠 Homepage Dashboard
- **URL**: http://localhost:3010
- **Purpose**: Central hub for all services
### 🏠 Homepage Dashboard
- **URL**: [http://localhost:3010](http://localhost:3010)
- **Purpose**: Central hub for all services
- **Features**: Service status, quick links, monitoring
### 💻 Development Tools
### 💻 Development Tools
- **Code Server**: [http://localhost:8888](http://localhost:8888) — VS Code in browser
- **Gitea**: [http://localhost:3030](http://localhost:3030) — Git repository management
- **MkDocs Dev**: [http://localhost:4000](http://localhost:4000) — Live documentation preview
- **MkDocs Prod**: [http://localhost:4001](http://localhost:4001) — Built documentation
- **Code Server**: http://localhost:8888 - VS Code in browser
- **Gitea**: http://localhost:3030 - Git repository management
- **MkDocs Dev**: http://localhost:4000 - Live documentation preview
- **MkDocs Prod**: http://localhost:4001 - Built documentation
### 📧 Communication
- **Listmonk**: [http://localhost:9000](http://localhost:9000) — Email campaigns
_Login with credentials set during configuration_
### 📧 Communication
### 🔄 Automation & Data
- **n8n**: [http://localhost:5678](http://localhost:5678) — Workflow automation
_Login with credentials set during configuration_
- **NocoDB**: [http://localhost:8090](http://localhost:8090) — No-code database
- **Listmonk**: http://localhost:9000 - Email campaigns
- Login with credentials set during configuration
### 🔄 Automation & Data
- **n8n**: http://localhost:5678 - Workflow automation
- Login with credentials set during configuration
- **NocoDB**: http://localhost:8090 - No-code database
### 🛠️ Interactive Tools
- **Map Viewer**: http://localhost:3000 - Geographic data visualization
- **Mini QR**: http://localhost:8089 - QR code generator
### 🛠️ Interactive Tools
- **Map Viewer**: [http://localhost:3000](http://localhost:3000) — Geographic data visualization
- **Mini QR**: [http://localhost:8089](http://localhost:8089) — QR code generator
## Production Deployment

View File

@@ -1,7 +0,0 @@
# Cloudflare
In this section, you will find instructions on how to properly set up Cloudflare for managing a Changemaker-lite.
## Config
When you first run the `./config.sh` it will prompt you for the a api token, zone id, and a tunnel id. A

View File

@@ -0,0 +1,57 @@
# Configure Cloudflare
Cloudflare is the largest DNS routing service on the planet. We use their free service tier to provide Changemaker users with a fast, secure, and reliable way to get online that blocks 99% of surface level attacks and has built in user authenticaion (if you so choose to use it)
## Credentials
The `config.sh` and `start-production.sh` scripts require the following Cloudflare credentials to function properly:
### 1. **Cloudflare API Token**
- **Purpose**: Used to authenticate API requests to Cloudflare for managing DNS records, tunnels, and access policies.
- **Required Permissions**:
- `Zone.DNS` (Read/Write)
- `Account.Cloudflare Tunnel` (Read/Write)
- **How to Obtain**:
- Log in to your Cloudflare account.
- Go to **My Profile** > **API Tokens** > **Create Token**.
- Use the **Edit zone DNS** template and add **Cloudflare Tunnel** permissions.
### 2. **Cloudflare Zone ID**
- **Purpose**: Identifies the specific DNS zone (domain) in Cloudflare where DNS records will be created.
- **How to Obtain**:
- Log in to your Cloudflare account.
- Select the domain you want to use.
- The Zone ID is displayed in the **Overview** section under **API**.
### 3. **Cloudflare Account ID**
- **Purpose**: Identifies your Cloudflare account for tunnel creation and management.
- **How to Obtain**:
- Log in to your Cloudflare account.
- Go to **My Profile** > **API Tokens**.
- The Account ID is displayed at the top of the page.
### 4. **Cloudflare Tunnel ID** (Optional in config.sh, Required in start-production.sh)
- **Purpose**: Identifies the specific Cloudflare Tunnel that will be used to route traffic to your services.
- **How to Obtain**:
- This is automatically generated when you create a tunnel using `cloudflared tunnel create` or via the Cloudflare dashboard.
- The start-production.sh script will create this for you if it doesn't exist.
### Summary of Required Credentials:
```bash
# In .env file
CF_API_TOKEN=your_cloudflare_api_token
CF_ZONE_ID=your_cloudflare_zone_id
CF_ACCOUNT_ID=your_cloudflare_account_id
CF_TUNNEL_ID=will_be_set_by_start_production # This will be set by start-production.sh
```
### Notes:
- The config.sh script will prompt you for these credentials during setup.
- The start-production.sh script will verify these credentials and use them to configure DNS records, create tunnels, and set up access policies.
- Ensure that the API token has the correct permissions, or the scripts will fail to configure Cloudflare services.

View File

@@ -0,0 +1,6 @@
# Configuration
There are several configuration steps to building a production ready Changemaker-Lite.
In the order we suggest doing them:

View File

@@ -0,0 +1,132 @@
"""
MkDocs Hook for Repository Widget Data Generation
Fetches repository data and generates JSON files during build
"""
import json
import os
import requests
from pathlib import Path
from typing import Dict, Any
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def on_pre_build(config: Dict[str, Any]) -> None:
"""
Hook that runs before MkDocs builds the site
Generates repository JSON data files
"""
logger.info("Generating repository widget data...")
# Define repositories to fetch - all repos from services.md
repositories = [
# Gitea repositories
{
"repo": "admin/changemaker.lite",
"gitea_url": "https://gitea.bnkops.com",
"token": os.getenv("GITEA_TOKEN")
},
# GitHub repositories
{
"repo": "coder/code-server",
"github": True
},
{
"repo": "n8n-io/n8n",
"github": True
},
{
"repo": "knadh/listmonk",
"github": True
},
{
"repo": "nocodb/nocodb",
"github": True
},
{ "repo": "squidfunk/mkdocs-material",
"github": True
},
{
"repo": "gethomepage/homepage",
"github": True
}
]
# Create output directory
docs_dir = config.get('docs_dir', 'docs')
output_dir = Path(docs_dir) / 'assets' / 'repo-data'
output_dir.mkdir(parents=True, exist_ok=True)
# Generate data for each repository
for repo_config in repositories:
try:
generate_repo_data(repo_config, output_dir)
except Exception as e:
logger.error(f"Failed to generate data for {repo_config['repo']}: {e}")
def generate_repo_data(repo_config: Dict[str, Any], output_dir: Path) -> None:
"""Generate JSON data file for a single repository"""
repo = repo_config['repo']
logger.info(f"Fetching data for {repo}")
# Determine API URL and headers
if repo_config.get('github'):
api_url = f"https://api.github.com/repos/{repo}"
headers = {'Accept': 'application/vnd.github.v3+json'}
github_token = os.getenv('GITHUB_TOKEN')
if github_token:
headers['Authorization'] = f'token {github_token}'
else:
gitea_url = repo_config.get('gitea_url', 'https://gitea.bnkops.com')
api_url = f"{gitea_url}/api/v1/repos/{repo}"
headers = {'Accept': 'application/json'}
token = repo_config.get('token') or os.getenv('GITEA_TOKEN')
if token:
headers['Authorization'] = f'token {token}'
# Fetch repository data
try:
response = requests.get(api_url, headers=headers, timeout=10)
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
logger.error(f"Failed to fetch {repo}: {e}")
return
# Extract widget data
widget_data = {
'full_name': data.get('full_name'),
'name': data.get('name'),
'description': data.get('description'),
'html_url': data.get('html_url'),
'language': data.get('language'),
'stars_count': data.get('stargazers_count') or data.get('stars_count', 0),
'forks_count': data.get('forks_count', 0),
'open_issues_count': data.get('open_issues_count', 0),
'updated_at': data.get('updated_at'),
'created_at': data.get('created_at'),
'clone_url': data.get('clone_url'),
'ssh_url': data.get('ssh_url'),
'default_branch': data.get('default_branch', 'main'),
'last_build_update': data.get('pushed_at') or data.get('updated_at')
}
# Generate filename
filename = f"{repo.replace('/', '-')}.json"
file_path = output_dir / filename
# Write JSON file
try:
with open(file_path, 'w') as f:
json.dump(widget_data, f, indent=2)
logger.info(f"Generated: {filename}")
except Exception as e:
logger.error(f"Failed to write {filename}: {e}")
def on_post_build(config: Dict[str, Any]) -> None:
"""Hook that runs after MkDocs builds the site"""
logger.info("Repository widget data generation complete")

View File

@@ -0,0 +1,132 @@
document.addEventListener('DOMContentLoaded', function() {
const widgets = document.querySelectorAll('.gitea-widget');
widgets.forEach(widget => {
const repo = widget.dataset.repo;
// Auto-generate data file path from repo name
const dataFile = `/assets/repo-data/${repo.replace('/', '-')}.json`;
const showDescription = widget.dataset.showDescription !== 'false';
const showLanguage = widget.dataset.showLanguage !== 'false';
const showLastUpdate = widget.dataset.showLastUpdate !== 'false';
// Show loading state
widget.innerHTML = `
<div class="gitea-widget-loading">
<div class="loading-spinner"></div>
<span>Loading ${repo}...</span>
</div>
`;
// Fetch repository data
fetch(dataFile)
.then(response => {
if (!response.ok) {
throw new Error(`Could not load data for ${repo}`);
}
return response.json();
})
.then(data => {
renderWidget(widget, data, { showDescription, showLanguage, showLastUpdate });
})
.catch(error => {
renderError(widget, repo, error.message);
});
});
});
function renderWidget(widget, data, options) {
const lastUpdate = new Date(data.updated_at).toLocaleDateString();
const language = data.language || 'Not specified';
widget.innerHTML = `
<div class="gitea-widget-container">
<div class="gitea-widget-header">
<div class="gitea-widget-title">
<svg class="gitea-icon" viewBox="0 0 16 16" width="16" height="16">
<path fill="currentColor" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"/>
</svg>
<a href="${data.html_url}" target="_blank" rel="noopener noreferrer" class="repo-link">
${data.full_name}
</a>
</div>
<div class="gitea-widget-stats">
<span class="stat-item" title="Stars">
<svg viewBox="0 0 16 16" width="14" height="14">
<path fill="currentColor" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
</svg>
${data.stars_count.toLocaleString()}
</span>
<span class="stat-item" title="Forks">
<svg viewBox="0 0 16 16" width="14" height="14">
<path fill="currentColor" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25V5.372a2.25 2.25 0 10-1.5 0v.878A.75.75 0 0110.25 7H5.75A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
</svg>
${data.forks_count.toLocaleString()}
</span>
<span class="stat-item" title="Open Issues">
<svg viewBox="0 0 16 16" width="14" height="14">
<path fill="currentColor" d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"/>
<path fill="currentColor" fill-rule="evenodd" d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z"/>
</svg>
${data.open_issues_count.toLocaleString()}
</span>
</div>
</div>
${options.showDescription && data.description ? `
<div class="gitea-widget-description">
${data.description}
</div>
` : ''}
<div class="gitea-widget-footer">
${options.showLanguage ? `
<span class="language-info">
<span class="language-dot" style="background-color: ${getLanguageColor(language)}"></span>
${language}
</span>
` : ''}
${options.showLastUpdate ? `
<span class="last-update">Updated ${lastUpdate}</span>
` : ''}
</div>
</div>
`;
}
function renderError(widget, repo, error) {
widget.innerHTML = `
<div class="gitea-widget-error">
<svg viewBox="0 0 16 16" width="16" height="16">
<path fill="currentColor" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"/>
</svg>
<span>Failed to load ${repo}</span>
<small>${error}</small>
</div>
`;
}
function getLanguageColor(language) {
const colors = {
'JavaScript': '#f1e05a',
'TypeScript': '#2b7489',
'Python': '#3572A5',
'Java': '#b07219',
'C++': '#f34b7d',
'C': '#555555',
'C#': '#239120',
'PHP': '#4F5D95',
'Ruby': '#701516',
'Go': '#00ADD8',
'Rust': '#dea584',
'Swift': '#ffac45',
'Kotlin': '#F18E33',
'Scala': '#c22d40',
'Shell': '#89e051',
'HTML': '#e34c26',
'CSS': '#563d7c',
'Vue': '#2c3e50',
'React': '#61dafb',
'Dockerfile': '#384d54',
'Markdown': '#083fa1'
};
return colors[language] || '#586069';
}

View File

@@ -0,0 +1,167 @@
let widgetsInitialized = new Set();
function initializeGitHubWidgets() {
const widgets = document.querySelectorAll('.github-widget');
widgets.forEach(widget => {
// Skip if already initialized
const widgetId = widget.dataset.repo + '-' + Math.random().toString(36).substr(2, 9);
if (widget.dataset.initialized) return;
widget.dataset.initialized = 'true';
const repo = widget.dataset.repo;
// Auto-generate data file path from repo name
const dataFile = `/assets/repo-data/${repo.replace('/', '-')}.json`;
const showDescription = widget.dataset.showDescription !== 'false';
const showLanguage = widget.dataset.showLanguage !== 'false';
const showLastUpdate = widget.dataset.showLastUpdate !== 'false';
// Show loading state
widget.innerHTML = `
<div class="github-widget-loading">
<div class="loading-spinner"></div>
<span>Loading ${repo}...</span>
</div>
`;
// Fetch repository data from pre-generated JSON file
fetch(dataFile)
.then(response => {
if (!response.ok) {
throw new Error(`Could not load data for ${repo}`);
}
return response.json();
})
.then(data => {
const lastUpdate = new Date(data.updated_at).toLocaleDateString();
const language = data.language || 'Not specified';
widget.innerHTML = `
<div class="github-widget-container">
<div class="github-widget-header">
<div class="github-widget-title">
<svg class="github-icon" viewBox="0 0 16 16" width="16" height="16">
<path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
<a href="${data.html_url}" target="_blank" rel="noopener noreferrer" class="repo-link">
${data.full_name}
</a>
</div>
<div class="github-widget-stats">
<span class="stat-item" title="Stars">
<svg viewBox="0 0 16 16" width="14" height="14">
<path fill="currentColor" d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
</svg>
${data.stars_count.toLocaleString()}
</span>
<span class="stat-item" title="Forks">
<svg viewBox="0 0 16 16" width="14" height="14">
<path fill="currentColor" d="M5 3.25a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm0 2.122a2.25 2.25 0 10-1.5 0v.878A2.25 2.25 0 005.75 8.5h1.5v2.128a2.251 2.251 0 101.5 0V8.5h1.5a2.25 2.25 0 002.25-2.25V5.372a2.25 2.25 0 10-1.5 0v.878A.75.75 0 0110.25 7H5.75A.75.75 0 015 6.25v-.878zm3.75 7.378a.75.75 0 11-1.5 0 .75.75 0 011.5 0zm3-8.75a.75.75 0 100-1.5.75.75 0 000 1.5z"/>
</svg>
${data.forks_count.toLocaleString()}
</span>
<span class="stat-item" title="Open Issues">
<svg viewBox="0 0 16 16" width="14" height="14">
<path fill="currentColor" d="M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"/>
<path fill="currentColor" fill-rule="evenodd" d="M8 0a8 8 0 100 16A8 8 0 008 0zM1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0z"/>
</svg>
${data.open_issues_count.toLocaleString()}
</span>
</div>
</div>
${showDescription && data.description ? `
<div class="github-widget-description">
${data.description}
</div>
` : ''}
<div class="github-widget-footer">
${showLanguage ? `
<span class="language-info">
<span class="language-dot" style="background-color: ${getLanguageColor(language)}"></span>
${language}
</span>
` : ''}
${showLastUpdate ? `
<span class="last-update">Updated ${lastUpdate}</span>
` : ''}
${data.license ? `
<span class="license-info">${data.license.spdx_id || data.license.name}</span>
` : ''}
</div>
</div>
`;
})
.catch(error => {
console.error('Error loading repository data:', error);
widget.innerHTML = `
<div class="github-widget-error">
<svg viewBox="0 0 16 16" width="16" height="16">
<path fill="currentColor" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"/>
</svg>
<span>Failed to load ${repo}</span>
<small>${error.message}</small>
</div>
`;
});
});
}
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', initializeGitHubWidgets);
// Watch for DOM changes (MkDocs Material dynamic content)
const observer = new MutationObserver((mutations) => {
let shouldReinitialize = false;
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// Check if any github-widget elements were added
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && // Element node
(node.classList?.contains('github-widget') ||
node.querySelector?.('.github-widget'))) {
shouldReinitialize = true;
}
});
}
});
if (shouldReinitialize) {
// Small delay to ensure DOM is stable
setTimeout(initializeGitHubWidgets, 100);
}
});
// Start observing
observer.observe(document.body, {
childList: true,
subtree: true
});
// Language color mapping (simplified version)
function getLanguageColor(language) {
const colors = {
'JavaScript': '#f1e05a',
'TypeScript': '#2b7489',
'Python': '#3572A5',
'Java': '#b07219',
'C++': '#f34b7d',
'C': '#555555',
'C#': '#239120',
'PHP': '#4F5D95',
'Ruby': '#701516',
'Go': '#00ADD8',
'Rust': '#dea584',
'Swift': '#ffac45',
'Kotlin': '#F18E33',
'Scala': '#c22d40',
'Shell': '#89e051',
'HTML': '#e34c26',
'CSS': '#563d7c',
'Vue': '#2c3e50',
'React': '#61dafb',
'Dockerfile': '#384d54'
};
return colors[language] || '#586069';
}

View File

@@ -1,339 +1,369 @@
{% extends "main.html" %}
{% block extrahead %}
{{ super() }}
<script>
{{ super() }}
<script>
// Add data attribute for CSS targeting
document.body.setAttribute('data-md-template', 'home');
</script>
</script>
{% endblock %}
{% block content %}
<div class="md-content--home">
<!-- Hero Grid Section -->
<section class="hero-grid">
<div class="grid-container">
<!-- Main Hero Block -->
<div class="hero-main grid-card">
<div class="meta-badge">🤯 This Site Runs On Changemaker Lite</div>
<h1>The Only FOSS Political Campaign Stack</h1>
<p>Stop paying $1,200/month to corporate platforms. Run your entire political campaign on $200 hardware with fewer corporate dependencies every day.</p>
<div class="hero-ctas">
<a href="#solution" class="btn btn-primary">See The Solution</a>
<a href="#get-started" class="btn btn-secondary">Get Started $200</a>
</div>
</div>
<!-- Problem Statement Block -->
<div class="hero-problem grid-card">
<h3>The Problem</h3>
<div class="problem-list">
<div class="problem-item">💸 NGP VAN: $1,000+/mo</div>
<div class="problem-item">📧 Mailchimp: $200+/mo</div>
<div class="problem-item">🔒 Your data: Their servers</div>
<div class="problem-item">👁️ Surveillance: Always on</div>
</div>
</div>
<!-- Solution Stats Block -->
<div class="hero-stats grid-card">
<h3>The Solution</h3>
<div class="stat-grid">
<div class="stat-item">
<div class="stat-number">$200</div>
<div class="stat-label">One-time cost</div>
</div>
<div class="stat-item">
<div class="stat-number">11</div>
<div class="stat-label">Campaign tools</div>
</div>
<div class="stat-item">
<div class="stat-number">100%</div>
<div class="stat-label">Your data</div>
</div>
<div class="stat-item">
<div class="stat-number"></div>
<div class="stat-label">Fewer corporate deps every day</div>
</div>
</div>
</div>
<!-- Trust Indicators -->
<div class="hero-trust grid-card">
<h3>Proven By</h3>
<div class="trust-items">
<div class="trust-item">
<span class="trust-icon"></span>
<span class="trust-text">Powers BNKops.com</span>
</div>
<div class="trust-item">
<span class="trust-icon">🏳️‍⚧️</span>
<span class="trust-text">Trans liberation tech</span>
</div>
<div class="trust-item">
<span class="trust-icon">🛡️</span>
<span class="trust-text">Zero surveillance</span>
</div>
<div class="trust-item">
<span class="trust-icon">🚀</span>
<span class="trust-text">30-min setup</span>
</div>
</div>
</div>
</div>
</section>
<!-- Hero Grid Section -->
<section class="hero-grid">
<div class="grid-container">
<!-- Main Hero Block -->
<div class="hero-main grid-card">
<div class="meta-badge">🤯 This Site Runs On Changemaker Lite</div>
<h1>The Only Free and Open Source Political Campaign Stack</h1>
<p>Stop paying thousands of dollars a month to corporate platforms just to be surveilled and your supports data sold to the highest bidder. Run your entire political campaign on ~$200 hardware and
with fewer corporate dependencies every day.</p>
<!-- Solution Grid -->
<section class="solution-grid" id="solution">
<div class="grid-container">
<div class="section-header">
<h2>Everything Your Campaign Needs</h2>
<p>Replace your entire SaaS stack with open source alternatives. <b>First free and open source canvassing solution included.</b></p>
</div>
<div class="services-grid">
<!-- Code Server -->
<div class="service-card grid-card">
<div class="service-icon">💻</div>
<h3>Code Server</h3>
<div class="service-replaces">Replaces: VS Code</div>
<ul class="service-features">
<li>Browser-based IDE</li>
<li>Customizable workspace</li>
<li>Live collaboration</li>
</ul>
<div class="service-tool">Code Server</div>
<div class="hero-ctas">
<a href="#solution" class="btn btn-primary">See The Solution</a>
<a href="#get-started" class="btn btn-secondary">Get Started</a>
</div>
</div>
<!-- MkDocs -->
<div class="service-card grid-card">
<div class="service-icon">📖</div>
<h3>Documentation</h3>
<div class="service-replaces">Replaces: Confluence</div>
<ul class="service-features">
<li>Markdown-based</li>
<li>Live preview</li>
<li>Static site generation</li>
</ul>
<div class="service-tool">MkDocs</div>
</div>
<!-- Listmonk -->
<div class="service-card grid-card">
<div class="service-icon">📧</div>
<h3>Email Campaigns</h3>
<div class="service-replaces">Replaces: Mailchimp</div>
<ul class="service-features">
<li>Unlimited subscribers</li>
<li>A/B testing</li>
<li>Analytics</li>
</ul>
<div class="service-tool">Listmonk</div>
</div>
<!-- NocoDB -->
<div class="service-card grid-card">
<div class="service-icon">📊</div>
<h3>Database & Canvassing</h3>
<div class="service-replaces">Replaces: Airtable</div>
<ul class="service-features">
<li>Contact management</li>
<li>Custom fields</li>
<li>Canvassing support</li>
</ul>
<div class="service-tool">NocoDB</div>
</div>
<!-- Map Viewer -->
<div class="service-card grid-card">
<div class="service-icon">🗺️</div>
<h3>Interactive Maps</h3>
<div class="service-replaces">Replaces: Google Maps</div>
<ul class="service-features">
<li>Real-time geolocation</li>
<li>Custom map layers</li>
<li>Location management</li>
</ul>
<div class="service-tool">Map Viewer</div>
</div>
<!-- Automation -->
<div class="service-card grid-card">
<div class="service-icon">🔄</div>
<h3>Workflow Automation</h3>
<div class="service-replaces">Replaces: Zapier</div>
<ul class="service-features">
<li>Workflow builder</li>
<li>API connections</li>
<li>Event triggers</li>
</ul>
<div class="service-tool">n8n</div>
</div>
<!-- QR Code Generator -->
<div class="service-card grid-card">
<div class="service-icon">📱</div>
<h3>QR Code Generator</h3>
<div class="service-replaces">Replaces: QR Code SaaS</div>
<ul class="service-features">
<li>Customizable codes</li>
<li>Batch generation</li>
<li>Static and dynamic codes</li>
</ul>
<div class="service-tool">Mini QR</div>
</div>
<!-- Gitea -->
<div class="service-card grid-card">
<div class="service-icon">🔧</div>
<h3>Version Control</h3>
<div class="service-replaces">Replaces: GitHub</div>
<ul class="service-features">
<li>Self-hosted Git</li>
<li>Collaborative development</li>
<li>Issue tracking</li>
</ul>
<div class="service-tool">Gitea</div>
</div>
</div>
</div>
</section>
<!-- Comparison Grid -->
<section class="comparison-grid">
<div class="grid-container">
<div class="section-header">
<h2>Corporate SaaS vs Changemaker Lite</h2>
<p>Why campaigns are switching to self-hosted FOSS</p>
</div>
<div class="comparison-table">
<div class="comparison-header grid-card">
<div></div>
<div class="compare-col">Corporate SaaS</div>
<div class="compare-col highlight">Changemaker Lite</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Monthly Cost</div>
<div class="compare-value bad">$1,200+</div>
<div class="compare-value good">$50 hosting</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Data Ownership</div>
<div class="compare-value bad">Their servers</div>
<div class="compare-value good">100% yours</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Surveillance</div>
<div class="compare-value bad">Always tracked</div>
<div class="compare-value good">Zero tracking</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Vendor Lock-in</div>
<div class="compare-value bad">Trapped forever</div>
<div class="compare-value good">Export anytime</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Customization</div>
<div class="compare-value bad">Limited options</div>
<div class="compare-value good">Full control</div>
</div>
</div>
</div>
</section>
<!-- Get Started Grid -->
<section class="get-started-grid" id="get-started">
<div class="grid-container">
<div class="section-header">
<h2>Start Your Digital Liberation</h2>
<p>Three ways to break free from corporate campaign tech</p>
</div>
<div class="options-grid">
<!-- Plug & Play -->
<div class="option-card grid-card featured">
<div class="option-badge">Most Popular</div>
<div class="option-icon">🚀</div>
<h3>Plug & Play</h3>
<div class="option-price">$200</div>
<div class="option-features">
<div class="feature">✓ Pre-configured hardware</div>
<div class="feature">✓ 30-minute setup</div>
<div class="feature">✓ All 11 services ready</div>
<div class="feature">✓ Priority support</div>
<div class="feature">✓ Ships on request</div>
<!-- Problem Statement Block -->
<div class="hero-problem grid-card">
<h3>The Problem</h3>
<div class="problem-list">
<div class="problem-item">💸 Canvassing: $100+/mo</div>
<div class="problem-item">📊 Nationbuilder: $500+/mo</div>
<div class="problem-item">📧 Mailchimp: $200+/mo</div>
<div class="problem-item">👁️ Surveillance: Always on</div>
<div class="problem-item">🔒 Vendor lock-in: Trapped forever</div>
<div class="problem-item">📉 Data ownership: Not yours</div>
<div class="promlem-itme">😠 Implicitly Supporting Evil Corporations</div>
</div>
<a href="https://bnkops.com/hardware" class="btn btn-primary">Order Hardware</a>
</div>
<!-- DIY -->
<div class="option-card grid-card">
<div class="option-icon">🛠️</div>
<h3>DIY Install</h3>
<div class="option-price">Free</div>
<div class="option-features">
<div class="feature">✓ Use your hardware</div>
<div class="feature">✓ Docker compose</div>
<div class="feature">✓ Full documentation</div>
<div class="feature">✓ Community support</div>
<div class="feature">✓ Learn the system</div>
<!-- Solution Stats Block -->
<div class="hero-stats grid-card">
<h3>The Solution</h3>
<div class="stat-grid">
<div class="stat-item">
<div class="stat-number">Runs on ~$200 Hardware</div>
<div class="stat-label">One-time cost</div>
</div>
<div class="stat-item">
<div class="stat-number">11</div>
<div class="stat-label">Campaign tools</div>
</div>
<div class="stat-item">
<div class="stat-number">Your Data</div>
<div class="stat-label">Localized & Secure</div>
</div>
<div class="stat-item">
<div class="stat-number"></div>
<div class="stat-label">Fewer corporate deps every day</div>
</div>
</div>
<a href="/getting-started/" class="btn btn-secondary">Install Guide</a>
</div>
<!-- Managed -->
<div class="option-card grid-card">
<div class="option-icon">☁️</div>
<h3>Managed Hosting</h3>
<div class="option-price">$???/mo</div>
<div class="option-features">
<div class="feature">✓ We manage everything</div>
<div class="feature">✓ Your domain</div>
<div class="feature">✓ Daily backups</div>
<div class="feature">✓ 24/7 monitoring</div>
<div class="feature">✓ Still your data</div>
<!-- Trust Indicators -->
<div class="hero-trust grid-card">
<h3>Proven By</h3>
<div class="trust-items">
<div class="trust-item">
<span class="trust-icon"></span>
<span class="trust-text">Powers BNKops.com</span>
</div>
<div class="trust-item">
<span class="trust-icon"> 🌈 </span>
<span class="trust-text">Liberation tech</span>
</div>
<div class="trust-item">
<span class="trust-icon">🛡️</span>
<span class="trust-text">Zero surveillance</span>
</div>
<div class="trust-item">
<span class="trust-icon">🚀</span>
<span class="trust-text">30-min setup</span>
</div>
</div>
<a href="https://bnkops.com/hosting" class="btn btn-secondary">Learn More</a>
</div>
</div>
<!-- Final CTA -->
<div class="final-cta grid-card">
<h3>Join the Movement</h3>
<p>Some of the biggest players in local politics are already using it; maybe give it a try?</p>
<div class="cta-buttons">
<a href="https://bnkops.com/newsletter" class="btn btn-primary">Get Campaign Tips</a>
<a href="https://docs.bnkops.com" class="btn btn-secondary">Read Documentation</a>
</section>
<!-- Solution Grid -->
<section class="solution-grid" id="solution">
<div class="grid-container">
<div class="section-header">
<h2>Your Complete Campaign Toolkit</h2>
<p>Simple, powerful tools designed for campaign staff - no technical skills needed</p>
</div>
<div class="services-grid">
<!-- Documentation Editor -->
<div class="service-card grid-card">
<div class="service-icon">✍️</div>
<h3>Campaign Content Editor</h3>
<div class="service-replaces">Replaces: Google Docs + WordPress (~$50/month)</div>
<ul class="service-features">
<li>Edit docs & blog posts</li>
<li>Live preview as you type</li>
<li>Simple formatting tools</li>
<li>Works in any browser</li>
</ul>
<div class="service-tool">Code Server</div>
</div>
<!-- Canvassing System -->
<div class="service-card grid-card featured">
<div class="service-badge">Field Team Essential</div>
<div class="service-icon">🚪</div>
<h3>Smart Canvassing</h3>
<div class="service-replaces">Replaces: Qumon (~$100+/mo)</div>
<ul class="service-features">
<li>Interactive voter maps</li>
<li>Track door knocks</li>
<li>Mobile-friendly</li>
<li>Works offline</li>
</ul>
<div class="service-tool">NocoDB + Map Viewer</div>
</div>
<!-- Email System -->
<div class="service-card grid-card">
<div class="service-icon">📩</div>
<h3>Email Blasts</h3>
<div class="service-replaces">Replaces: Mailchimp (~$200+/mo)</div>
<ul class="service-features">
<li>Simple email creator</li>
<li>Track opens/clicks</li>
<li>Unlimited subscribers</li>
<li>Pre-made templates</li>
</ul>
<div class="service-tool">Listmonk</div>
</div>
<!-- Voter Database -->
<div class="service-card grid-card">
<div class="service-icon">🗃️</div>
<h3>Voter Database</h3>
<div class="service-replaces">Replaces: NationBuilder (~$500+/mo)</div>
<ul class="service-features">
<li>Spreadsheet interface</li>
<li>Easy voter searches</li>
<li>Custom survey forms</li>
<li>Export walk lists</li>
</ul>
<div class="service-tool">NocoDB</div>
</div>
<!-- Automation -->
<div class="service-card grid-card">
<div class="service-icon">🤖</div>
<h3>Smart Automation</h3>
<div class="service-replaces">Replaces: Zapier (~$300/month)</div>
<ul class="service-features">
<li>Auto follow-up emails</li>
<li>Volunteer reminders</li>
<li>Donation thank yous</li>
<li>Pre-built workflows</li>
</ul>
<div class="service-tool">n8n</div>
</div>
<!-- Website -->
<div class="service-card grid-card">
<div class="service-icon">🌐</div>
<h3>Campaign Website</h3>
<div class="service-replaces">Replaces: Squarespace (~$100/month)</div>
<ul class="service-features">
<li>Pre-designed templates</li>
<li>Easy content updates</li>
<li>Built-in blog</li>
<li>Fast loading</li>
</ul>
<div class="service-tool">MkDocs</div>
</div>
<!-- Volunteer Management -->
<div class="service-card grid-card">
<div class="service-icon">👥</div>
<h3>Volunteer HQ</h3>
<div class="service-replaces">Replaces: Mobilize (~$200/month)</div>
<ul class="service-features">
<li>Shift scheduling</li>
<li>Automated reminders</li>
<li>Skills matching</li>
<li>Progress tracking</li>
</ul>
<div class="service-tool">NocoDB + n8n</div>
</div>
<!-- Linking Features -->
<div class="service-card grid-card">
<div class="service-icon">👀</div>
<h3> Mobile, Social, & Hot Link Friendly</h3>
<div class="service-replaces">Replaces: Linktree (~$20/month)</div>
<ul class="service-features">
<li>Automatic Social Cards</li>
<li>Hot Linking to All Assets & Headers</li>
<li>Documentation Indexing & Search</li>
<li>Progress tracking</li>
</ul>
<div class="service-tool">NocoDB + n8n</div>
</div>
<!-- Rapid Response -->
<div class="service-card grid-card">
<div class="service-icon"></div>
<h3>Rapid Response</h3>
<div class="service-replaces">Replaces: Slack + Google Docs (~$50/month)</div>
<ul class="service-features">
<li>Real-time collaboration</li>
<li>Community Editing</li>
<li>Open source documentation</li>
<li>Version control</li>
</ul>
<div class="service-tool">Code Server + Gitea</div>
</div>
</div>
</div>
<!-- Subscribe Card -->
<div class="subscribe-card grid-card">
<h3>Subscribe</h3>
<form action="https://newsletter.bnkops.com/subscription/form" method="POST" target="_blank" autocomplete="off">
<input type="hidden" name="nonce">
<p>
<input type="email" name="email" required placeholder="E-mail">
</p>
<p>
<input type="text" name="name" placeholder="Name (optional)">
</p>
<p>
<input id="48b84" type="checkbox" name="l" checked value="48b841e7-841b-458b-95f8-63903a1c0912">
<label for="48b84">~ Weekly Updates</label>
</p>
<input type="submit" value="Subscribe" class="action-btn primary">
</form>
</section>
<!-- Comparison Grid -->
<section class="comparison-grid">
<div class="grid-container">
<div class="section-header">
<h2>Corporate SaaS vs Changemaker Lite</h2>
<p>Why campaigns are switching to self-hosted FOSS</p>
</div>
<div class="comparison-table">
<div class="comparison-header grid-card">
<div></div>
<div class="compare-col">Corporate SaaS</div>
<div class="compare-col highlight">Changemaker Lite</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Monthly Cost</div>
<div class="compare-value bad">$1,200+</div>
<div class="compare-value good">$0 Self-Hosted</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Data Ownership</div>
<div class="compare-value bad">Their servers</div>
<div class="compare-value good">100% yours</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Surveillance</div>
<div class="compare-value bad">Always tracked</div>
<div class="compare-value good">Zero tracking</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Vendor Lock-in</div>
<div class="compare-value bad">Trapped forever</div>
<div class="compare-value good">Export anytime</div>
</div>
<div class="comparison-row grid-card">
<div class="compare-label">Customization</div>
<div class="compare-value bad">Limited options</div>
<div class="compare-value good">Full control</div>
</div>
</div>
</div>
</div>
</section>
</section>
<!-- Get Started Grid -->
<section class="get-started-grid" id="get-started">
<div class="grid-container">
<div class="section-header">
<h2>Start Your Digital Liberation</h2>
<p>Three ways to break free from corporate campaign tech</p>
</div>
<div class="options-grid">
<!-- Plug & Play -->
<div class="option-card grid-card featured">
<div class="option-badge">Most Popular</div>
<div class="option-icon">🚀</div>
<h3>Plug & Play</h3>
<div class="option-price">~$400 (Built to Suit)</div>
<div class="option-features">
<div class="feature">✓ Pre-configured hardware</div>
<div class="feature">✓ 30-minute setup</div>
<div class="feature">✓ All 11 services ready</div>
<div class="feature">✓ Priority support</div>
<div class="feature">✓ Ships on request</div>
</div>
<a href="https://bnkops.com/hardware" class="btn btn-primary">Order Hardware</a>
</div>
<!-- DIY -->
<div class="option-card grid-card">
<div class="option-icon">🛠️</div>
<h3>DIY Install</h3>
<div class="option-price">Free</div>
<div class="option-features">
<div class="feature">✓ Use your hardware</div>
<div class="feature">✓ Docker compose</div>
<div class="feature">✓ Full documentation</div>
<div class="feature">✓ Community support</div>
<div class="feature">✓ Learn the system</div>
</div>
<a href="/getting-started" class="btn btn-secondary">Install Guide</a>
</div>
<!-- Managed -->
<div class="option-card grid-card">
<div class="option-icon">☁️</div>
<h3>Managed Hosting</h3>
<div class="option-price">$???/mo</div>
<div class="option-features">
<div class="feature">✓ We manage everything</div>
<div class="feature">✓ Your domain</div>
<div class="feature">✓ Daily backups</div>
<div class="feature">✓ 24/7 monitoring</div>
<div class="feature">✓ Still your data</div>
</div>
<a href="https://bnkops.com" class="btn btn-secondary">Learn More</a>
</div>
</div>
<!-- Cards in a row -->
<div class="row-cards">
<div class="final-cta grid-card">
<h3>Join the Movement</h3>
<p>Some of the biggest players in local politics are already using it; maybe give it a try?</p>
<div class="cta-buttons">
<a href="https://bnkops.com/newsletter" class="btn btn-primary">Get Campaign Tips</a>
<a href="https://docs.bnkops.com" class="btn btn-secondary">Read Documentation</a>
</div>
</div>
<div class="subscribe-card grid-card">
<h3>Subscribe</h3>
<form action="https://newsletter.bnkops.com/subscription/form" method="POST" target="_blank"
autocomplete="off">
<input type="hidden" name="nonce">
<p>
<input type="email" name="email" required placeholder="E-mail">
</p>
<p>
<input type="text" name="name" placeholder="Name (optional)">
</p>
<p>
<input id="48b84" type="checkbox" name="l" checked
value="48b841e7-841b-458b-95f8-63903a1c0912">
<label for="48b84">~ Weekly Updates</label>
</p>
<input type="submit" value="Subscribe" class="action-btn primary">
</form>
</div>
</div>
</div>
</section>
</div>
{% endblock %}
{% endblock %}

View File

@@ -104,6 +104,8 @@ The tools we use shape the movements we build. Corporate tools create corporate
**No.** We specifically designed Changemaker Lite for organizers, activists, and movement builders who may not have technical backgrounds. Our philosophy is that everyone deserves digital sovereignty, not just people with computer science degrees.
This is not to say that you won't need to learn! These tools are just that; tools. They have no fancy or white-labeled marketing and are technical in nature. You will need to learn to use them, just as any worker needs to learn the power tools they use on the job.
### "What about convenience?"
Corporate platforms are convenient because they've extracted billions of dollars from users to fund that convenience. When you own your tools, there's a learning curve—but it's the same learning curve as learning to organize, learning to build power, learning to create change.

View File

@@ -1,6 +1,6 @@
# Code Server
Visual Studio Code in your browser for remote development.
<div class="github-widget" data-repo="coder/code-server"></div>
## Overview

View File

@@ -6,6 +6,7 @@ Changemaker Lite includes several powerful services that work together to provid
### [Code Server](code-server.md)
**Port: 8888** | Visual Studio Code in your browser for remote development
<div class="github-widget" data-repo="coder/code-server"></div>
- Full IDE experience
- Extensions support
- Git integration
@@ -13,6 +14,7 @@ Changemaker Lite includes several powerful services that work together to provid
### [Listmonk](listmonk.md)
**Port: 9000** | Self-hosted newsletter and mailing list manager
<div class="github-widget" data-repo="knadh/listmonk"></div>
- Email campaigns
- Subscriber management
- Analytics
@@ -27,6 +29,7 @@ Changemaker Lite includes several powerful services that work together to provid
### [MkDocs Material](mkdocs.md)
**Port: 4000** | Documentation site generator with live preview
<div class="github-widget" data-repo="squidfunk/mkdocs-material"></div>
- Material Design theme
- Live reload
- Search functionality
@@ -41,6 +44,7 @@ Changemaker Lite includes several powerful services that work together to provid
### [n8n](n8n.md)
**Port: 5678** | Workflow automation tool
<div class="github-widget" data-repo="n8n-io/n8n"></div>
- Visual workflow editor
- 400+ integrations
- Custom code execution
@@ -48,6 +52,7 @@ Changemaker Lite includes several powerful services that work together to provid
### [NocoDB](nocodb.md)
**Port: 8090** | No-code database platform
<div class="github-widget" data-repo="nocodb/nocodb"></div>
- Smart spreadsheet interface
- Form builder and API generation
- Real-time collaboration
@@ -55,6 +60,7 @@ Changemaker Lite includes several powerful services that work together to provid
### [Homepage](homepage.md)
**Port: 3010** | Modern dashboard for all services
<div class="github-widget" data-repo="gethomepage/homepage"></div>
- Service dashboard and monitoring
- Docker integration
- Customizable layout

View File

@@ -1,5 +1,7 @@
# n8n
<div class="github-widget" data-repo="n8n-io/n8n"></div>
Workflow automation tool for connecting services and automating tasks.
## Overview

View File

@@ -0,0 +1,554 @@
.login-button {
display: inline-block;
padding: 2px 10px;
margin-left: auto; /* Push the button to the right */
margin-right: 10px;
background-color: hsl(315, 80%, 42%); /* Use a solid, high-contrast color */
color: #fff; /* Ensure text is white */
text-decoration: none;
border-radius: 4px;
font-weight: bold;
transition: background-color 0.2s ease;
font-size: 0.9em;
vertical-align: middle;
}
.login-button:hover {
background-color: #003c8f; /* Darker shade for hover */
text-decoration: none;
}
.git-code-button {
display: inline-block;
background: #30363d;
color: white !important;
padding: 0.6rem 1.2rem;
border-radius: 20px;
text-decoration: none;
font-size: 0.95rem;
font-weight: bold;
margin-left: 1rem;
transition: all 0.3s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
}
.git-code-button:hover {
background: #444d56;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
text-decoration: none;
}
.git-code-button .material-icons {
font-size: 1rem;
vertical-align: middle;
margin-right: 4px;
}
/* GitHub Widget Styles */
.github-widget {
margin: 1.5rem 0;
display: block;
}
.github-widget-container {
border: 1px solid rgba(var(--md-primary-fg-color--rgb), 0.15);
border-radius: 12px;
padding: 20px;
background: linear-gradient(135deg, var(--md-code-bg-color) 0%, rgba(var(--md-primary-fg-color--rgb), 0.03) 100%);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.github-widget-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--md-primary-fg-color), var(--md-accent-fg-color));
border-radius: 12px 12px 0 0;
}
.github-widget-container:hover {
border-color: var(--md-accent-fg-color);
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.github-widget-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.github-widget-title {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.github-icon {
color: var(--md-default-fg-color--light);
flex-shrink: 0;
width: 20px;
height: 20px;
}
.github-widget .repo-link {
color: var(--md-accent-fg-color);
text-decoration: none;
font-weight: 600;
font-size: 16px;
transition: color 0.2s ease;
word-break: break-word;
}
.github-widget .repo-link:hover {
color: var(--md-primary-fg-color);
text-decoration: none;
}
.github-widget-stats {
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.stat-item {
display: flex;
align-items: center;
gap: 6px;
color: var(--md-default-fg-color);
font-size: 13px;
font-weight: 600;
background: rgba(var(--md-primary-fg-color--rgb), 0.08);
padding: 4px 8px;
border-radius: 16px;
transition: all 0.2s ease;
}
.stat-item:hover {
background: rgba(var(--md-accent-fg-color--rgb), 0.15);
transform: scale(1.05);
}
.stat-item svg {
color: var(--md-accent-fg-color);
width: 14px;
height: 14px;
}
.github-widget-description {
color: var(--md-default-fg-color--light);
margin-bottom: 16px;
line-height: 1.5;
font-size: 14px;
font-style: italic;
padding: 12px;
background: rgba(var(--md-default-fg-color--rgb), 0.03);
border-radius: 8px;
border-left: 3px solid var(--md-accent-fg-color);
}
.github-widget-footer {
display: flex;
gap: 20px;
align-items: center;
font-size: 12px;
color: var(--md-default-fg-color--lighter);
border-top: 1px solid rgba(var(--md-default-fg-color--rgb), 0.1);
padding-top: 16px;
margin-top: 16px;
flex-wrap: wrap;
}
.language-info {
display: flex;
align-items: center;
gap: 6px;
}
.language-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.last-update,
.license-info {
color: var(--md-default-fg-color--lighter);
}
/* Loading State */
.github-widget-loading {
display: flex;
align-items: center;
gap: 12px;
padding: 20px;
color: var(--md-default-fg-color--light);
justify-content: center;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid var(--md-default-fg-color--lightest);
border-top: 2px solid var(--md-accent-fg-color);
border-radius: 50%;
animation: github-widget-spin 1s linear infinite;
}
@keyframes github-widget-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Error State */
.github-widget-error {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px;
color: var(--md-typeset-color);
text-align: center;
background: var(--md-code-bg-color);
border: 1px solid #f85149;
border-radius: 6px;
}
.github-widget-error svg {
color: #f85149;
}
.github-widget-error small {
color: var(--md-default-fg-color--lighter);
font-size: 11px;
}
/* Dark mode specific adjustments */
[data-md-color-scheme="slate"] .github-widget-container {
background: var(--md-code-bg-color);
border-color: #30363d;
}
[data-md-color-scheme="slate"] .github-widget-container:hover {
border-color: var(--md-accent-fg-color);
}
/* Multiple widgets in a row */
.github-widgets-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.github-widgets-row .github-widget {
margin: 0;
}
/* Compact widget variant */
.github-widget.compact .github-widget-container {
padding: 12px;
}
.github-widget.compact .github-widget-header {
margin-bottom: 8px;
}
.github-widget.compact .github-widget-description {
display: none;
}
.github-widget.compact .github-widget-footer {
margin-top: 8px;
padding-top: 8px;
}
/* GitHub Widget Responsive - placed after existing mobile styles */
@media (max-width: 768px) {
.github-widget-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.github-widget-stats {
gap: 12px;
}
.github-widget-footer {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
/* Gitea Widget Styles */
.gitea-widget {
margin: 1.5rem 0;
display: block;
}
.gitea-widget-container {
border: 1px solid rgba(var(--md-primary-fg-color--rgb), 0.15);
border-radius: 12px;
padding: 20px;
background: linear-gradient(135deg, var(--md-code-bg-color) 0%, rgba(var(--md-primary-fg-color--rgb), 0.03) 100%);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.gitea-widget-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #609926, #89c442);
border-radius: 12px 12px 0 0;
}
.gitea-widget-container:hover {
border-color: #89c442;
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.gitea-widget-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
flex-wrap: wrap;
gap: 12px;
}
.gitea-widget-title {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.gitea-icon {
color: #89c442;
flex-shrink: 0;
width: 20px;
height: 20px;
}
.gitea-widget .repo-link {
color: #89c442;
text-decoration: none;
font-weight: 600;
font-size: 16px;
transition: color 0.2s ease;
word-break: break-word;
}
.gitea-widget .repo-link:hover {
color: #609926;
text-decoration: none;
}
.gitea-widget-stats {
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.gitea-widget .stat-item {
display: flex;
align-items: center;
gap: 6px;
color: var(--md-default-fg-color);
font-size: 13px;
font-weight: 600;
background: rgba(137, 196, 66, 0.1);
padding: 4px 8px;
border-radius: 16px;
transition: all 0.2s ease;
}
.gitea-widget .stat-item:hover {
background: rgba(137, 196, 66, 0.2);
transform: scale(1.05);
}
.gitea-widget .stat-item svg {
color: #89c442;
width: 14px;
height: 14px;
}
.gitea-widget-description {
color: var(--md-default-fg-color--light);
margin-bottom: 16px;
line-height: 1.5;
font-size: 14px;
font-style: italic;
padding: 12px;
background: rgba(var(--md-default-fg-color--rgb), 0.03);
border-radius: 8px;
border-left: 3px solid #89c442;
}
.gitea-widget-footer {
display: flex;
gap: 20px;
align-items: center;
font-size: 12px;
color: var(--md-default-fg-color--lighter);
border-top: 1px solid rgba(var(--md-default-fg-color--rgb), 0.1);
padding-top: 16px;
margin-top: 16px;
flex-wrap: wrap;
}
.gitea-widget .language-info {
display: flex;
align-items: center;
gap: 6px;
}
.gitea-widget .language-dot {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.gitea-widget .last-update,
.gitea-widget .license-info {
color: var(--md-default-fg-color--lighter);
}
/* Gitea Loading State */
.gitea-widget-loading {
display: flex;
align-items: center;
gap: 12px;
padding: 20px;
color: var(--md-default-fg-color--light);
justify-content: center;
}
.gitea-widget-loading .loading-spinner {
width: 20px;
height: 20px;
border: 2px solid var(--md-default-fg-color--lightest);
border-top: 2px solid #89c442;
border-radius: 50%;
animation: gitea-widget-spin 1s linear infinite;
}
@keyframes gitea-widget-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Gitea Error State */
.gitea-widget-error {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px;
color: var(--md-typeset-color);
text-align: center;
background: var(--md-code-bg-color);
border: 1px solid #f85149;
border-radius: 6px;
}
.gitea-widget-error svg {
color: #f85149;
}
.gitea-widget-error small {
color: var(--md-default-fg-color--lighter);
font-size: 11px;
}
/* Dark mode specific adjustments for Gitea */
[data-md-color-scheme="slate"] .gitea-widget-container {
background: var(--md-code-bg-color);
border-color: #30363d;
}
[data-md-color-scheme="slate"] .gitea-widget-container:hover {
border-color: #89c442;
}
/* Multiple Gitea widgets in a row */
.gitea-widgets-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.gitea-widgets-row .gitea-widget {
margin: 0;
}
/* Compact Gitea widget variant */
.gitea-widget.compact .gitea-widget-container {
padding: 12px;
}
.gitea-widget.compact .gitea-widget-header {
margin-bottom: 8px;
}
.gitea-widget.compact .gitea-widget-description {
display: none;
}
.gitea-widget.compact .gitea-widget-footer {
margin-top: 8px;
padding-top: 8px;
}
/* Gitea Widget Responsive */
@media (max-width: 768px) {
.gitea-widget-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.gitea-widget-stats {
gap: 12px;
}
.gitea-widget-footer {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}

View File

@@ -265,7 +265,7 @@ body[data-md-template="home"] .md-main__inner {
.stat-number {
font-size: 1.2rem;
font-weight: 700;
color: var(--trans-blue);
color: var(--trans-pink);
margin: 0;
animation: number-glow 4s ease-in-out infinite;
}
@@ -309,6 +309,8 @@ body[data-md-template="home"] .md-main__inner {
.section-header {
text-align: center;
margin-bottom: var(--space-lg);
position: relative;
z-index: 10;
}
.section-header h2 {
@@ -672,6 +674,21 @@ body[data-md-template="home"] .md-main__inner {
}
}
/* Row cards at bottom: two cards in a row on desktop, stack on mobile */
.row-cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-sm);
margin-top: var(--space-lg);
}
@media (max-width: 768px) {
.row-cards {
grid-template-columns: 1fr;
gap: var(--space-xs);
}
}
/* =================================
PERFORMANCE
================================= */