2026-04-30 19:07:17 -06:00

3694 lines
110 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Multi-tenant management for provisioning and operating multiple Changemaker Lite instances.">
<meta name="author" content="Bunker Operations">
<link rel="canonical" href="https://cmlite.org/docs/getting-started/control-panel/">
<link rel="prev" href="../upgrades/">
<link rel="next" href="../features/">
<link rel="icon" href="../../../assets/favicon.svg">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.6">
<title>Control Panel (CCP) - Changemaker Lite</title>
<link rel="stylesheet" href="../../../assets/stylesheets/main.484c7ddc.min.css">
<link rel="stylesheet" href="../../../assets/stylesheets/palette.ab4e12ef.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter:300,300i,400,400i,700,700i%7CJetBrains+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Inter";--md-code-font:"JetBrains Mono"}</style>
<link rel="stylesheet" href="../../../stylesheets/extra.css">
<link rel="stylesheet" href="../../../stylesheets/home.css">
<link rel="stylesheet" href="../../../stylesheets/docs-comments.css">
<link rel="stylesheet" href="../../../assets/css/video-player.css">
<link rel="stylesheet" href="../../../assets/css/image-gallery.css">
<link rel="stylesheet" href="../../../assets/css/payment-widgets.css">
<script>__md_scope=new URL("../../..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
<meta property="og:type" content="website" />
<meta property="og:title" content="Control Panel (CCP) - Changemaker Lite" />
<meta property="og:description" content="Multi-tenant management for provisioning and operating multiple Changemaker Lite instances." />
<meta property="og:image" content="https://cmlite.org/assets/images/social/docs/getting-started/control-panel.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:url" content="https://cmlite.org/docs/getting-started/control-panel/" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:title" content="Control Panel (CCP) - Changemaker Lite" />
<meta property="twitter:description" content="Multi-tenant management for provisioning and operating multiple Changemaker Lite instances." />
<meta property="twitter:image" content="https://cmlite.org/assets/images/social/docs/getting-started/control-panel.png" />
</head>
<body dir="ltr" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#changemaker-control-panel-ccp" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
<aside class="md-banner">
<div class="md-banner__inner md-grid md-typeset">
<button class="md-banner__button md-icon" aria-label="Don't show this again">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet">
<nav class="cm-header-nav" role="navigation" aria-label="Application">
<div class="cm-header-nav__brand">
<a href="#" data-path="/home" class="cm-header-nav__brand-link">
<span class="cm-header-nav__brand-text">Changemaker Lite</span>
</a>
</div>
<div class="cm-header-nav__links">
<div class="cm-header-nav__links-inner">
<a href="#" data-path="/" class="cm-header-nav__link" data-nav-id="home" target="_blank" rel="noopener noreferrer"><span class="material-icons-outlined">home</span><span class="cm-header-nav__label">Home</span></a>
<a href="#" data-path="/campaigns" class="cm-header-nav__link" data-nav-id="campaigns"><span class="material-icons-outlined">send</span><span class="cm-header-nav__label">Campaigns</span></a>
<a href="#" data-path="/map" class="cm-header-nav__link" data-nav-id="map"><span class="material-icons-outlined">place</span><span class="cm-header-nav__label">Map</span></a>
<a href="#" data-path="/shifts" class="cm-header-nav__link" data-nav-id="shifts"><span class="material-icons-outlined">event</span><span class="cm-header-nav__label">Shifts</span></a>
<a href="#" data-path="/events" class="cm-header-nav__link" data-nav-id="events" target="_blank" rel="noopener noreferrer"><span class="material-icons-outlined">event</span><span class="cm-header-nav__label">Events</span></a>
<a href="#" data-path="/gallery" class="cm-header-nav__link" data-nav-id="gallery"><span class="material-icons-outlined">play_circle</span><span class="cm-header-nav__label">Gallery</span></a>
<a href="#" data-path="/pricing" class="cm-header-nav__link" data-nav-id="pricing"><span class="material-icons-outlined">attach_money</span><span class="cm-header-nav__label">Pricing</span></a>
<a href="#" data-path="/shop" class="cm-header-nav__link" data-nav-id="shop"><span class="material-icons-outlined">shopping_bag</span><span class="cm-header-nav__label">Shop</span></a>
<a href="#" data-path="/donate" class="cm-header-nav__link" data-nav-id="donate"><span class="material-icons-outlined">favorite_border</span><span class="cm-header-nav__label">Donate</span></a>
<label for="__search" class="cm-header-nav__utility" title="Search">
<span class="material-icons-outlined">search</span>
</label>
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
<span class="material-icons-outlined">dark_mode</span>
</button>
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
<span class="material-icons-outlined">login</span>
<span class="cm-header-nav__label">Sign In</span>
</a>
<div class="cm-header-nav__dropdown" id="cm-admin-dropdown" style="display:none">
<span class="cm-header-nav__link cm-header-nav__dropdown-trigger">
<span class="material-icons-outlined">person</span>
<span class="cm-header-nav__label">Admin</span>
<span class="material-icons-outlined cm-header-nav__chevron">expand_more</span>
</span>
<div class="cm-header-nav__dropdown-menu cm-header-nav__dropdown-menu--right">
<a href="#" data-path="/app" class="cm-header-nav__dropdown-item"><span class="material-icons-outlined">dashboard</span><span>Admin Panel</span></a>
<a href="#" data-path="/volunteer" class="cm-header-nav__dropdown-item"><span class="material-icons-outlined">volunteer_activism</span><span>Volunteer Portal</span></a>
<a href="#" data-path="/volunteer/profile" class="cm-header-nav__dropdown-item"><span class="material-icons-outlined">account_circle</span><span>My Profile</span></a>
<a href="#" data-path="/logout" class="cm-header-nav__dropdown-item"><span class="material-icons-outlined">logout</span><span>Logout</span></a>
</div>
</div>
</div>
<button class="cm-header-nav__hamburger" aria-label="Open navigation menu">
<span class="material-icons-outlined">menu</span>
</button>
</div>
</nav>
<div class="cm-header-nav__mobile-drawer" id="cm-mobile-drawer">
<div class="cm-header-nav__mobile-header">
<span class="cm-header-nav__brand-text">Changemaker Lite</span>
<button class="cm-header-nav__mobile-close" aria-label="Close navigation menu">
<span class="material-icons-outlined">close</span>
</button>
</div>
<div class="cm-header-nav__mobile-links">
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
<span class="material-icons-outlined">search</span>
<span>Search</span>
</label>
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
<span class="material-icons-outlined">dark_mode</span>
<span>Dark Mode</span>
</button>
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
<span class="material-icons-outlined">menu_book</span>
<span>Docs Navigation</span>
</button>
<div class="cm-header-nav__mobile-divider"></div>
<a href="#" data-path="/" class="cm-header-nav__mobile-link" data-nav-id="home" target="_blank" rel="noopener noreferrer"><span class="material-icons-outlined">home</span><span>Home</span></a>
<a href="#" data-path="/campaigns" class="cm-header-nav__mobile-link" data-nav-id="campaigns"><span class="material-icons-outlined">send</span><span>Campaigns</span></a>
<a href="#" data-path="/map" class="cm-header-nav__mobile-link" data-nav-id="map"><span class="material-icons-outlined">place</span><span>Map</span></a>
<a href="#" data-path="/shifts" class="cm-header-nav__mobile-link" data-nav-id="shifts"><span class="material-icons-outlined">event</span><span>Shifts</span></a>
<a href="#" data-path="/events" class="cm-header-nav__mobile-link" data-nav-id="events" target="_blank" rel="noopener noreferrer"><span class="material-icons-outlined">event</span><span>Events</span></a>
<a href="#" data-path="/gallery" class="cm-header-nav__mobile-link" data-nav-id="gallery"><span class="material-icons-outlined">play_circle</span><span>Gallery</span></a>
<a href="#" data-path="/pricing" class="cm-header-nav__mobile-link" data-nav-id="pricing"><span class="material-icons-outlined">attach_money</span><span>Pricing</span></a>
<a href="#" data-path="/shop" class="cm-header-nav__mobile-link" data-nav-id="shop"><span class="material-icons-outlined">shopping_bag</span><span>Shop</span></a>
<a href="#" data-path="/donate" class="cm-header-nav__mobile-link" data-nav-id="donate"><span class="material-icons-outlined">favorite_border</span><span>Donate</span></a>
<div class="cm-header-nav__mobile-divider"></div>
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
<span class="material-icons-outlined">login</span>
<span>Sign In</span>
</a>
<div class="cm-header-nav__mobile-group" data-group-id="admin" id="cm-mobile-admin-group" style="display:none">
<span class="cm-header-nav__mobile-link cm-header-nav__mobile-group-trigger" role="button">
<span class="material-icons-outlined">person</span>
<span style="flex:1">Admin</span>
<span class="material-icons-outlined cm-header-nav__mobile-chevron">expand_more</span>
</span>
<div class="cm-header-nav__mobile-group-children">
<a href="#" data-path="/app" class="cm-header-nav__mobile-link" style="padding-left:48px"><span class="material-icons-outlined">dashboard</span><span>Admin Panel</span></a>
<a href="#" data-path="/volunteer" class="cm-header-nav__mobile-link" style="padding-left:48px"><span class="material-icons-outlined">volunteer_activism</span><span>Volunteer Portal</span></a>
<a href="#" data-path="/volunteer/profile" class="cm-header-nav__mobile-link" style="padding-left:48px"><span class="material-icons-outlined">account_circle</span><span>My Profile</span></a>
<a href="#" data-path="/logout" class="cm-header-nav__mobile-link" style="padding-left:48px"><span class="material-icons-outlined">logout</span><span>Logout</span></a>
</div>
</div>
</div>
</div>
<div class="cm-header-nav__mobile-overlay" id="cm-mobile-overlay"></div>
<script>
(function() {
var h = location.hostname;
var base;
if (h === 'localhost' || h === '127.0.0.1') {
base = location.protocol + '//localhost:' + (3002 || 3000);
} else {
var parts = h.split('.');
if (parts.length >= 3) { parts[0] = 'app'; }
else { parts.unshift('app'); }
base = location.protocol + '//' + parts.join('.');
}
var links = document.querySelectorAll('[data-path]');
for (var i = 0; i < links.length; i++) {
links[i].setAttribute('href', base + links[i].getAttribute('data-path'));
}
// Highlight active nav link based on current path
var path = location.pathname;
var activeLink = null;
if (path.indexOf('/docs') === 0) activeLink = 'docs';
document.querySelectorAll('.cm-header-nav__link[data-nav-id], .cm-header-nav__mobile-link[data-nav-id]').forEach(function(el) {
if (el.getAttribute('data-nav-id') === activeLink) {
el.classList.add('cm-header-nav__link--active');
}
});
// Hamburger toggle
var hamburger = document.querySelector('.cm-header-nav__hamburger');
var drawer = document.getElementById('cm-mobile-drawer');
var overlay = document.getElementById('cm-mobile-overlay');
var closeBtn = document.querySelector('.cm-header-nav__mobile-close');
function openDrawer() { drawer.classList.add('open'); overlay.classList.add('open'); }
function closeDrawer() { drawer.classList.remove('open'); overlay.classList.remove('open'); }
if (hamburger) hamburger.addEventListener('click', openDrawer);
if (closeBtn) closeBtn.addEventListener('click', closeDrawer);
if (overlay) overlay.addEventListener('click', closeDrawer);
// Mobile group expand/collapse toggles
document.querySelectorAll('.cm-header-nav__mobile-group-trigger').forEach(function(trigger) {
trigger.addEventListener('click', function() {
var group = this.closest('.cm-header-nav__mobile-group');
var children = group.querySelector('.cm-header-nav__mobile-group-children');
var isExpanded = group.classList.contains('expanded');
if (isExpanded) {
group.classList.remove('expanded');
children.style.display = 'none';
} else {
group.classList.add('expanded');
children.style.display = 'block';
}
});
});
// Auth-aware: show Admin dropdown for logged-in users, Sign In for guests.
// Uses hidden iframe + postMessage to read auth state from the app's origin.
function showAdminMenu() {
var s1 = document.getElementById('cm-signin-link');
var s2 = document.getElementById('cm-mobile-signin-link');
var a1 = document.getElementById('cm-admin-dropdown');
var a2 = document.getElementById('cm-mobile-admin-group');
if (s1) s1.style.display = 'none';
if (s2) s2.style.display = 'none';
if (a1) a1.style.display = '';
if (a2) a2.style.display = '';
}
// 1. Same-origin check (works when MkDocs served from same origin as app)
try {
var stored = localStorage.getItem('cml-auth');
if (stored) {
var parsed = JSON.parse(stored);
if (parsed && parsed.state && parsed.state.accessToken) {
showAdminMenu();
}
}
} catch(e) {}
// 2. Cross-origin check via hidden iframe + postMessage
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = base + '/auth-check.html?origin=' + encodeURIComponent(location.origin);
window.addEventListener('message', function(event) {
if (event.origin !== base) return;
if (event.data && event.data.type === 'cml-auth-status' && event.data.authenticated) {
showAdminMenu();
}
});
document.body.appendChild(iframe);
// Palette toggle (dark/light mode)
function togglePalette() {
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
for (var i = 0; i < inputs.length; i++) {
if (!inputs[i].checked) { inputs[i].click(); break; }
}
setTimeout(updatePaletteIcon, 50);
}
function updatePaletteIcon() {
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
var isDark = scheme === 'slate';
var icon = isDark ? 'light_mode' : 'dark_mode';
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
el.textContent = icon;
});
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
}
var ptBtn = document.getElementById('cm-palette-toggle');
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
// Docs sidebar toggle (opens Material's docs navigation drawer)
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
if (docsSidebarBtn) {
docsSidebarBtn.addEventListener('click', function() {
closeDrawer();
var dt = document.getElementById('__drawer');
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
});
}
// Close custom drawer when search label is clicked on mobile + auto-focus input
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
el.addEventListener('click', function() {
if (el.classList.contains('md-search__overlay')) return; // overlay has its own handler
closeDrawer();
setTimeout(function() {
var input = document.querySelector('.md-search__input');
if (input) input.focus();
}, 150);
});
});
// Search activation: Material may open search via checkbox OR by focusing the
// input directly (varies by version). Detect both and mirror as body class.
// NOTE: search DOM elements render AFTER the announce block in the template,
// so we must defer element queries until DOMContentLoaded.
var searchToggle = null;
var searchInput = null;
// Apply search layout inline styles (CSS-in-stylesheet is unreliable due to
// cross-origin Material stylesheets overriding !important rules)
function applySearchLayout(active) {
var inner = document.querySelector('.md-search__inner');
var output = document.querySelector('.md-search__output');
var scrollwrap = document.querySelector('.md-search__scrollwrap');
if (!inner) return;
var isDesktop = window.matchMedia('(min-width: 60em)').matches;
if (active) {
inner.style.setProperty('display', 'flex', 'important');
inner.style.setProperty('flex-direction', 'column', 'important');
inner.style.setProperty('overflow', 'hidden', 'important');
// Firefox needs explicit height (not just max-height) for flex children to grow
if (isDesktop) {
inner.style.setProperty('height', 'calc(100vh - 64px)', 'important');
}
if (output) {
output.style.setProperty('position', 'relative', 'important');
output.style.setProperty('flex', '1 1 0px', 'important');
output.style.setProperty('min-height', '0', 'important');
output.style.setProperty('display', 'flex', 'important');
output.style.setProperty('flex-direction', 'column', 'important');
output.style.setProperty('overflow', 'hidden', 'important');
output.style.setProperty('width', '100%', 'important');
}
if (scrollwrap) {
scrollwrap.style.setProperty('max-height', 'none', 'important');
scrollwrap.style.setProperty('flex', '1 1 0px', 'important');
scrollwrap.style.setProperty('min-height', '0', 'important');
scrollwrap.style.setProperty('overflow-y', 'auto', 'important');
}
// Force search result elements visible + ensure proper stacking (Firefox)
var resultList = document.querySelector('.md-search-result__list');
if (resultList) {
resultList.style.setProperty('display', 'block', 'important');
resultList.style.setProperty('visibility', 'visible', 'important');
resultList.style.setProperty('opacity', '1', 'important');
resultList.style.setProperty('max-height', 'none', 'important');
resultList.style.setProperty('overflow', 'visible', 'important');
resultList.style.setProperty('color', 'var(--md-default-fg-color)', 'important');
}
var resultContainer = document.querySelector('.md-search-result');
if (resultContainer) {
resultContainer.style.setProperty('display', 'block', 'important');
resultContainer.style.setProperty('visibility', 'visible', 'important');
resultContainer.style.setProperty('opacity', '1', 'important');
}
// Ensure scrollwrap has z-index above overlay
if (scrollwrap) {
scrollwrap.style.setProperty('position', 'relative', 'important');
scrollwrap.style.setProperty('z-index', '1', 'important');
scrollwrap.style.setProperty('background', 'var(--md-default-bg-color)', 'important');
}
} else {
inner.style.removeProperty('display');
inner.style.removeProperty('flex-direction');
inner.style.removeProperty('overflow');
inner.style.removeProperty('height');
if (output) {
output.style.removeProperty('position');
output.style.removeProperty('flex');
output.style.removeProperty('min-height');
output.style.removeProperty('display');
output.style.removeProperty('flex-direction');
output.style.removeProperty('overflow');
output.style.removeProperty('width');
}
if (scrollwrap) {
scrollwrap.style.removeProperty('max-height');
scrollwrap.style.removeProperty('flex');
scrollwrap.style.removeProperty('min-height');
scrollwrap.style.removeProperty('overflow-y');
scrollwrap.style.removeProperty('position');
scrollwrap.style.removeProperty('z-index');
scrollwrap.style.removeProperty('background');
}
var resultList = document.querySelector('.md-search-result__list');
if (resultList) resultList.removeAttribute('style');
var resultContainer = document.querySelector('.md-search-result');
if (resultContainer) resultContainer.removeAttribute('style');
}
}
function activateSearch() {
document.body.classList.add('cm-search-active');
if (searchToggle) searchToggle.checked = true;
applySearchLayout(true);
}
function deactivateSearch() {
document.body.classList.remove('cm-search-active');
if (searchToggle) searchToggle.checked = false;
if (searchInput) searchInput.blur();
applySearchLayout(false);
}
function isSearchActive() {
return document.body.classList.contains('cm-search-active');
}
// Custom search labels in the cm-header-nav (these exist now, in announce block)
document.querySelectorAll('label[for="__search"]').forEach(function(lbl) {
lbl.addEventListener('click', function() {
if (lbl.classList.contains('md-search__overlay')) return;
setTimeout(function() { activateSearch(); if (searchInput) searchInput.focus(); }, 50);
});
});
// Deferred bindings: attach handlers to search elements once they exist in the DOM
document.addEventListener('DOMContentLoaded', function() {
searchToggle = document.getElementById('__search');
searchInput = document.querySelector('.md-search__input');
// Detect search open via input focus
if (searchInput) {
searchInput.addEventListener('focus', activateSearch);
}
// Detect search open via checkbox
if (searchToggle) {
searchToggle.addEventListener('change', function() {
if (searchToggle.checked) activateSearch(); else deactivateSearch();
});
}
// Click on overlay (md-search__overlay label) to dismiss search
var searchOverlay = document.querySelector('.md-search__overlay');
if (searchOverlay) {
searchOverlay.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (isSearchActive()) deactivateSearch();
});
}
});
// Click-outside to dismiss search (on document, works immediately)
document.addEventListener('mousedown', function(e) {
if (!isSearchActive()) return;
var panel = document.querySelector('.md-search__inner');
if (panel && panel.contains(e.target)) return;
// Let the overlay's own click handler deal with it
if (e.target.closest && e.target.closest('.md-search__overlay')) return;
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
if (e.target.closest && e.target.closest('.cm-header-nav__utility')) return;
deactivateSearch();
});
// Escape key to dismiss
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isSearchActive()) setTimeout(deactivateSearch, 50);
});
// Init palette icon + observe changes
setTimeout(updatePaletteIcon, 100);
new MutationObserver(function() { updatePaletteIcon(); })
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
})();
</script>
<style>
.md-banner {
background: transparent !important;
color: #ffffff !important;
padding: 0 !important;
margin: 0 !important;
overflow: visible !important;
border: none !important;
box-shadow: none !important;
position: relative;
z-index: 301;
}
.md-banner__inner {
overflow: visible !important;
margin: 0 !important;
padding: 0 !important;
max-width: 100% !important;
}
.md-banner__button {
display: none !important;
}
.cm-header-nav {
background: linear-gradient(135deg, #005a9c 0%, #007acc 100%);
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
position: relative;
z-index: 100;
box-sizing: border-box;
}
.cm-header-nav a {
color: rgba(255, 255, 255, 0.85) !important;
}
.cm-header-nav__brand-link {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none !important;
color: #fff !important;
}
.cm-header-nav__brand-text {
font-size: 18px;
font-weight: 600;
color: #fff !important;
}
.cm-header-nav__links {
display: flex;
align-items: center;
}
.cm-header-nav__links-inner {
display: flex;
align-items: center;
gap: 16px;
}
.cm-header-nav__link {
color: rgba(255, 255, 255, 0.85) !important;
text-decoration: none !important;
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 14px;
transition: color 0.2s, border-color 0.2s;
white-space: nowrap;
padding-bottom: 2px;
border-bottom: 2px solid transparent;
}
.cm-header-nav__link:hover {
color: #fff !important;
text-decoration: none !important;
}
.cm-header-nav__link--active,
.cm-header-nav__link--active:hover {
color: #fff !important;
font-weight: 600;
border-bottom-color: #fff;
}
.cm-header-nav__link .material-icons-outlined {
font-size: 16px;
}
.cm-header-nav__hamburger {
display: none;
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
color: #fff;
}
.cm-header-nav__hamburger .material-icons-outlined {
font-size: 24px;
}
/* Desktop dropdown menus */
.cm-header-nav__dropdown {
position: relative;
display: inline-flex;
align-items: center;
}
.cm-header-nav__dropdown-trigger {
cursor: pointer;
user-select: none;
}
.cm-header-nav__dropdown-trigger .cm-header-nav__chevron {
font-size: 14px;
transition: transform 0.2s;
}
.cm-header-nav__dropdown:hover .cm-header-nav__chevron {
transform: rotate(180deg);
}
.cm-header-nav__dropdown-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
min-width: 180px;
background: #1b2838;
border-radius: 8px;
padding: 6px 0;
box-shadow: 0 6px 16px rgba(0,0,0,0.3);
z-index: 100;
margin-top: 4px;
}
.cm-header-nav__dropdown:hover .cm-header-nav__dropdown-menu {
display: block;
}
.cm-header-nav__dropdown-menu--right {
left: auto;
right: 0;
}
.cm-header-nav__dropdown-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
color: rgba(255, 255, 255, 0.85) !important;
text-decoration: none !important;
font-size: 14px;
white-space: nowrap;
transition: background 0.15s;
}
.cm-header-nav__dropdown-item:hover {
background: rgba(255,255,255,0.1);
color: #fff !important;
text-decoration: none !important;
}
.cm-header-nav__dropdown-item .material-icons-outlined {
font-size: 16px;
}
/* Mobile drawer */
.cm-header-nav__mobile-drawer {
position: fixed;
top: 0;
right: -280px;
width: 280px;
height: 100vh;
background: #0d1b2a;
z-index: 10001;
transition: right 0.3s ease;
display: flex;
flex-direction: column;
}
.cm-header-nav__mobile-drawer.open {
right: 0;
}
.cm-header-nav__mobile-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid rgba(255,255,255,0.1);
background: #1b2838;
}
.cm-header-nav__mobile-close {
background: none;
border: none;
cursor: pointer;
color: rgba(255,255,255,0.85);
padding: 4px;
}
.cm-header-nav__mobile-links {
display: flex;
flex-direction: column;
gap: 4px;
padding: 16px 0;
}
.cm-header-nav__mobile-link {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
color: rgba(255,255,255,0.85) !important;
text-decoration: none !important;
font-size: 15px;
border-radius: 4px;
}
.cm-header-nav__mobile-link:hover {
background: rgba(255,255,255,0.1);
color: #fff !important;
text-decoration: none !important;
}
.cm-header-nav__mobile-link--active {
color: #fff !important;
font-weight: 600;
background: rgba(255,255,255,0.1);
}
.cm-header-nav__mobile-link .material-icons-outlined {
font-size: 18px;
}
/* Mobile group expand/collapse */
.cm-header-nav__mobile-group-trigger {
cursor: pointer;
user-select: none;
}
.cm-header-nav__mobile-chevron {
font-size: 14px !important;
transition: transform 0.2s;
}
.cm-header-nav__mobile-group.expanded .cm-header-nav__mobile-chevron {
transform: rotate(180deg);
}
.cm-header-nav__mobile-group-children {
display: none;
}
.cm-header-nav__mobile-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10000;
}
.cm-header-nav__mobile-overlay.open {
display: block;
}
@media (max-width: 768px) {
.cm-header-nav { padding: 0 16px; }
.cm-header-nav__links-inner { display: none; }
.cm-header-nav__hamburger { display: block; }
.cm-header-nav__dropdown-menu { display: none !important; }
}
/* Sidebar sticky offset = 0 since blue header scrolls away */
:root {
--md-header-height: 0px;
}
/* Hidden Material header — keeps search anchored near tabs */
.md-header--cm-hidden {
height: 0 !important;
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: 0 !important;
overflow: visible !important;
background: transparent !important;
box-shadow: none !important;
position: sticky;
top: 0;
z-index: 200;
}
/* === DESKTOP SEARCH (>= 60em / 960px) === */
@media screen and (min-width: 60em) {
/* Fixed dropdown panel — layout (flex) applied via JS inline styles */
body.cm-search-active .md-header--cm-hidden .md-search__inner {
position: fixed !important;
top: 48px !important;
right: 16px !important;
left: auto !important;
width: min(34rem, calc(100vw - 32px)) !important;
max-height: calc(100vh - 64px) !important;
background: var(--md-default-bg-color) !important;
border-radius: 0 0 8px 8px !important;
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
z-index: 300 !important;
opacity: 1 !important;
transform: none !important;
visibility: visible !important;
pointer-events: auto !important;
clip-path: none !important;
}
/* Dark overlay behind search panel — catches clicks to dismiss */
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
background: rgba(0,0,0,0.54) !important;
opacity: 1 !important;
z-index: 299 !important;
border-radius: 0 !important;
transform: none !important;
cursor: default !important;
pointer-events: auto !important;
}
}
/* === MOBILE SEARCH (< 60em / 960px) === */
@media screen and (max-width: 59.984375em) {
/* Full-screen search takeover — layout (flex) applied via JS inline styles */
body.cm-search-active .md-header--cm-hidden .md-search__inner {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
height: 100% !important;
opacity: 1 !important;
transform: none !important;
visibility: visible !important;
pointer-events: auto !important;
z-index: 300 !important;
background: var(--md-default-bg-color) !important;
clip-path: none !important;
}
}
/* Force search elements visible when active (layout handled by JS inline styles) */
body.cm-search-active .md-header--cm-hidden .md-search {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
overflow: visible !important;
}
body.cm-search-active .md-header--cm-hidden .md-search__output {
opacity: 1 !important;
visibility: visible !important;
clip-path: none !important;
transform: none !important;
}
.cm-palette-container {
height: 0 !important;
overflow: hidden !important;
}
/* Material tabs: sticky at viewport top when blue header scrolls away */
.md-tabs {
position: sticky;
top: 0;
z-index: 99;
}
/* On mobile, hide tabs (sidebar provides navigation) */
@media (max-width: 768px) {
.md-tabs { display: none; }
}
/* Utility icon styling */
.cm-header-nav__utility {
background: none;
border: none;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
padding: 4px;
display: inline-flex;
align-items: center;
transition: color 0.2s;
}
.cm-header-nav__utility:hover { color: #fff; }
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
.cm-header-nav__utility-btn {
background: none;
border: none;
color: rgba(255,255,255,0.85);
cursor: pointer;
font-size: 15px;
font-family: inherit;
width: 100%;
text-align: left;
}
.cm-header-nav__mobile-divider {
height: 1px;
background: rgba(255,255,255,0.1);
margin: 8px 24px;
}
</style>
</div>
<script>var el=document.querySelector("[data-md-component=announce]");if(el){var content=el.querySelector(".md-typeset");__md_hash(content.innerHTML)===__md_get("__announce")&&(el.hidden=!0)}</script>
</aside>
</div>
<header class="md-header md-header--cm-hidden" data-md-component="header">
<div class="cm-palette-container">
<form class="md-header__option" data-md-component="palette">
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to light mode" type="radio" name="__palette" id="__palette_0">
<label class="md-header__button md-icon" title="Switch to light mode" for="__palette_1" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m17.75 4.09-2.53 1.94.91 3.06-2.63-1.81-2.63 1.81.91-3.06-2.53-1.94L12.44 4l1.06-3 1.06 3zm3.5 6.91-1.64 1.25.59 1.98-1.7-1.17-1.7 1.17.59-1.98L15.75 11l2.06-.05L18.5 9l.69 1.95zm-2.28 4.95c.83-.08 1.72 1.1 1.19 1.85-.32.45-.66.87-1.08 1.27C15.17 23 8.84 23 4.94 19.07c-3.91-3.9-3.91-10.24 0-14.14.4-.4.82-.76 1.27-1.08.75-.53 1.93.36 1.85 1.19-.27 2.86.69 5.83 2.89 8.02a9.96 9.96 0 0 0 8.02 2.89m-1.64 2.02a12.08 12.08 0 0 1-7.8-3.47c-2.17-2.19-3.33-5-3.49-7.82-2.81 3.14-2.7 7.96.31 10.98 3.02 3.01 7.84 3.12 10.98.31"/></svg>
</label>
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="deep-purple" data-md-color-accent="amber" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_0" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3m0-7 2.39 3.42C13.65 5.15 12.84 5 12 5s-1.65.15-2.39.42zM3.34 7l4.16-.35A7.2 7.2 0 0 0 5.94 8.5c-.44.74-.69 1.5-.83 2.29zm.02 10 1.76-3.77a7.131 7.131 0 0 0 2.38 4.14zM20.65 7l-1.77 3.79a7.02 7.02 0 0 0-2.38-4.15zm-.01 10-4.14.36c.59-.51 1.12-1.14 1.54-1.86.42-.73.69-1.5.83-2.29zM12 22l-2.41-3.44c.74.27 1.55.44 2.41.44.82 0 1.63-.17 2.37-.44z"/></svg>
</label>
</form>
</div>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<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>
</label>
<nav class="md-search__options" aria-label="Search">
<a href="javascript:void(0)" class="md-search__icon md-icon" title="Share" aria-label="Share" data-clipboard data-clipboard-text="" data-md-component="search-share" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9a3 3 0 0 0-3 3 3 3 0 0 0 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.15c-.05.21-.08.43-.08.66 0 1.61 1.31 2.91 2.92 2.91s2.92-1.3 2.92-2.91A2.92 2.92 0 0 0 18 16.08"/></svg>
</a>
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
</header>
<div class="md-container" data-md-component="container">
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
<div class="md-grid">
<ul class="md-tabs__list">
<li class="md-tabs__item">
<a href="../../.." class="md-tabs__link">
Home
</a>
</li>
<li class="md-tabs__item md-tabs__item--active">
<a href="../../" class="md-tabs__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
Docs
</a>
</li>
<li class="md-tabs__item">
<a href="../../../blog/" class="md-tabs__link">
Blog
</a>
</li>
</ul>
</div>
</nav>
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href="../../.." title="Changemaker Lite" class="md-nav__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
<img src="../../../assets/logo.svg" alt="logo">
</a>
Changemaker Lite
</label>
<div class="md-nav__source">
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
changemaker.lite
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../../.." class="md-nav__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active md-nav__item--section md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../../" class="md-nav__link ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
<span class="md-ellipsis">
Docs
</span>
</a>
<label class="md-nav__link " for="__nav_2" id="__nav_2_label" tabindex="">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2">
<span class="md-nav__icon md-icon"></span>
Docs
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item md-nav__item--active md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_2_2" checked>
<div class="md-nav__link md-nav__container">
<a href="../" class="md-nav__link ">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m13.13 22.19-1.63-3.83c1.57-.58 3.04-1.36 4.4-2.27zM5.64 12.5l-3.83-1.63 6.1-2.77C7 9.46 6.22 10.93 5.64 12.5M21.61 2.39S16.66.269 11 5.93c-2.19 2.19-3.5 4.6-4.35 6.71-.28.75-.09 1.57.46 2.13l2.13 2.12c.55.56 1.37.74 2.12.46A19.1 19.1 0 0 0 18.07 13c5.66-5.66 3.54-10.61 3.54-10.61m-7.07 7.07c-.78-.78-.78-2.05 0-2.83s2.05-.78 2.83 0c.77.78.78 2.05 0 2.83s-2.05.78-2.83 0m-5.66 7.07-1.41-1.41zM6.24 22l3.64-3.64c-.34-.09-.67-.24-.97-.45L4.83 22zM2 22h1.41l4.77-4.76-1.42-1.41L2 20.59zm0-2.83 4.09-4.08c-.21-.3-.36-.62-.45-.97L2 17.76z"/></svg>
<span class="md-ellipsis">
Getting Started
</span>
</a>
<label class="md-nav__link " for="__nav_2_2" id="__nav_2_2_label" tabindex="0">
<span class="md-nav__icon md-icon"></span>
</label>
</div>
<nav class="md-nav" data-md-level="2" aria-labelledby="__nav_2_2_label" aria-expanded="true">
<label class="md-nav__title" for="__nav_2_2">
<span class="md-nav__icon md-icon"></span>
Getting Started
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../prerequisites/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1s-2.4.84-2.82 2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2m-7 0a1 1 0 0 1 1 1 1 1 0 0 1-1 1 1 1 0 0 1-1-1 1 1 0 0 1 1-1M7 7h10V5h2v14H5V5h2zm.5 6.5L9 12l2 2 4.5-4.5L17 11l-6 6z"/></svg>
<span class="md-ellipsis">
Prerequisites
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../installation/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7z"/></svg>
<span class="md-ellipsis">
Installation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../services/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.81 10.25c-.06-.04-.56-.43-1.64-.43-.28 0-.56.03-.84.08-.21-1.4-1.38-2.11-1.43-2.14l-.29-.17-.18.27c-.24.36-.43.77-.51 1.19-.2.8-.08 1.56.33 2.21-.49.28-1.29.35-1.46.35H2.62c-.34 0-.62.28-.62.63 0 1.15.18 2.3.58 3.38.45 1.19 1.13 2.07 2 2.61.98.6 2.59.94 4.42.94.79 0 1.61-.07 2.42-.22 1.12-.2 2.2-.59 3.19-1.16A8.3 8.3 0 0 0 16.78 16c1.05-1.17 1.67-2.5 2.12-3.65h.19c1.14 0 1.85-.46 2.24-.85.26-.24.45-.53.59-.87l.08-.24zm-17.96.99h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H3.85c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.43 0h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H6.28c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.47 0h1.75c.1 0 .17-.07.17-.16V9.5c0-.08-.06-.16-.17-.16H8.75c-.08 0-.15.07-.15.16v1.58c0 .09.06.16.15.16m2.44 0h1.77c.08 0 .15-.07.15-.16V9.5c0-.08-.06-.16-.15-.16h-1.77c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16M6.28 9h1.76c.08 0 .16-.09.16-.18V7.25c0-.09-.07-.16-.16-.16H6.28c-.09 0-.16.06-.16.16v1.57c.01.09.07.18.16.18m2.47 0h1.75c.1 0 .17-.09.17-.18V7.25c0-.09-.06-.16-.17-.16H8.75c-.08 0-.15.06-.15.16v1.57c0 .09.06.18.15.18m2.44 0h1.77c.08 0 .15-.09.15-.18V7.25c0-.09-.07-.16-.15-.16h-1.77c-.08 0-.15.06-.15.16v1.57c0 .09.07.18.15.18m0-2.28h1.77c.08 0 .15-.07.15-.16V5c0-.1-.07-.17-.15-.17h-1.77c-.08 0-.15.06-.15.17v1.56c0 .08.07.16.15.16m2.46 4.52h1.76c.09 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16h-1.76c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16"/></svg>
<span class="md-ellipsis">
Services Overview
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../environment-variables/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M6 2c-1.11 0-2 .89-2 2v16a2 2 0 0 0 2 2h6.68a7 7 0 0 1-.68-3 7 7 0 0 1 7-7 7 7 0 0 1 1 .08V8l-6-6zm7 1.5L18.5 9H13zM18 14a.26.26 0 0 0-.26.21l-.19 1.32c-.3.13-.59.29-.85.47l-1.24-.5c-.11 0-.24 0-.31.13l-1 1.73c-.06.11-.04.24.06.32l1.06.82a4.2 4.2 0 0 0 0 1l-1.06.82a.26.26 0 0 0-.06.32l1 1.73c.06.13.19.13.31.13l1.24-.5c.26.18.54.35.85.47l.19 1.32c.02.12.12.21.26.21h2c.11 0 .22-.09.24-.21l.19-1.32c.3-.13.57-.29.84-.47l1.23.5c.13 0 .26 0 .33-.13l1-1.73a.26.26 0 0 0-.06-.32l-1.07-.82c.02-.17.04-.33.04-.5s-.01-.33-.04-.5l1.06-.82a.26.26 0 0 0 .06-.32l-1-1.73c-.06-.13-.19-.13-.32-.13l-1.23.5c-.27-.18-.54-.35-.85-.47l-.19-1.32A.236.236 0 0 0 20 14zm1 3.5c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5c-.84 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5"/></svg>
<span class="md-ellipsis">
Environment Variables
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../first-steps/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10.74 11.72c.47 1.23.42 2.51-.99 3.02-2.9 1.07-3.55-1.74-3.59-1.88zm-5.03-.81 4.32-1.07c-.19-1.05.1-2.1.1-3.34 0-1.68-1.33-4.97-3.45-4.44-2.42.6-2.77 3.29-2.68 4.59.12 1.3 1.64 4.08 1.71 4.26m12.14 8.94c-.03.15-.69 2.95-3.59 1.89-1.4-.52-1.46-1.8-.99-3.03zm2.15-6.2c.1-1.3-.24-4-2.67-4.6-2.11-.55-3.44 2.76-3.44 4.45 0 1.23.28 2.28.11 3.33l4.3 1.07c.08-.18 1.59-2.96 1.7-4.25"/></svg>
<span class="md-ellipsis">
First Steps
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../upgrades/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 10.12h-6.78l2.74-2.82c-2.73-2.7-7.15-2.8-9.88-.1a6.887 6.887 0 0 0 0 9.8c2.73 2.7 7.15 2.7 9.88 0 1.36-1.35 2.04-2.92 2.04-4.9h2c0 1.98-.88 4.55-2.64 6.29-3.51 3.48-9.21 3.48-12.72 0-3.5-3.47-3.53-9.11-.02-12.58a8.987 8.987 0 0 1 12.65 0L21 3zM12.5 8v4.25l3.5 2.08-.72 1.21L11 13V8z"/></svg>
<span class="md-ellipsis">
Updates & Upgrades
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 19V7H4v12zm0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm-7 14v-2h5v2zm-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z"/></svg>
<span class="md-ellipsis">
Control Panel (CCP)
</span>
<span class="md-status md-status--new" title="Recently added">
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 19V7H4v12zm0-16a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm-7 14v-2h5v2zm-3.42-4L5.57 9H8.4l3.3 3.3c.39.39.39 1.03 0 1.42L8.42 17H5.59z"/></svg>
<span class="md-ellipsis">
Control Panel (CCP)
</span>
<span class="md-status md-status--new" title="Recently added">
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#when-to-use-ccp" class="md-nav__link">
<span class="md-ellipsis">
When to Use CCP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#architecture" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#setup" class="md-nav__link">
<span class="md-ellipsis">
Setup
</span>
</a>
<nav class="md-nav" aria-label="Setup">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#1-run-the-setup-script" class="md-nav__link">
<span class="md-ellipsis">
1. Run the Setup Script
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-review-environment" class="md-nav__link">
<span class="md-ellipsis">
2. Review Environment
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#3-start-ccp" class="md-nav__link">
<span class="md-ellipsis">
3. Start CCP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#4-log-in" class="md-nav__link">
<span class="md-ellipsis">
4. Log In
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#creating-an-instance" class="md-nav__link">
<span class="md-ellipsis">
Creating an Instance
</span>
</a>
<nav class="md-nav" aria-label="Creating an Instance">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#step-1-basic-information" class="md-nav__link">
<span class="md-ellipsis">
Step 1: Basic Information
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-2-features" class="md-nav__link">
<span class="md-ellipsis">
Step 2: Features
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-3-email" class="md-nav__link">
<span class="md-ellipsis">
Step 3: Email
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-4-tunnel" class="md-nav__link">
<span class="md-ellipsis">
Step 4: Tunnel
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-5-review" class="md-nav__link">
<span class="md-ellipsis">
Step 5: Review
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#provisioning-flow" class="md-nav__link">
<span class="md-ellipsis">
Provisioning Flow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#port-allocation" class="md-nav__link">
<span class="md-ellipsis">
Port Allocation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pages-overview" class="md-nav__link">
<span class="md-ellipsis">
Pages Overview
</span>
</a>
<nav class="md-nav" aria-label="Pages Overview">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#dashboard" class="md-nav__link">
<span class="md-ellipsis">
Dashboard
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#instance-list" class="md-nav__link">
<span class="md-ellipsis">
Instance List
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#instance-detail" class="md-nav__link">
<span class="md-ellipsis">
Instance Detail
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#backups" class="md-nav__link">
<span class="md-ellipsis">
Backups
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#audit-log" class="md-nav__link">
<span class="md-ellipsis">
Audit Log
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#settings" class="md-nav__link">
<span class="md-ellipsis">
Settings
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#roles" class="md-nav__link">
<span class="md-ellipsis">
Roles
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#security" class="md-nav__link">
<span class="md-ellipsis">
Security
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#registering-an-existing-install-phone-home" class="md-nav__link">
<span class="md-ellipsis">
Registering an Existing Install (Phone-Home)
</span>
</a>
<nav class="md-nav" aria-label="Registering an Existing Install (Phone-Home)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#1-generate-an-invite-code-in-ccp" class="md-nav__link">
<span class="md-ellipsis">
1. Generate an invite code in CCP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-install-the-target-with-ccp-flags" class="md-nav__link">
<span class="md-ellipsis">
2. Install the target with --ccp-* flags
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#3-approve-in-ccp" class="md-nav__link">
<span class="md-ellipsis">
3. Approve in CCP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#4-deregister-on-teardown" class="md-nav__link">
<span class="md-ellipsis">
4. Deregister on teardown
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#next-steps" class="md-nav__link">
<span class="md-ellipsis">
Next Steps
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../features/" class="md-nav__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m18.09 11.77 1.47 6.33L14 14.74 8.44 18.1l1.46-6.33L5 7.5l6.47-.54L14 1l2.53 5.96L23 7.5zM2 12.43c.19 0 .38-.06.55-.17l3.2-2.11-1.57-1.36-2.73 1.8c-.461.3-.589.91-.29 1.41.2.27.52.43.84.43m-.84 9.12c.2.29.52.45.84.45.19 0 .38-.05.55-.16l4.11-2.71.34-1.37.31-1.45-5.86 3.85c-.461.31-.589.93-.29 1.39m.29-6.17a1 1 0 0 0-.29 1.38c.2.3.52.45.84.45.19 0 .38-.05.55-.16l5.42-3.55.27-1.19-.92-.81z"/></svg>
<span class="md-ellipsis">
Features at a Glance
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../admin/" class="md-nav__link">
<span class="md-ellipsis">
Admin Guide
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../user-guide/" class="md-nav__link">
<span class="md-ellipsis">
User Guide
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../volunteer/" class="md-nav__link">
<span class="md-ellipsis">
Volunteer Guide
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../deployment/" class="md-nav__link">
<span class="md-ellipsis">
Deployment
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../architecture/" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../services/" class="md-nav__link">
<span class="md-ellipsis">
Services
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../api/" class="md-nav__link">
<span class="md-ellipsis">
API Reference
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../troubleshooting/" class="md-nav__link">
<span class="md-ellipsis">
Troubleshooting
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
<li class="md-nav__item">
<a href="../../phil/" class="md-nav__link">
<span class="md-ellipsis">
Philosophy
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
<a href="../../../blog/" class="md-nav__link">
<span class="md-ellipsis">
Blog
</span>
<span class="md-nav__icon md-icon"></span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="On this page">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
On this page
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#when-to-use-ccp" class="md-nav__link">
<span class="md-ellipsis">
When to Use CCP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#architecture" class="md-nav__link">
<span class="md-ellipsis">
Architecture
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#setup" class="md-nav__link">
<span class="md-ellipsis">
Setup
</span>
</a>
<nav class="md-nav" aria-label="Setup">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#1-run-the-setup-script" class="md-nav__link">
<span class="md-ellipsis">
1. Run the Setup Script
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-review-environment" class="md-nav__link">
<span class="md-ellipsis">
2. Review Environment
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#3-start-ccp" class="md-nav__link">
<span class="md-ellipsis">
3. Start CCP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#4-log-in" class="md-nav__link">
<span class="md-ellipsis">
4. Log In
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#creating-an-instance" class="md-nav__link">
<span class="md-ellipsis">
Creating an Instance
</span>
</a>
<nav class="md-nav" aria-label="Creating an Instance">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#step-1-basic-information" class="md-nav__link">
<span class="md-ellipsis">
Step 1: Basic Information
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-2-features" class="md-nav__link">
<span class="md-ellipsis">
Step 2: Features
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-3-email" class="md-nav__link">
<span class="md-ellipsis">
Step 3: Email
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-4-tunnel" class="md-nav__link">
<span class="md-ellipsis">
Step 4: Tunnel
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#step-5-review" class="md-nav__link">
<span class="md-ellipsis">
Step 5: Review
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#provisioning-flow" class="md-nav__link">
<span class="md-ellipsis">
Provisioning Flow
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#port-allocation" class="md-nav__link">
<span class="md-ellipsis">
Port Allocation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#pages-overview" class="md-nav__link">
<span class="md-ellipsis">
Pages Overview
</span>
</a>
<nav class="md-nav" aria-label="Pages Overview">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#dashboard" class="md-nav__link">
<span class="md-ellipsis">
Dashboard
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#instance-list" class="md-nav__link">
<span class="md-ellipsis">
Instance List
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#instance-detail" class="md-nav__link">
<span class="md-ellipsis">
Instance Detail
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#backups" class="md-nav__link">
<span class="md-ellipsis">
Backups
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#audit-log" class="md-nav__link">
<span class="md-ellipsis">
Audit Log
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#settings" class="md-nav__link">
<span class="md-ellipsis">
Settings
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#roles" class="md-nav__link">
<span class="md-ellipsis">
Roles
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#security" class="md-nav__link">
<span class="md-ellipsis">
Security
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#registering-an-existing-install-phone-home" class="md-nav__link">
<span class="md-ellipsis">
Registering an Existing Install (Phone-Home)
</span>
</a>
<nav class="md-nav" aria-label="Registering an Existing Install (Phone-Home)">
<ul class="md-nav__list">
<li class="md-nav__item">
<a href="#1-generate-an-invite-code-in-ccp" class="md-nav__link">
<span class="md-ellipsis">
1. Generate an invite code in CCP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#2-install-the-target-with-ccp-flags" class="md-nav__link">
<span class="md-ellipsis">
2. Install the target with --ccp-* flags
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#3-approve-in-ccp" class="md-nav__link">
<span class="md-ellipsis">
3. Approve in CCP
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#4-deregister-on-teardown" class="md-nav__link">
<span class="md-ellipsis">
4. Deregister on teardown
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="#next-steps" class="md-nav__link">
<span class="md-ellipsis">
Next Steps
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<nav class="md-path" aria-label="Navigation" >
<ol class="md-path__list">
<li class="md-path__item">
<a href="../../.." class="md-path__link">
<span class="md-ellipsis">
Home
</span>
</a>
</li>
<li class="md-path__item">
<a href="../../" class="md-path__link">
<span class="md-ellipsis">
Docs
</span>
</a>
</li>
<li class="md-path__item">
<a href="../" class="md-path__link">
<span class="md-ellipsis">
Getting Started
</span>
</a>
</li>
</ol>
</nav>
<article class="md-content__inner md-typeset">
<nav class="md-tags" >
<span class="md-tag">guide</span>
<span class="md-tag">multi-tenant</span>
<span class="md-tag">operator</span>
</nav>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/getting-started/control-panel.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>
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/getting-started/control-panel.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>
<h1 id="changemaker-control-panel-ccp">Changemaker Control Panel (CCP)<a class="headerlink" href="#changemaker-control-panel-ccp" title="Permanent link">&para;</a></h1>
<div class="admonition tip">
<p class="admonition-title">Need help getting set up?</p>
<p><strong>Bunker Operations</strong> provides managed infrastructure and hands-on setup assistance for organizations running Changemaker Lite. We handle domains, tunnels, SMTP, and servers so you can focus on your campaign. <strong>Get in touch:</strong> <a href="https://bnkops.com">bnkops.com</a> | <code>admin@bnkops.ca</code></p>
</div>
<p>The Changemaker Control Panel is a <strong>multi-tenant management layer</strong> for operators who run multiple Changemaker Lite instances from a single server. It provides a web UI to provision, monitor, and maintain a fleet of instances without manual configuration.</p>
<div class="admonition info">
<p class="admonition-title">Single instance?</p>
<p>If you're running a single Changemaker Lite instance, you don't need CCP. Skip this page and continue with <a href="../first-steps/">First Steps</a>.</p>
</div>
<hr />
<h2 id="when-to-use-ccp">When to Use CCP<a class="headerlink" href="#when-to-use-ccp" title="Permanent link">&para;</a></h2>
<p>CCP is designed for:</p>
<ul>
<li><strong>Campaign organizations</strong> managing instances for multiple chapters or regions</li>
<li><strong>Hosting providers</strong> offering Changemaker Lite as a managed service</li>
<li><strong>Development teams</strong> spinning up isolated test instances</li>
</ul>
<p>CCP handles the entire instance lifecycle: provisioning, configuration, health monitoring, backups, and upgrades — all from a single dashboard.</p>
<hr />
<h2 id="architecture">Architecture<a class="headerlink" href="#architecture" title="Permanent link">&para;</a></h2>
<p>CCP runs as 4 Docker containers alongside (but independent from) your CML instances:</p>
<div class="language-text highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a>┌──────────────────────────┐
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a>│ CCP Admin GUI (5100) │ React + Vite + Ant Design
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a>│ Dark theme, SPA │ Zustand auth store
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a>└────────────┬─────────────┘
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a>
</span><span id="__span-0-6"><a id="__codelineno-0-6" name="__codelineno-0-6" href="#__codelineno-0-6"></a>┌────────────▼─────────────┐
</span><span id="__span-0-7"><a id="__codelineno-0-7" name="__codelineno-0-7" href="#__codelineno-0-7"></a>│ CCP API (5000) │ Express + TypeScript
</span><span id="__span-0-8"><a id="__codelineno-0-8" name="__codelineno-0-8" href="#__codelineno-0-8"></a>│ JWT auth, RBAC │ Prisma ORM → PostgreSQL
</span><span id="__span-0-9"><a id="__codelineno-0-9" name="__codelineno-0-9" href="#__codelineno-0-9"></a>│ Docker socket access │ Winston logger
</span><span id="__span-0-10"><a id="__codelineno-0-10" name="__codelineno-0-10" href="#__codelineno-0-10"></a>└────────────┬─────────────┘
</span><span id="__span-0-11"><a id="__codelineno-0-11" name="__codelineno-0-11" href="#__codelineno-0-11"></a>
</span><span id="__span-0-12"><a id="__codelineno-0-12" name="__codelineno-0-12" href="#__codelineno-0-12"></a> ┌────────┼────────┐
</span><span id="__span-0-13"><a id="__codelineno-0-13" name="__codelineno-0-13" href="#__codelineno-0-13"></a> ▼ ▼ ▼
</span><span id="__span-0-14"><a id="__codelineno-0-14" name="__codelineno-0-14" href="#__codelineno-0-14"></a>ccp-postgres ccp-redis Docker Socket
</span><span id="__span-0-15"><a id="__codelineno-0-15" name="__codelineno-0-15" href="#__codelineno-0-15"></a>(port 5480) (port 6399)
</span></code></pre></div>
<table>
<thead>
<tr>
<th>Service</th>
<th>Container</th>
<th>Port</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>CCP API</td>
<td><code>ccp-api</code></td>
<td>5000</td>
<td>Express API with Docker CLI access</td>
</tr>
<tr>
<td>CCP Admin</td>
<td><code>ccp-admin</code></td>
<td>5100</td>
<td>React admin GUI</td>
</tr>
<tr>
<td>CCP PostgreSQL</td>
<td><code>ccp-postgres</code></td>
<td>5480</td>
<td>CCP metadata database</td>
</tr>
<tr>
<td>CCP Redis</td>
<td><code>ccp-redis</code></td>
<td>6399</td>
<td>Rate limiting, caching</td>
</tr>
</tbody>
</table>
<p>Each managed CML instance gets its own isolated set of containers and PostgreSQL database, with ports allocated from non-overlapping ranges.</p>
<hr />
<h2 id="setup">Setup<a class="headerlink" href="#setup" title="Permanent link">&para;</a></h2>
<h3 id="1-run-the-setup-script">1. Run the Setup Script<a class="headerlink" href="#1-run-the-setup-script" title="Permanent link">&para;</a></h3>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-1-1"><a id="__codelineno-1-1" name="__codelineno-1-1" href="#__codelineno-1-1"></a><span class="nb">cd</span><span class="w"> </span>changemaker-control-panel
</span><span id="__span-1-2"><a id="__codelineno-1-2" name="__codelineno-1-2" href="#__codelineno-1-2"></a>chmod<span class="w"> </span>+x<span class="w"> </span>setup.sh
</span><span id="__span-1-3"><a id="__codelineno-1-3" name="__codelineno-1-3" href="#__codelineno-1-3"></a>./setup.sh
</span></code></pre></div>
<p>The setup script:</p>
<ul>
<li>Detects the installation directory and resolves absolute paths</li>
<li>Creates <code>instances/</code> and <code>backups/</code> directories</li>
<li>Copies <code>.env.example</code> to <code>.env</code> if not present</li>
<li>Sets <code>INSTANCES_BASE_PATH</code>, <code>BACKUP_STORAGE_PATH</code>, and <code>CML_SOURCE_PATH</code></li>
<li>Generates random secrets for any placeholder values</li>
</ul>
<h3 id="2-review-environment">2. Review Environment<a class="headerlink" href="#2-review-environment" title="Permanent link">&para;</a></h3>
<p>Edit <code>.env</code> and verify the key settings:</p>
<table>
<thead>
<tr>
<th>Variable</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>JWT_ACCESS_SECRET</code></td>
<td>Auto-generated</td>
<td>JWT signing key</td>
</tr>
<tr>
<td><code>JWT_REFRESH_SECRET</code></td>
<td>Auto-generated</td>
<td>Refresh token signing key</td>
</tr>
<tr>
<td><code>ENCRYPTION_KEY</code></td>
<td>Auto-generated</td>
<td>AES-256 key for instance secrets at rest</td>
</tr>
<tr>
<td><code>INITIAL_ADMIN_EMAIL</code></td>
<td><code>admin@example.com</code></td>
<td>Bootstrap admin email</td>
</tr>
<tr>
<td><code>INITIAL_ADMIN_PASSWORD</code></td>
<td><code>ChangeMe2025!!</code></td>
<td>Bootstrap admin password</td>
</tr>
<tr>
<td><code>INSTANCES_BASE_PATH</code></td>
<td><code>./instances</code></td>
<td>Where instance directories are created</td>
</tr>
<tr>
<td><code>CML_SOURCE_PATH</code></td>
<td>Auto-detected</td>
<td>Path to CML source repo for provisioning</td>
</tr>
<tr>
<td><code>BACKUP_STORAGE_PATH</code></td>
<td><code>./backups</code></td>
<td>Backup archive storage</td>
</tr>
<tr>
<td><code>PANGOLIN_API_URL</code></td>
<td></td>
<td>Pangolin API for tunnel management</td>
</tr>
<tr>
<td><code>PANGOLIN_API_KEY</code></td>
<td></td>
<td>Pangolin authentication</td>
</tr>
<tr>
<td><code>PANGOLIN_ORG_ID</code></td>
<td></td>
<td>Pangolin organization</td>
</tr>
</tbody>
</table>
<h3 id="3-start-ccp">3. Start CCP<a class="headerlink" href="#3-start-ccp" title="Permanent link">&para;</a></h3>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-2-1"><a id="__codelineno-2-1" name="__codelineno-2-1" href="#__codelineno-2-1"></a>docker<span class="w"> </span>compose<span class="w"> </span>up<span class="w"> </span>-d
</span><span id="__span-2-2"><a id="__codelineno-2-2" name="__codelineno-2-2" href="#__codelineno-2-2"></a>
</span><span id="__span-2-3"><a id="__codelineno-2-3" name="__codelineno-2-3" href="#__codelineno-2-3"></a><span class="c1"># Run database migrations and seed the admin user</span>
</span><span id="__span-2-4"><a id="__codelineno-2-4" name="__codelineno-2-4" href="#__codelineno-2-4"></a>docker<span class="w"> </span>compose<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>ccp-api<span class="w"> </span>npx<span class="w"> </span>prisma<span class="w"> </span>migrate<span class="w"> </span>deploy
</span><span id="__span-2-5"><a id="__codelineno-2-5" name="__codelineno-2-5" href="#__codelineno-2-5"></a>docker<span class="w"> </span>compose<span class="w"> </span><span class="nb">exec</span><span class="w"> </span>ccp-api<span class="w"> </span>npx<span class="w"> </span>prisma<span class="w"> </span>db<span class="w"> </span>seed
</span></code></pre></div>
<h3 id="4-log-in">4. Log In<a class="headerlink" href="#4-log-in" title="Permanent link">&para;</a></h3>
<p>Open <strong>http://localhost:5100</strong> and sign in with the admin credentials from <code>.env</code>.</p>
<hr />
<h2 id="creating-an-instance">Creating an Instance<a class="headerlink" href="#creating-an-instance" title="Permanent link">&para;</a></h2>
<p>The Create Instance wizard walks through 5 steps:</p>
<h3 id="step-1-basic-information">Step 1: Basic Information<a class="headerlink" href="#step-1-basic-information" title="Permanent link">&para;</a></h3>
<ul>
<li><strong>Instance name</strong> — human-readable label (e.g., "Edmonton Chapter")</li>
<li><strong>Slug</strong> — URL-safe identifier (e.g., <code>edmonton</code>), used for directory names and compose project</li>
<li><strong>Domain</strong> — the domain this instance will serve (e.g., <code>edmonton.example.org</code>)</li>
</ul>
<h3 id="step-2-features">Step 2: Features<a class="headerlink" href="#step-2-features" title="Permanent link">&para;</a></h3>
<p>Toggle which platform features to enable for this instance:</p>
<ul>
<li>Media Manager</li>
<li>Listmonk newsletter sync</li>
<li>Payments</li>
<li>Rocket.Chat</li>
<li>Gancio events</li>
<li>Jitsi Meet</li>
<li>SMS Campaigns</li>
</ul>
<h3 id="step-3-email">Step 3: Email<a class="headerlink" href="#step-3-email" title="Permanent link">&para;</a></h3>
<p>Configure SMTP for the instance, or use MailHog for testing.</p>
<h3 id="step-4-tunnel">Step 4: Tunnel<a class="headerlink" href="#step-4-tunnel" title="Permanent link">&para;</a></h3>
<p>Optionally configure Pangolin tunnel credentials for public access.</p>
<h3 id="step-5-review">Step 5: Review<a class="headerlink" href="#step-5-review" title="Permanent link">&para;</a></h3>
<p>Review all settings, then click <strong>Create</strong> to start provisioning.</p>
<hr />
<h2 id="provisioning-flow">Provisioning Flow<a class="headerlink" href="#provisioning-flow" title="Permanent link">&para;</a></h2>
<p>When you create an instance, CCP runs a <strong>13-step async provisioning process</strong>:</p>
<table>
<thead>
<tr>
<th>Step</th>
<th>What Happens</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Validate uniqueness (slug + domain)</td>
</tr>
<tr>
<td>2</td>
<td>Allocate 4 ports from ranges</td>
</tr>
<tr>
<td>3</td>
<td>Generate 14 secrets (passwords, JWT keys, encryption keys)</td>
</tr>
<tr>
<td>4</td>
<td>Create Instance record (status: PROVISIONING)</td>
</tr>
<tr>
<td>5</td>
<td>Create instance directory</td>
</tr>
<tr>
<td>6</td>
<td>Copy CML source code (rsync, excluding node_modules/.git/.env)</td>
</tr>
<tr>
<td>7</td>
<td>Decrypt secrets and build template context</td>
</tr>
<tr>
<td>8</td>
<td>Render 7 config files from Handlebars templates (docker-compose.yml, .env, nginx configs, Pangolin, Prometheus)</td>
</tr>
<tr>
<td>9</td>
<td>Copy static files (nginx.conf)</td>
</tr>
<tr>
<td>10</td>
<td><code>docker compose pull</code> (non-fatal if images are cached)</td>
</tr>
<tr>
<td>11</td>
<td><code>docker compose build</code></td>
</tr>
<tr>
<td>12</td>
<td>Start infrastructure (PostgreSQL + Redis), wait for healthy</td>
</tr>
<tr>
<td>13</td>
<td>Start API (runs migrations + seed), then start all remaining services</td>
</tr>
</tbody>
</table>
<p>The admin GUI polls every 3 seconds during provisioning to show progress. When complete, the instance status changes to <strong>RUNNING</strong>.</p>
<hr />
<h2 id="port-allocation">Port Allocation<a class="headerlink" href="#port-allocation" title="Permanent link">&para;</a></h2>
<p>CCP allocates ports from 4 non-overlapping ranges to prevent conflicts between instances:</p>
<table>
<thead>
<tr>
<th>Range</th>
<th>Start</th>
<th>End</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td>API</td>
<td>14000</td>
<td>14999</td>
<td>Express API server</td>
</tr>
<tr>
<td>Admin</td>
<td>13000</td>
<td>13999</td>
<td>React admin GUI</td>
</tr>
<tr>
<td>PostgreSQL</td>
<td>15400</td>
<td>15499</td>
<td>Database</td>
</tr>
<tr>
<td>Nginx</td>
<td>10000</td>
<td>10999</td>
<td>Reverse proxy</td>
</tr>
</tbody>
</table>
<p>Each new instance receives one port from each range. Ports are tracked in the database and released when instances are deleted.</p>
<hr />
<h2 id="pages-overview">Pages Overview<a class="headerlink" href="#pages-overview" title="Permanent link">&para;</a></h2>
<h3 id="dashboard">Dashboard<a class="headerlink" href="#dashboard" title="Permanent link">&para;</a></h3>
<p>At-a-glance fleet status:</p>
<ul>
<li>Total instances, running, healthy, degraded, stopped, error counts</li>
<li>Instance cards with status indicators and quick actions</li>
</ul>
<h3 id="instance-list">Instance List<a class="headerlink" href="#instance-list" title="Permanent link">&para;</a></h3>
<p>Searchable, filterable table of all instances with status, domain, health, and creation date.</p>
<h3 id="instance-detail">Instance Detail<a class="headerlink" href="#instance-detail" title="Permanent link">&para;</a></h3>
<p>5-tab view for each instance:</p>
<table>
<thead>
<tr>
<th>Tab</th>
<th>Content</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Overview</strong></td>
<td>Status, domain, ports, features, health summary</td>
</tr>
<tr>
<td><strong>Services</strong></td>
<td>Per-container status grid with restart and log-view actions</td>
</tr>
<tr>
<td><strong>Logs</strong></td>
<td>Real-time log viewer with service filter, tail count, and time range</td>
</tr>
<tr>
<td><strong>Backups</strong></td>
<td>Backup list with create, download, and delete actions</td>
</tr>
<tr>
<td><strong>Tunnel</strong></td>
<td>Pangolin tunnel status and configuration</td>
</tr>
</tbody>
</table>
<h3 id="backups">Backups<a class="headerlink" href="#backups" title="Permanent link">&para;</a></h3>
<p>Cross-instance backup management:</p>
<ul>
<li>All backups in one table with instance filter</li>
<li>Stats: total count, total size, last backup time</li>
<li>"Backup All Running" bulk action</li>
<li>Download and delete individual archives</li>
</ul>
<h3 id="audit-log">Audit Log<a class="headerlink" href="#audit-log" title="Permanent link">&para;</a></h3>
<p>Filterable activity trail with 18 action types:</p>
<ul>
<li>Instance lifecycle: CREATE, UPDATE, DELETE, START, STOP, RESTART, UPGRADE</li>
<li>Backups: CREATE, DELETE</li>
<li>Tunnel: PANGOLIN_SETUP, PANGOLIN_SYNC</li>
<li>Users: LOGIN, CREATE, UPDATE, DELETE</li>
<li>Settings: UPDATE</li>
</ul>
<p>Each entry includes timestamp, user, action, instance, IP address, and details (expandable JSON).</p>
<h3 id="settings">Settings<a class="headerlink" href="#settings" title="Permanent link">&para;</a></h3>
<p>CCP-level configuration:</p>
<ul>
<li>Port ranges</li>
<li>Pangolin credentials</li>
<li>Default feature flags for new instances</li>
<li>Health check interval</li>
<li>Backup retention period</li>
</ul>
<hr />
<h2 id="roles">Roles<a class="headerlink" href="#roles" title="Permanent link">&para;</a></h2>
<table>
<thead>
<tr>
<th>Role</th>
<th>Capabilities</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>SUPER_ADMIN</strong></td>
<td>Full access: create/delete instances, manage users, view secrets, delete backups</td>
</tr>
<tr>
<td><strong>OPERATOR</strong></td>
<td>Manage instances: create, start/stop/restart, backups, health checks</td>
</tr>
<tr>
<td><strong>VIEWER</strong></td>
<td>Read-only: view instances, logs, health, backups, audit log</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="security">Security<a class="headerlink" href="#security" title="Permanent link">&para;</a></h2>
<ul>
<li><strong>JWT authentication</strong> with 15-minute access tokens and 7-day refresh tokens (atomic rotation)</li>
<li><strong>AES-256-GCM encryption</strong> for instance secrets stored in the database</li>
<li><strong>Audit logging</strong> on all operations with IP address capture</li>
<li><strong>Role-based access control</strong> on all API endpoints</li>
<li><strong>Docker socket access</strong> restricted to the CCP API container only</li>
</ul>
<hr />
<h2 id="registering-an-existing-install-phone-home">Registering an Existing Install (Phone-Home)<a class="headerlink" href="#registering-an-existing-install-phone-home" title="Permanent link">&para;</a></h2>
<p>Instead of provisioning from scratch, CCP can adopt a Changemaker Lite install that already runs elsewhere. The target host's <code>ccp-agent</code> container phones home with an invite code, you approve the registration in CCP, and CCP issues an mTLS cert bundle for ongoing management.</p>
<h3 id="1-generate-an-invite-code-in-ccp">1. Generate an invite code in CCP<a class="headerlink" href="#1-generate-an-invite-code-in-ccp" title="Permanent link">&para;</a></h3>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-3-1"><a id="__codelineno-3-1" name="__codelineno-3-1" href="#__codelineno-3-1"></a>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="nv">$CCP_URL</span>/api/invite-codes<span class="w"> </span><span class="se">\</span>
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a><span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Authorization: Bearer </span><span class="nv">$ADMIN_TOKEN</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a><span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a><span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{&quot;expiryHours&quot;:48}&#39;</span>
</span></code></pre></div>
<p>The response contains a <code>code</code> like <code>KNZH-B6WW</code>. Invite codes are single-use and expire.</p>
<h3 id="2-install-the-target-with-ccp-flags">2. Install the target with <code>--ccp-*</code> flags<a class="headerlink" href="#2-install-the-target-with-ccp-flags" title="Permanent link">&para;</a></h3>
<p>On the host that will be registered:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-4-1"><a id="__codelineno-4-1" name="__codelineno-4-1" href="#__codelineno-4-1"></a>bash<span class="w"> </span>config.sh<span class="w"> </span>-y<span class="w"> </span>--enable-all<span class="w"> </span><span class="se">\</span>
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a><span class="w"> </span>--domain<span class="w"> </span>yourdomain.org<span class="w"> </span><span class="se">\</span>
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a><span class="w"> </span>--admin-password<span class="w"> </span><span class="s1">&#39;StrongPassword1&#39;</span><span class="w"> </span><span class="se">\</span>
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="w"> </span>--ccp-url<span class="w"> </span>https://ccp.example.com<span class="w"> </span><span class="se">\</span>
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a><span class="w"> </span>--ccp-invite-code<span class="w"> </span>KNZH-B6WW<span class="w"> </span><span class="se">\</span>
</span><span id="__span-4-6"><a id="__codelineno-4-6" name="__codelineno-4-6" href="#__codelineno-4-6"></a><span class="w"> </span>--ccp-agent-url<span class="w"> </span>https://100.90.78.47:7443<span class="w"> </span><span class="se">\</span>
</span><span id="__span-4-7"><a id="__codelineno-4-7" name="__codelineno-4-7" href="#__codelineno-4-7"></a><span class="w"> </span><span class="c1"># ... tunnel, SMTP, and other flags</span>
</span></code></pre></div>
<p>All three CCP flags are required together. The wizard:</p>
<ul>
<li>Sets <code>ENABLE_CCP_AGENT=true</code> in <code>.env</code></li>
<li>Appends <code>ccp-agent</code> to <code>COMPOSE_PROFILES</code> (without clobbering <code>monitoring</code>)</li>
<li>On <code>docker compose up -d</code>, the <code>ccp-agent</code> container starts and phones home</li>
</ul>
<h3 id="3-approve-in-ccp">3. Approve in CCP<a class="headerlink" href="#3-approve-in-ccp" title="Permanent link">&para;</a></h3>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-5-1"><a id="__codelineno-5-1" name="__codelineno-5-1" href="#__codelineno-5-1"></a><span class="c1"># List pending registrations</span>
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a>curl<span class="w"> </span>-s<span class="w"> </span><span class="nv">$CCP_URL</span>/api/agents/registrations<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Authorization: Bearer </span><span class="nv">$ADMIN_TOKEN</span><span class="s2">&quot;</span>
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a>
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4" href="#__codelineno-5-4"></a><span class="c1"># Approve one</span>
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5" href="#__codelineno-5-5"></a>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="nv">$CCP_URL</span>/api/agents/registrations/<span class="nv">$REG_ID</span>/approve<span class="w"> </span><span class="se">\</span>
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6" href="#__codelineno-5-6"></a><span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Authorization: Bearer </span><span class="nv">$ADMIN_TOKEN</span><span class="s2">&quot;</span><span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{}&#39;</span>
</span></code></pre></div>
<p>On approval, CCP issues a mTLS cert bundle. The agent picks it up on its next poll, writes certs to disk, and restarts into mTLS mode. The instance then shows as <code>RUNNING</code> in CCP with live container status via <code>docker compose ps</code> proxied through the agent.</p>
<div class="admonition info">
<p class="admonition-title">Approval SLA and rate limits</p>
<p>The agent polls <code>/api/agents/poll</code> every 30s while waiting. The endpoint accepts up to 180 polls per 15 minutes (one every ~5s upper bound), so the agent comfortably handles human-paced approval times. If the agent does hit HTTP 429, it backs off exponentially (30s → 60s → 120s → 300s cap) and resets on the next successful poll — no manual restart required.</p>
</div>
<h3 id="4-deregister-on-teardown">4. Deregister on teardown<a class="headerlink" href="#4-deregister-on-teardown" title="Permanent link">&para;</a></h3>
<p>When you wipe the target host (<code>docker compose down -v</code> + <code>sudo rm -rf</code>), also deregister the instance from CCP. Otherwise the stale Instance row blocks re-registration of the same slug, and CCP will return HTTP 409 <code>SLUG_CONFLICT</code> on the next approval.</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-6-1"><a id="__codelineno-6-1" name="__codelineno-6-1" href="#__codelineno-6-1"></a><span class="c1"># From the target host (or anywhere with network access to CCP):</span>
</span><span id="__span-6-2"><a id="__codelineno-6-2" name="__codelineno-6-2" href="#__codelineno-6-2"></a>bash<span class="w"> </span>scripts/ccp-deregister.sh<span class="w"> </span><span class="se">\</span>
</span><span id="__span-6-3"><a id="__codelineno-6-3" name="__codelineno-6-3" href="#__codelineno-6-3"></a><span class="w"> </span>--ccp-url<span class="w"> </span>https://ccp.example.com<span class="w"> </span><span class="se">\</span>
</span><span id="__span-6-4"><a id="__codelineno-6-4" name="__codelineno-6-4" href="#__codelineno-6-4"></a><span class="w"> </span>--token<span class="w"> </span><span class="nv">$ADMIN_TOKEN</span><span class="w"> </span><span class="se">\</span>
</span><span id="__span-6-5"><a id="__codelineno-6-5" name="__codelineno-6-5" href="#__codelineno-6-5"></a><span class="w"> </span>--yes
</span></code></pre></div>
<p>Defaults: matches by <code>CCP_AGENT_URL</code> from <code>.env</code>. Use <code>--slug</code> or <code>--instance-id</code> for explicit targeting. Dry-run by default.</p>
<p>The teardown sequence for a registered instance is therefore:</p>
<div class="language-bash highlight"><pre><span></span><code><span id="__span-7-1"><a id="__codelineno-7-1" name="__codelineno-7-1" href="#__codelineno-7-1"></a><span class="nb">cd</span><span class="w"> </span>~/changemaker.lite
</span><span id="__span-7-2"><a id="__codelineno-7-2" name="__codelineno-7-2" href="#__codelineno-7-2"></a>bash<span class="w"> </span>scripts/ccp-deregister.sh<span class="w"> </span>--token<span class="w"> </span><span class="nv">$ADMIN_TOKEN</span><span class="w"> </span>--yes<span class="w"> </span><span class="c1"># CCP-side</span>
</span><span id="__span-7-3"><a id="__codelineno-7-3" name="__codelineno-7-3" href="#__codelineno-7-3"></a>bash<span class="w"> </span>scripts/pangolin-teardown.sh<span class="w"> </span>--yes<span class="w"> </span><span class="c1"># Pangolin-side</span>
</span><span id="__span-7-4"><a id="__codelineno-7-4" name="__codelineno-7-4" href="#__codelineno-7-4"></a>docker<span class="w"> </span>compose<span class="w"> </span>--profile<span class="w"> </span>monitoring<span class="w"> </span>down<span class="w"> </span>-v<span class="w"> </span>--remove-orphans<span class="w"> </span><span class="c1"># Docker</span>
</span><span id="__span-7-5"><a id="__codelineno-7-5" name="__codelineno-7-5" href="#__codelineno-7-5"></a>sudo<span class="w"> </span>rm<span class="w"> </span>-rf<span class="w"> </span>~/changemaker.lite<span class="w"> </span><span class="c1"># Filesystem</span>
</span></code></pre></div>
<hr />
<h2 id="next-steps">Next Steps<a class="headerlink" href="#next-steps" title="Permanent link">&para;</a></h2>
<ul>
<li><a href="../services/">Services Overview</a> — learn about the services CCP provisions for each instance</li>
<li><a href="../upgrades/">Updates &amp; Upgrades</a> — upgrading CML instances</li>
<li><a href="../../deployment/">Deployment</a> — production setup with tunneling and SSL</li>
</ul>
</article>
</div>
<script>var tabs=__md_get("__tabs");if(Array.isArray(tabs))e:for(var set of document.querySelectorAll(".tabbed-set")){var labels=set.querySelector(".tabbed-labels");for(var tab of tabs)for(var label of labels.getElementsByTagName("label"))if(label.innerText.trim()===tab){var input=document.getElementById(label.htmlFor);input.checked=!0;continue e}}</script>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
Back to top
</button>
</main>
<footer class="md-footer">
<nav class="md-footer__inner md-grid" aria-label="Footer" >
<a href="../upgrades/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Updates &amp; Upgrades">
<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>
</div>
<div class="md-footer__title">
<span class="md-footer__direction">
Previous
</span>
<div class="md-ellipsis">
Updates & Upgrades
</div>
</div>
</a>
<a href="../features/" class="md-footer__link md-footer__link--next" aria-label="Next: Features at a Glance">
<div class="md-footer__title">
<span class="md-footer__direction">
Next
</span>
<div class="md-ellipsis">
Features at a Glance
</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">
<div class="md-copyright">
<div class="md-copyright__highlight">
Copyright &copy; 20242026 The Bunker Operations <a href="#__consent">Change cookie settings</a>
</div>
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
<div class="md-social">
<a href="https://gitea.bnkops.com/admin" target="_blank" rel="noopener" title="Gitea Repository" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M80 104a24 24 0 1 0 0-48 24 24 0 1 0 0 48m80-24c0 32.8-19.7 61-48 73.3V224h176c26.5 0 48-21.5 48-48v-22.7c-28.3-12.3-48-40.5-48-73.3 0-44.2 35.8-80 80-80s80 35.8 80 80c0 32.8-19.7 61-48 73.3V176c0 61.9-50.1 112-112 112H112v70.7c28.3 12.3 48 40.5 48 73.3 0 44.2-35.8 80-80 80S0 476.2 0 432c0-32.8 19.7-61 48-73.3V153.4C19.7 141 0 112.8 0 80 0 35.8 35.8 0 80 0s80 35.8 80 80m232 0a24 24 0 1 0-48 0 24 24 0 1 0 48 0M80 456a24 24 0 1 0 0-48 24 24 0 1 0 0 48"/></svg>
</a>
<a href="https://listmonk.bnkops.com/subscription/form" target="_blank" rel="noopener" title="Newsletter" class="md-social__link">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M536.4-26.3c9.8-3.5 20.6-1 28 6.3s9.8 18.2 6.3 28l-178 496.9c-5 13.9-18.1 23.1-32.8 23.1-14.2 0-27-8.6-32.3-21.7l-64.2-158c-4.5-11-2.5-23.6 5.2-32.6l94.5-112.4c5.1-6.1 4.7-15-.9-20.6s-14.6-6-20.6-.9l-112.4 94.3c-9.1 7.6-21.6 9.6-32.6 5.2L38.1 216.8c-13.1-5.3-21.7-18.1-21.7-32.3 0-14.7 9.2-27.8 23.1-32.8z"/></svg>
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<div class="md-progress" data-md-component="progress" role="progressbar"></div>
<script id="__config" type="application/json">{"annotate": null, "base": "../../..", "features": ["announce.dismiss", "content.action.edit", "content.action.view", "content.code.annotate", "content.code.copy", "content.code.select", "content.tabs.link", "content.tooltips", "navigation.footer", "navigation.indexes", "navigation.instant", "navigation.instant.prefetch", "navigation.instant.progress", "navigation.path", "navigation.prune", "navigation.tabs", "navigation.tabs.sticky", "navigation.top", "navigation.tracking", "search.highlight", "search.share", "search.suggest", "toc.follow"], "search": "../../../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../../../assets/javascripts/bundle.79ae519e.min.js"></script>
<script src="../../../javascripts/home.js"></script>
<script src="../../../javascripts/github-widget.js"></script>
<script src="../../../javascripts/gitea-widget.js"></script>
<script src="../../../assets/js/env-config.js"></script>
<script src="../../../assets/js/video-player.js"></script>
<script src="../../../assets/js/image-gallery.js"></script>
<script src="../../../assets/js/gancio-events.js"></script>
<script src="../../../assets/js/payment-widgets.js"></script>
<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>
</body>
</html>