Use inline JS styles (applySearchLayout) instead of CSS-only approach for search panel layout - fixes Firefox compatibility where cross-origin Material stylesheets override !important rules. Adds explicit height, flex layout, z-index, and background on search elements. Also fixes click-to-exit by deferring DOM queries to DOMContentLoaded. Syncs header-builder.service.ts with main.html changes. Bunker Admin
4418 lines
130 KiB
HTML
4418 lines
130 KiB
HTML
|
||
|
||
<!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="Complete guide to setting up the Termux Android SMS bridge for text message outreach, contact management, and response tracking.">
|
||
|
||
|
||
<meta name="author" content="Bunker Operations">
|
||
|
||
|
||
<link rel="canonical" href="https://bnkserve.org/docs/admin/broadcast/sms/">
|
||
|
||
|
||
<link rel="prev" href="../email-templates/">
|
||
|
||
|
||
<link rel="next" href="../../web/">
|
||
|
||
|
||
|
||
|
||
|
||
<link rel="icon" href="../../../../assets/favicon.png">
|
||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.2">
|
||
|
||
|
||
|
||
<title>SMS Campaigns - 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>
|
||
|
||
|
||
|
||
|
||
|
||
<script>
|
||
(function () {
|
||
// API URL injected from MkDocs config.extra (set by env_config_hook.py)
|
||
var apiUrl = "http://localhost:4002";
|
||
if (!apiUrl) return;
|
||
|
||
var trackUrl = apiUrl + "/api/docs-analytics/track";
|
||
|
||
// Anonymous session UUID (sessionStorage — dies with tab close, no cookies)
|
||
function getSessionHash() {
|
||
var key = "__docs_sh";
|
||
var hash = sessionStorage.getItem(key);
|
||
if (!hash) {
|
||
hash = crypto.randomUUID
|
||
? crypto.randomUUID()
|
||
: "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
||
var r = (Math.random() * 16) | 0;
|
||
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
||
});
|
||
sessionStorage.setItem(key, hash);
|
||
}
|
||
return hash;
|
||
}
|
||
|
||
function trackPageView(path) {
|
||
var payload = JSON.stringify({
|
||
path: path,
|
||
referrer: document.referrer || undefined,
|
||
sessionHash: getSessionHash(),
|
||
});
|
||
|
||
// Prefer sendBeacon for reliability (works during tab close)
|
||
if (navigator.sendBeacon) {
|
||
navigator.sendBeacon(trackUrl, new Blob([payload], { type: "application/json" }));
|
||
} else {
|
||
fetch(trackUrl, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: payload,
|
||
keepalive: true,
|
||
}).catch(function () {});
|
||
}
|
||
}
|
||
|
||
// Track initial page load
|
||
trackPageView(location.pathname);
|
||
|
||
// Subscribe to Material's SPA navigation observable (instant loading)
|
||
if (typeof document$ !== "undefined") {
|
||
document$.subscribe(function () {
|
||
trackPageView(location.pathname);
|
||
});
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
<script>"undefined"!=typeof __md_analytics&&__md_analytics()</script>
|
||
|
||
|
||
|
||
|
||
|
||
<meta property="og:type" content="website" />
|
||
<meta property="og:title" content="SMS Campaigns - Changemaker Lite" />
|
||
<meta property="og:description" content="Complete guide to setting up the Termux Android SMS bridge for text message outreach, contact management, and response tracking." />
|
||
<meta property="og:image" content="https://bnkserve.org/assets/images/social/docs/admin/broadcast/sms.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://bnkserve.org/docs/admin/broadcast/sms/" />
|
||
<meta property="twitter:card" content="summary_large_image" />
|
||
<meta property="twitter:title" content="SMS Campaigns - Changemaker Lite" />
|
||
<meta property="twitter:description" content="Complete guide to setting up the Termux Android SMS bridge for text message outreach, contact management, and response tracking." />
|
||
<meta property="twitter:image" content="https://bnkserve.org/assets/images/social/docs/admin/broadcast/sms.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="#sms-campaigns" 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="/home" class="cm-header-nav__link" data-nav-id="home"><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>
|
||
<div class="cm-header-nav__dropdown">
|
||
<span class="cm-header-nav__link cm-header-nav__dropdown-trigger">
|
||
<span class="material-icons-outlined">apps</span>
|
||
<span class="cm-header-nav__label">Scheduling</span>
|
||
<span class="material-icons-outlined cm-header-nav__chevron">expand_more</span>
|
||
</span>
|
||
<div class="cm-header-nav__dropdown-menu">
|
||
<a href="#" data-path="/shifts" class="cm-header-nav__dropdown-item" data-nav-id="shifts"><span class="material-icons-outlined">schedule</span><span>Shifts</span></a>
|
||
<a href="#" data-path="/events" class="cm-header-nav__dropdown-item" data-nav-id="events"><span class="material-icons-outlined">event</span><span>Calendar</span></a>
|
||
<a href="#" data-path="/polls" class="cm-header-nav__dropdown-item" data-nav-id="polls"><span class="material-icons-outlined">bar_chart</span><span>Polls</span></a>
|
||
</div>
|
||
</div>
|
||
<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>
|
||
<div class="cm-header-nav__dropdown">
|
||
<span class="cm-header-nav__link cm-header-nav__dropdown-trigger">
|
||
<span class="material-icons-outlined">account_balance_wallet</span>
|
||
<span class="cm-header-nav__label">Commerce</span>
|
||
<span class="material-icons-outlined cm-header-nav__chevron">expand_more</span>
|
||
</span>
|
||
<div class="cm-header-nav__dropdown-menu">
|
||
<a href="#" data-path="/pricing" class="cm-header-nav__dropdown-item" 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__dropdown-item" 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__dropdown-item" data-nav-id="donate"><span class="material-icons-outlined">favorite_border</span><span>Donate</span></a>
|
||
</div>
|
||
</div>
|
||
<a href="#" data-path="/wall-of-fame" class="cm-header-nav__link" data-nav-id="wall-of-fame"><span class="material-icons-outlined">emoji_events</span><span class="cm-header-nav__label">Wall of Fame</span></a>
|
||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</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="/home" class="cm-header-nav__mobile-link" data-nav-id="home"><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>
|
||
<div class="cm-header-nav__mobile-group" data-group-id="scheduling">
|
||
<span class="cm-header-nav__mobile-link cm-header-nav__mobile-group-trigger" role="button">
|
||
<span class="material-icons-outlined">apps</span>
|
||
<span style="flex:1">Scheduling</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="/shifts" class="cm-header-nav__mobile-link" data-nav-id="shifts" style="padding-left:48px"><span class="material-icons-outlined">schedule</span><span>Shifts</span></a>
|
||
<a href="#" data-path="/events" class="cm-header-nav__mobile-link" data-nav-id="events" style="padding-left:48px"><span class="material-icons-outlined">event</span><span>Calendar</span></a>
|
||
<a href="#" data-path="/polls" class="cm-header-nav__mobile-link" data-nav-id="polls" style="padding-left:48px"><span class="material-icons-outlined">bar_chart</span><span>Polls</span></a>
|
||
</div>
|
||
</div>
|
||
<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>
|
||
<div class="cm-header-nav__mobile-group" data-group-id="commerce">
|
||
<span class="cm-header-nav__mobile-link cm-header-nav__mobile-group-trigger" role="button">
|
||
<span class="material-icons-outlined">account_balance_wallet</span>
|
||
<span style="flex:1">Commerce</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="/pricing" class="cm-header-nav__mobile-link" data-nav-id="pricing" style="padding-left:48px"><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" style="padding-left:48px"><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" style="padding-left:48px"><span class="material-icons-outlined">favorite_border</span><span>Donate</span></a>
|
||
</div>
|
||
</div>
|
||
<a href="#" data-path="/wall-of-fame" class="cm-header-nav__mobile-link" data-nav-id="wall-of-fame"><span class="material-icons-outlined">emoji_events</span><span>Wall of Fame</span></a>
|
||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</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; }
|
||
}
|
||
/* Tell Material that sidebar sticky offset = tabs height (blue header scrolls away) */
|
||
:root {
|
||
--md-header-height: 0px;
|
||
}
|
||
/* Hidden Material header — 0 height but search children overflow visibly */
|
||
.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 top (blue header scrolls away, tabs persist) */
|
||
.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.png" 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--pruned md-nav__item--nested">
|
||
|
||
|
||
|
||
|
||
|
||
<a href="../../../getting-started/" class="md-nav__link">
|
||
|
||
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Getting Started
|
||
|
||
|
||
|
||
</span>
|
||
|
||
|
||
|
||
|
||
<span class="md-nav__icon md-icon"></span>
|
||
|
||
</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<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_3" 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 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5zm0 4a3 3 0 0 1 3 3 3 3 0 0 1-3 3 3 3 0 0 1-3-3 3 3 0 0 1 3-3m5.13 12A9.7 9.7 0 0 1 12 20.92 9.7 9.7 0 0 1 6.87 17c-.34-.5-.63-1-.87-1.53 0-1.65 2.71-3 6-3s6 1.32 6 3c-.24.53-.53 1.03-.87 1.53"/></svg>
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Admin Guide
|
||
|
||
|
||
|
||
</span>
|
||
|
||
|
||
|
||
</a>
|
||
|
||
|
||
<label class="md-nav__link " for="__nav_2_3" id="__nav_2_3_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_3_label" aria-expanded="true">
|
||
<label class="md-nav__title" for="__nav_2_3">
|
||
<span class="md-nav__icon md-icon"></span>
|
||
|
||
|
||
Admin Guide
|
||
|
||
|
||
</label>
|
||
<ul class="md-nav__list" data-md-scrollfix>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<li class="md-nav__item">
|
||
<a href="../../dashboard/" class="md-nav__link">
|
||
|
||
|
||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 3v6h8V3m-8 18h8V11h-8M3 21h8v-6H3m0-2h8V3H3z"/></svg>
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Dashboard
|
||
|
||
|
||
|
||
</span>
|
||
|
||
|
||
|
||
</a>
|
||
</li>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<li class="md-nav__item">
|
||
<a href="../../people-access/" class="md-nav__link">
|
||
|
||
|
||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 5.5A3.5 3.5 0 0 1 15.5 9a3.5 3.5 0 0 1-3.5 3.5A3.5 3.5 0 0 1 8.5 9 3.5 3.5 0 0 1 12 5.5M5 8c.56 0 1.08.15 1.53.42-.15 1.43.27 2.85 1.13 3.96C7.16 13.34 6.16 14 5 14a3 3 0 0 1-3-3 3 3 0 0 1 3-3m14 0a3 3 0 0 1 3 3 3 3 0 0 1-3 3c-1.16 0-2.16-.66-2.66-1.62a5.54 5.54 0 0 0 1.13-3.96c.45-.27.97-.42 1.53-.42M5.5 18.25c0-2.07 2.91-3.75 6.5-3.75s6.5 1.68 6.5 3.75V20h-13zM0 20v-1.5c0-1.39 1.89-2.56 4.45-2.9-.59.68-.95 1.62-.95 2.65V20zm24 0h-3.5v-1.75c0-1.03-.36-1.97-.95-2.65 2.56.34 4.45 1.51 4.45 2.9z"/></svg>
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
People & Access
|
||
|
||
|
||
|
||
</span>
|
||
|
||
|
||
|
||
</a>
|
||
</li>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
|
||
|
||
|
||
|
||
|
||
|
||
<a href="../../advocacy/" class="md-nav__link">
|
||
|
||
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Advocacy
|
||
|
||
|
||
|
||
</span>
|
||
|
||
|
||
|
||
|
||
<span class="md-nav__icon md-icon"></span>
|
||
|
||
</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<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_3_5" 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 8H4a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h1v4a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-4h3l5 4V4zm9.5 4c0 1.71-.96 3.26-2.5 4V8c1.53.75 2.5 2.3 2.5 4"/></svg>
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Broadcast
|
||
|
||
|
||
|
||
</span>
|
||
|
||
|
||
|
||
</a>
|
||
|
||
|
||
<label class="md-nav__link " for="__nav_2_3_5" id="__nav_2_3_5_label" tabindex="0">
|
||
<span class="md-nav__icon md-icon"></span>
|
||
</label>
|
||
|
||
</div>
|
||
|
||
<nav class="md-nav" data-md-level="3" aria-labelledby="__nav_2_3_5_label" aria-expanded="true">
|
||
<label class="md-nav__title" for="__nav_2_3_5">
|
||
<span class="md-nav__icon md-icon"></span>
|
||
|
||
|
||
Broadcast
|
||
|
||
|
||
</label>
|
||
<ul class="md-nav__list" data-md-scrollfix>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<li class="md-nav__item">
|
||
<a href="../newsletter/" class="md-nav__link">
|
||
|
||
|
||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 3H4c-1.11 0-2 .89-2 2v14c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V5c0-1.11-.89-2-2-2M5 7h5v6H5zm14 10H5v-2h14zm0-4h-7v-2h7zm0-4h-7V7h7z"/></svg>
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Newsletter
|
||
|
||
|
||
|
||
</span>
|
||
|
||
|
||
|
||
</a>
|
||
</li>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<li class="md-nav__item">
|
||
<a href="../email-templates/" class="md-nav__link">
|
||
|
||
|
||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 12.13c-.15.01-.29.06-.39.17l-1 1 2.05 2 1-1c.22-.21.22-.56 0-.77l-1.24-1.23a.56.56 0 0 0-.38-.17m-2 1.75L13 19.94V22h2.06l6.06-6.07M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h7v-.89l8.24-8.22c.47-.49 1.12-.76 1.8-.76.34 0 .68.06 1 .19V6c0-1.12-.92-2-2.04-2m0 4-8 5-8-5V6l8 5 8-5"/></svg>
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Email Templates
|
||
|
||
|
||
|
||
</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 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2M6 9h12v2H6m8 3H6v-2h8m4-4H6V6h12"/></svg>
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
SMS
|
||
|
||
|
||
|
||
</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 2H4a2 2 0 0 0-2 2v18l4-4h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2M6 9h12v2H6m8 3H6v-2h8m4-4H6V6h12"/></svg>
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
SMS
|
||
|
||
|
||
|
||
</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="#architecture-overview" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Architecture Overview
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#prerequisites" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Prerequisites
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#phone-setup" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Phone Setup
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Phone Setup">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-1-install-apps-from-f-droid" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 1: Install Apps from F-Droid
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-2-generate-api-key" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 2: Generate API Key
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-3-run-the-setup-script" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 3: Run the Setup Script
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Step 3: Run the Setup Script">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#recommended-install-service-supervisor" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Recommended: Install Service Supervisor
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-4-prevent-android-from-killing-termux" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 4: Prevent Android from Killing Termux
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#updating" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Updating
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#service-management" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Service Management
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#accessing-the-phone" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Accessing the Phone
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Accessing the Phone">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#direct-on-phone" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Direct (on phone)
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#ssh-remote-access" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
SSH (remote access)
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#scrcpy-screen-mirror" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
scrcpy (screen mirror)
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#setup-wizard" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Setup Wizard
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Setup Wizard">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-1-prepare-phone" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 1: Prepare Phone
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-2-connect" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 2: Connect
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-3-test-save" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 3: Test & Save
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#how-it-works" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
How It Works
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="How It Works">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#sending-messages" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Sending Messages
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#receiving-responses" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Receiving Responses
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#device-monitoring" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Device Monitoring
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#key-features" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Key Features
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#admin-routes" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Admin Routes
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#configuration" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Configuration
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Configuration">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#environment-variables" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Environment Variables
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#phone-side-configuration" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Phone-Side Configuration
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#phone-api-endpoints" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Phone API Endpoints
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#troubleshooting" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Troubleshooting
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Troubleshooting">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#phone-cant-be-reached" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Phone can't be reached
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#authentication-required-errors" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
"Authentication required" errors
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#sms-not-sending" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
SMS not sending
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#termux-keeps-getting-killed" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Termux keeps getting killed
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#server-wont-start-missing-sms_api_secret" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Server won't start — "Missing SMS_API_SECRET"
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#rcs-chat-features-interfering-with-replies" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
RCS / Chat Features interfering with replies
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#updating-the-sms-server" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Updating the SMS server
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
|
||
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<li class="md-nav__item md-nav__item--pruned md-nav__item--nested">
|
||
|
||
|
||
|
||
|
||
|
||
<a href="../../web/" class="md-nav__link">
|
||
|
||
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Web Content
|
||
|
||
|
||
|
||
</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="../../map/" class="md-nav__link">
|
||
|
||
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Map & Canvassing
|
||
|
||
|
||
|
||
</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="../../media/" class="md-nav__link">
|
||
|
||
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Media
|
||
|
||
|
||
|
||
</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="../../payments/" class="md-nav__link">
|
||
|
||
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Payments
|
||
|
||
|
||
|
||
</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">
|
||
<a href="../../settings/" class="md-nav__link">
|
||
|
||
|
||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97s-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1s.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64z"/></svg>
|
||
|
||
<span class="md-ellipsis">
|
||
|
||
|
||
Settings
|
||
|
||
|
||
|
||
</span>
|
||
|
||
|
||
|
||
</a>
|
||
</li>
|
||
|
||
|
||
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</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="#architecture-overview" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Architecture Overview
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#prerequisites" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Prerequisites
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#phone-setup" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Phone Setup
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Phone Setup">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-1-install-apps-from-f-droid" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 1: Install Apps from F-Droid
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-2-generate-api-key" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 2: Generate API Key
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-3-run-the-setup-script" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 3: Run the Setup Script
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Step 3: Run the Setup Script">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#recommended-install-service-supervisor" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Recommended: Install Service Supervisor
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-4-prevent-android-from-killing-termux" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 4: Prevent Android from Killing Termux
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#updating" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Updating
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#service-management" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Service Management
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#accessing-the-phone" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Accessing the Phone
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Accessing the Phone">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#direct-on-phone" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Direct (on phone)
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#ssh-remote-access" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
SSH (remote access)
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#scrcpy-screen-mirror" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
scrcpy (screen mirror)
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#setup-wizard" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Setup Wizard
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Setup Wizard">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-1-prepare-phone" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 1: Prepare Phone
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-2-connect" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 2: Connect
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#step-3-test-save" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Step 3: Test & Save
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#how-it-works" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
How It Works
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="How It Works">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#sending-messages" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Sending Messages
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#receiving-responses" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Receiving Responses
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#device-monitoring" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Device Monitoring
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#key-features" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Key Features
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#admin-routes" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Admin Routes
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#configuration" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Configuration
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Configuration">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#environment-variables" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Environment Variables
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#phone-side-configuration" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Phone-Side Configuration
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#phone-api-endpoints" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Phone API Endpoints
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#troubleshooting" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Troubleshooting
|
||
|
||
</span>
|
||
</a>
|
||
|
||
<nav class="md-nav" aria-label="Troubleshooting">
|
||
<ul class="md-nav__list">
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#phone-cant-be-reached" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Phone can't be reached
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#authentication-required-errors" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
"Authentication required" errors
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#sms-not-sending" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
SMS not sending
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#termux-keeps-getting-killed" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Termux keeps getting killed
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#server-wont-start-missing-sms_api_secret" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Server won't start — "Missing SMS_API_SECRET"
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#rcs-chat-features-interfering-with-replies" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
RCS / Chat Features interfering with replies
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
<li class="md-nav__item">
|
||
<a href="#updating-the-sms-server" class="md-nav__link">
|
||
<span class="md-ellipsis">
|
||
|
||
Updating the SMS server
|
||
|
||
</span>
|
||
</a>
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
</nav>
|
||
|
||
</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">
|
||
Admin Guide
|
||
</span>
|
||
|
||
</a>
|
||
</li>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
<li class="md-path__item">
|
||
<a href="../" class="md-path__link">
|
||
|
||
<span class="md-ellipsis">
|
||
Broadcast
|
||
</span>
|
||
|
||
</a>
|
||
</li>
|
||
|
||
|
||
|
||
|
||
</ol>
|
||
</nav>
|
||
|
||
|
||
<article class="md-content__inner md-typeset">
|
||
|
||
|
||
|
||
|
||
|
||
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/sms.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
|
||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
|
||
</a>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="https://gitea.bnkops.com/admin/changemaker.lite/src/branch/main/mkdocs/docs/docs/admin/broadcast/sms.md" title="View source of this page" class="md-content__button md-icon">
|
||
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17 18c.56 0 1 .44 1 1s-.44 1-1 1-1-.44-1-1 .44-1 1-1m0-3c-2.73 0-5.06 1.66-6 4 .94 2.34 3.27 4 6 4s5.06-1.66 6-4c-.94-2.34-3.27-4-6-4m0 6.5a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5M9.27 20H6V4h7v5h5v4.07c.7.08 1.36.25 2 .49V8l-6-6H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h4.5a8.2 8.2 0 0 1-1.23-2"/></svg>
|
||
</a>
|
||
|
||
|
||
|
||
<h1 id="sms-campaigns">SMS Campaigns<a class="headerlink" href="#sms-campaigns" title="Permanent link">¶</a></h1>
|
||
<p>Text message outreach via a Termux Android bridge. Uses a real Android phone to send and receive SMS — no third-party SMS gateway or Twilio account needed.</p>
|
||
<p>Enable with <code>ENABLE_SMS=true</code> or via the setup wizard.</p>
|
||
<hr />
|
||
<h2 id="architecture-overview">Architecture Overview<a class="headerlink" href="#architecture-overview" title="Permanent link">¶</a></h2>
|
||
<p>The SMS system uses a three-tier architecture where your server communicates with a lightweight Python Flask API running on an Android phone:</p>
|
||
<pre class="mermaid"><code>graph LR
|
||
A[Admin Dashboard<br/>Campaign UI] -->|API calls| B[Express API<br/>BullMQ Queue]
|
||
B -->|HTTP + API Key| C[Android Phone<br/>Flask on Termux]
|
||
C -->|termux-sms-send| D[Android SMS]
|
||
D -->|Carrier Network| E[Recipients]
|
||
E -->|Reply SMS| D
|
||
D -->|termux-sms-list| C
|
||
C -->|HTTP response| B</code></pre>
|
||
<p><strong>Why this approach?</strong></p>
|
||
<ul>
|
||
<li><strong>No SaaS dependency</strong> — your phone is the SMS gateway, no Twilio/MessageBird/etc.</li>
|
||
<li><strong>Real phone number</strong> — recipients see a real number, not a short code</li>
|
||
<li><strong>Two-way messaging</strong> — incoming replies sync automatically</li>
|
||
<li><strong>Low cost</strong> — just your phone plan's SMS allowance</li>
|
||
<li><strong>Full control</strong> — FOSS stack end-to-end</li>
|
||
</ul>
|
||
<hr />
|
||
<h2 id="prerequisites">Prerequisites<a class="headerlink" href="#prerequisites" title="Permanent link">¶</a></h2>
|
||
<p>Before starting setup, you'll need:</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Item</th>
|
||
<th>Details</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><strong>Android phone</strong></td>
|
||
<td>Any Android 7+ device with an active SIM card and SMS plan</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Termux</strong></td>
|
||
<td>Terminal emulator — install from <strong>F-Droid</strong> (not Play Store)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Termux:API</strong></td>
|
||
<td>Termux plugin for SMS/contacts/battery — install from <strong>F-Droid</strong></td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Tailscale</strong> (recommended)</td>
|
||
<td>VPN mesh for stable IP — install from Play Store</td>
|
||
</tr>
|
||
<tr>
|
||
<td><strong>Network access</strong></td>
|
||
<td>Phone must be reachable from the server (Tailscale, LAN, or port forwarding)</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="admonition danger">
|
||
<p class="admonition-title">Both Apps MUST Come from F-Droid</p>
|
||
<p>The Play Store version of Termux is <strong>abandoned and incompatible</strong> with the API plugin. If you install Termux from the Play Store and Termux:API from F-Droid (or vice versa), SMS commands will fail with:</p>
|
||
<blockquote>
|
||
<p>Termux:API is not yet available on Google Play</p>
|
||
</blockquote>
|
||
<p><strong>Fix:</strong> Uninstall both apps, then reinstall <strong>both</strong> from <a href="https://f-droid.org">F-Droid</a>. They must come from the same source because Android verifies matching app signatures for inter-process communication.</p>
|
||
</div>
|
||
<hr />
|
||
<h2 id="phone-setup">Phone Setup<a class="headerlink" href="#phone-setup" title="Permanent link">¶</a></h2>
|
||
<h3 id="step-1-install-apps-from-f-droid">Step 1: Install Apps from F-Droid<a class="headerlink" href="#step-1-install-apps-from-f-droid" title="Permanent link">¶</a></h3>
|
||
<p>On your Android phone:</p>
|
||
<ol>
|
||
<li><strong>Install F-Droid</strong> — download from <a href="https://f-droid.org">f-droid.org</a> if you don't have it</li>
|
||
<li><strong>Install Termux</strong> — search in F-Droid and install</li>
|
||
<li><strong>Install Termux:API</strong> — search in F-Droid and install</li>
|
||
<li><strong>Install Termux:Boot</strong> (optional) — for auto-start on phone reboot. Open once after install to register.</li>
|
||
<li><strong>Install Tailscale</strong> (recommended) — from Play Store, connect to your tailnet for a stable IP</li>
|
||
</ol>
|
||
<div class="admonition note">
|
||
<p class="admonition-title">termux-api package</p>
|
||
<p>You need <strong>two things</strong> called "termux-api": the <strong>F-Droid app</strong> (Termux:API) and the <strong>Termux package</strong> (<code>pkg install termux-api</code>). The setup script installs the package automatically, but the F-Droid app must be installed manually.</p>
|
||
</div>
|
||
<h3 id="step-2-generate-api-key">Step 2: Generate API Key<a class="headerlink" href="#step-2-generate-api-key" title="Permanent link">¶</a></h3>
|
||
<p>Go to the admin dashboard <strong>SMS Setup</strong> page (<code>/app/sms/setup</code>) and click <strong>Generate API Key</strong>. Copy the key — you'll paste it into the setup command.</p>
|
||
<h3 id="step-3-run-the-setup-script">Step 3: Run the Setup Script<a class="headerlink" href="#step-3-run-the-setup-script" title="Permanent link">¶</a></h3>
|
||
<p>Open Termux on the phone and run:</p>
|
||
<div class="language-bash highlight"><pre><span></span><code><span id="__span-0-1"><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="c1"># Clone the SMS server (first time only)</span>
|
||
</span><span id="__span-0-2"><a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a>pkg<span class="w"> </span>install<span class="w"> </span>-y<span class="w"> </span>git<span class="w"> </span><span class="o">&&</span><span class="w"> </span>git<span class="w"> </span>clone<span class="w"> </span>https://gitea.bnkops.com/admin/campaign_connector.git<span class="w"> </span>~/sms-server
|
||
</span><span id="__span-0-3"><a id="__codelineno-0-3" name="__codelineno-0-3" href="#__codelineno-0-3"></a>
|
||
</span><span id="__span-0-4"><a id="__codelineno-0-4" name="__codelineno-0-4" href="#__codelineno-0-4"></a><span class="c1"># Run the setup script — paste your API key at the end</span>
|
||
</span><span id="__span-0-5"><a id="__codelineno-0-5" name="__codelineno-0-5" href="#__codelineno-0-5"></a>bash<span class="w"> </span>~/sms-server/android/setup.sh<span class="w"> </span>YOUR_API_KEY_HERE
|
||
</span></code></pre></div>
|
||
<p>The setup script automatically:</p>
|
||
<ul>
|
||
<li>Installs Python, Flask, termux-api, and openssh</li>
|
||
<li>Saves the API key to <code>~/.bashrc</code></li>
|
||
<li>Requests SMS and Contacts permissions (tap <strong>Allow</strong> when prompted)</li>
|
||
<li>Creates a Termux:Boot auto-start script (if Termux:Boot is installed)</li>
|
||
<li>Starts the SMS server</li>
|
||
</ul>
|
||
<p>When done, note the <strong>Phone URL</strong> displayed (e.g. <code>http://100.64.0.5:5001</code>).</p>
|
||
<h4 id="recommended-install-service-supervisor">Recommended: Install Service Supervisor<a class="headerlink" href="#recommended-install-service-supervisor" title="Permanent link">¶</a></h4>
|
||
<p>After initial setup, install <code>termux-services</code> for reliable process management. This uses runit, a proper UNIX service supervisor that automatically restarts the server if it crashes:</p>
|
||
<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>~/sms-server<span class="w"> </span><span class="o">&&</span><span class="w"> </span>bash<span class="w"> </span>android/setup-services.sh
|
||
</span></code></pre></div>
|
||
<p>This registers two supervised services:</p>
|
||
<ul>
|
||
<li><strong>sms-api</strong> — Flask SMS API server (port 5001)</li>
|
||
<li><strong>sshd-custom</strong> — SSH daemon for remote management (port 8022)</li>
|
||
</ul>
|
||
<h3 id="step-4-prevent-android-from-killing-termux">Step 4: Prevent Android from Killing Termux<a class="headerlink" href="#step-4-prevent-android-from-killing-termux" title="Permanent link">¶</a></h3>
|
||
<p>This is <strong>required</strong> for the server to run reliably in the background:</p>
|
||
<ol>
|
||
<li>Open <strong>Android Settings → Apps → Termux → Battery</strong> → set to <strong>Unrestricted</strong></li>
|
||
<li>Lock Termux in the recent apps view (long-press the app card → Lock/Pin)</li>
|
||
<li>Samsung phones: also add Termux to <strong>Settings → Device Care → Battery → Never Sleeping Apps</strong></li>
|
||
</ol>
|
||
<h3 id="updating">Updating<a class="headerlink" href="#updating" title="Permanent link">¶</a></h3>
|
||
<p>To pull the latest server code and re-run setup:</p>
|
||
<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><span class="nb">cd</span><span class="w"> </span>~/sms-server<span class="w"> </span><span class="o">&&</span><span class="w"> </span>git<span class="w"> </span>pull<span class="w"> </span><span class="o">&&</span><span class="w"> </span>bash<span class="w"> </span>android/setup.sh<span class="w"> </span>YOUR_API_KEY_HERE
|
||
</span></code></pre></div>
|
||
<h3 id="service-management">Service Management<a class="headerlink" href="#service-management" title="Permanent link">¶</a></h3>
|
||
<p>If you installed <code>termux-services</code> (recommended):</p>
|
||
<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><span class="c1"># Check status</span>
|
||
</span><span id="__span-3-2"><a id="__codelineno-3-2" name="__codelineno-3-2" href="#__codelineno-3-2"></a>sv<span class="w"> </span>status<span class="w"> </span>sms-api
|
||
</span><span id="__span-3-3"><a id="__codelineno-3-3" name="__codelineno-3-3" href="#__codelineno-3-3"></a>
|
||
</span><span id="__span-3-4"><a id="__codelineno-3-4" name="__codelineno-3-4" href="#__codelineno-3-4"></a><span class="c1"># Restart</span>
|
||
</span><span id="__span-3-5"><a id="__codelineno-3-5" name="__codelineno-3-5" href="#__codelineno-3-5"></a>sv<span class="w"> </span>restart<span class="w"> </span>sms-api
|
||
</span><span id="__span-3-6"><a id="__codelineno-3-6" name="__codelineno-3-6" href="#__codelineno-3-6"></a>
|
||
</span><span id="__span-3-7"><a id="__codelineno-3-7" name="__codelineno-3-7" href="#__codelineno-3-7"></a><span class="c1"># Stop</span>
|
||
</span><span id="__span-3-8"><a id="__codelineno-3-8" name="__codelineno-3-8" href="#__codelineno-3-8"></a>sv<span class="w"> </span>down<span class="w"> </span>sms-api
|
||
</span><span id="__span-3-9"><a id="__codelineno-3-9" name="__codelineno-3-9" href="#__codelineno-3-9"></a>
|
||
</span><span id="__span-3-10"><a id="__codelineno-3-10" name="__codelineno-3-10" href="#__codelineno-3-10"></a><span class="c1"># Start</span>
|
||
</span><span id="__span-3-11"><a id="__codelineno-3-11" name="__codelineno-3-11" href="#__codelineno-3-11"></a>sv<span class="w"> </span>up<span class="w"> </span>sms-api
|
||
</span><span id="__span-3-12"><a id="__codelineno-3-12" name="__codelineno-3-12" href="#__codelineno-3-12"></a>
|
||
</span><span id="__span-3-13"><a id="__codelineno-3-13" name="__codelineno-3-13" href="#__codelineno-3-13"></a><span class="c1"># View logs</span>
|
||
</span><span id="__span-3-14"><a id="__codelineno-3-14" name="__codelineno-3-14" href="#__codelineno-3-14"></a>tail<span class="w"> </span>-f<span class="w"> </span>~/logs/sms-api.log
|
||
</span><span id="__span-3-15"><a id="__codelineno-3-15" name="__codelineno-3-15" href="#__codelineno-3-15"></a>
|
||
</span><span id="__span-3-16"><a id="__codelineno-3-16" name="__codelineno-3-16" href="#__codelineno-3-16"></a><span class="c1"># Health check</span>
|
||
</span><span id="__span-3-17"><a id="__codelineno-3-17" name="__codelineno-3-17" href="#__codelineno-3-17"></a>curl<span class="w"> </span>http://127.0.0.1:5001/health
|
||
</span></code></pre></div>
|
||
<p>Without <code>termux-services</code> (legacy watchdog):</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><span class="c1"># Check if the server is running</span>
|
||
</span><span id="__span-4-2"><a id="__codelineno-4-2" name="__codelineno-4-2" href="#__codelineno-4-2"></a>curl<span class="w"> </span>http://127.0.0.1:5001/health
|
||
</span><span id="__span-4-3"><a id="__codelineno-4-3" name="__codelineno-4-3" href="#__codelineno-4-3"></a>
|
||
</span><span id="__span-4-4"><a id="__codelineno-4-4" name="__codelineno-4-4" href="#__codelineno-4-4"></a><span class="c1"># Restart manually</span>
|
||
</span><span id="__span-4-5"><a id="__codelineno-4-5" name="__codelineno-4-5" href="#__codelineno-4-5"></a><span class="nb">cd</span><span class="w"> </span>~/sms-server/android<span class="w"> </span><span class="o">&&</span><span class="w"> </span>bash<span class="w"> </span>sms-watchdog.sh
|
||
</span></code></pre></div>
|
||
<hr />
|
||
<h2 id="accessing-the-phone">Accessing the Phone<a class="headerlink" href="#accessing-the-phone" title="Permanent link">¶</a></h2>
|
||
<p>There are several ways to run commands on the phone:</p>
|
||
<h3 id="direct-on-phone">Direct (on phone)<a class="headerlink" href="#direct-on-phone" title="Permanent link">¶</a></h3>
|
||
<p>Simply open the Termux app and type commands. Best for initial setup.</p>
|
||
<h3 id="ssh-remote-access">SSH (remote access)<a class="headerlink" href="#ssh-remote-access" title="Permanent link">¶</a></h3>
|
||
<p>Start the SSH server in Termux, then connect from your computer:</p>
|
||
<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"># On the phone (first time only):</span>
|
||
</span><span id="__span-5-2"><a id="__codelineno-5-2" name="__codelineno-5-2" href="#__codelineno-5-2"></a>pkg<span class="w"> </span>install<span class="w"> </span>openssh
|
||
</span><span id="__span-5-3"><a id="__codelineno-5-3" name="__codelineno-5-3" href="#__codelineno-5-3"></a>passwd<span class="w"> </span><span class="c1"># Set a password</span>
|
||
</span><span id="__span-5-4"><a id="__codelineno-5-4" name="__codelineno-5-4" href="#__codelineno-5-4"></a>sshd<span class="w"> </span><span class="c1"># Start SSH server on port 8022</span>
|
||
</span><span id="__span-5-5"><a id="__codelineno-5-5" name="__codelineno-5-5" href="#__codelineno-5-5"></a>
|
||
</span><span id="__span-5-6"><a id="__codelineno-5-6" name="__codelineno-5-6" href="#__codelineno-5-6"></a><span class="c1"># From your computer:</span>
|
||
</span><span id="__span-5-7"><a id="__codelineno-5-7" name="__codelineno-5-7" href="#__codelineno-5-7"></a>ssh<span class="w"> </span>-p<span class="w"> </span><span class="m">8022</span><span class="w"> </span>your-phone-ip
|
||
</span><span id="__span-5-8"><a id="__codelineno-5-8" name="__codelineno-5-8" href="#__codelineno-5-8"></a><span class="c1"># Or with Tailscale:</span>
|
||
</span><span id="__span-5-9"><a id="__codelineno-5-9" name="__codelineno-5-9" href="#__codelineno-5-9"></a>ssh<span class="w"> </span>-p<span class="w"> </span><span class="m">8022</span><span class="w"> </span><span class="m">100</span>.x.x.x
|
||
</span></code></pre></div>
|
||
<h3 id="scrcpy-screen-mirror">scrcpy (screen mirror)<a class="headerlink" href="#scrcpy-screen-mirror" title="Permanent link">¶</a></h3>
|
||
<p>Mirror the phone screen to your computer — great for setup:</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"># Install scrcpy on your computer (Ubuntu)</span>
|
||
</span><span id="__span-6-2"><a id="__codelineno-6-2" name="__codelineno-6-2" href="#__codelineno-6-2"></a>sudo<span class="w"> </span>apt<span class="w"> </span>install<span class="w"> </span>scrcpy
|
||
</span><span id="__span-6-3"><a id="__codelineno-6-3" name="__codelineno-6-3" href="#__codelineno-6-3"></a>
|
||
</span><span id="__span-6-4"><a id="__codelineno-6-4" name="__codelineno-6-4" href="#__codelineno-6-4"></a><span class="c1"># Connect via USB</span>
|
||
</span><span id="__span-6-5"><a id="__codelineno-6-5" name="__codelineno-6-5" href="#__codelineno-6-5"></a>scrcpy
|
||
</span><span id="__span-6-6"><a id="__codelineno-6-6" name="__codelineno-6-6" href="#__codelineno-6-6"></a>
|
||
</span><span id="__span-6-7"><a id="__codelineno-6-7" name="__codelineno-6-7" href="#__codelineno-6-7"></a><span class="c1"># Or wireless (phone must be on same network)</span>
|
||
</span><span id="__span-6-8"><a id="__codelineno-6-8" name="__codelineno-6-8" href="#__codelineno-6-8"></a>scrcpy<span class="w"> </span>--tcpip<span class="o">=</span>phone-ip:5555
|
||
</span></code></pre></div>
|
||
<hr />
|
||
<h2 id="setup-wizard">Setup Wizard<a class="headerlink" href="#setup-wizard" title="Permanent link">¶</a></h2>
|
||
<p>The admin panel provides a guided three-step wizard at <code>/app/sms/setup</code>:</p>
|
||
<h3 id="step-1-prepare-phone">Step 1: Prepare Phone<a class="headerlink" href="#step-1-prepare-phone" title="Permanent link">¶</a></h3>
|
||
<p>Walks you through installing apps, cloning the server, setting the API key, and starting the Flask server. Generates a shared API key that both the server and phone use for authentication.</p>
|
||
<h3 id="step-2-connect">Step 2: Connect<a class="headerlink" href="#step-2-connect" title="Permanent link">¶</a></h3>
|
||
<p>Choose how to find your phone's IP address:</p>
|
||
<div class="tabbed-set tabbed-alternate" data-tabs="1:2"><input checked="checked" id="__tabbed_1_1" name="__tabbed_1" type="radio" /><input id="__tabbed_1_2" name="__tabbed_1" type="radio" /><div class="tabbed-labels"><label for="__tabbed_1_1">Tailscale Auto-Discovery (Recommended)</label><label for="__tabbed_1_2">Manual URL Entry</label></div>
|
||
<div class="tabbed-content">
|
||
<div class="tabbed-block">
|
||
<ol>
|
||
<li>Enter your Tailscale API key (<code>tskey-api-...</code>)</li>
|
||
<li>Click <strong>Discover Devices</strong></li>
|
||
<li>The wizard queries the Tailscale API and lists all devices on your tailnet</li>
|
||
<li>Select your Android phone — the URL auto-fills with its stable <code>100.x.x.x</code> IP</li>
|
||
</ol>
|
||
<div class="admonition info">
|
||
<p class="admonition-title">Getting a Tailscale API Key</p>
|
||
<p>Go to <a href="https://login.tailscale.com/admin/settings/keys">Tailscale Admin Console</a> → Settings → Keys → Generate auth key or API access token.</p>
|
||
</div>
|
||
</div>
|
||
<div class="tabbed-block">
|
||
<p>Enter the phone's URL directly:</p>
|
||
<ul>
|
||
<li><strong>With Tailscale:</strong> <code>http://100.x.x.x:5001</code> (stable IP, works across networks)</li>
|
||
<li><strong>On same LAN:</strong> <code>http://192.168.x.x:5001</code> (changes if phone reconnects)</li>
|
||
<li><strong>Via port forward:</strong> <code>http://your-public-ip:5001</code> (requires router config)</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<h3 id="step-3-test-save">Step 3: Test & Save<a class="headerlink" href="#step-3-test-save" title="Permanent link">¶</a></h3>
|
||
<ol>
|
||
<li>Click <strong>Test Connection</strong> — the wizard calls the phone's <code>/health</code> endpoint</li>
|
||
<li>On success, you'll see device uptime and message count</li>
|
||
<li>Click <strong>Save Configuration</strong> — stores the URL and key encrypted in the database</li>
|
||
<li>The <code>enableSms</code> feature flag is automatically enabled</li>
|
||
</ol>
|
||
<hr />
|
||
<h2 id="how-it-works">How It Works<a class="headerlink" href="#how-it-works" title="Permanent link">¶</a></h2>
|
||
<h3 id="sending-messages">Sending Messages<a class="headerlink" href="#sending-messages" title="Permanent link">¶</a></h3>
|
||
<ol>
|
||
<li>Admin creates an SMS campaign with a message template and contact list</li>
|
||
<li>Campaign is started → messages are queued in <strong>BullMQ</strong> (one at a time, serial delivery)</li>
|
||
<li>For each message, the Express API calls <code>POST /api/sms/send</code> on the phone</li>
|
||
<li>The Flask server on the phone executes <code>termux-sms-send</code> to send via Android's native SMS</li>
|
||
<li>A notification appears on the phone for each sent message</li>
|
||
<li>Results are tracked in the database (success/failure per recipient)</li>
|
||
</ol>
|
||
<h3 id="receiving-responses">Receiving Responses<a class="headerlink" href="#receiving-responses" title="Permanent link">¶</a></h3>
|
||
<p>A background service (<code>sms-response-sync.service.ts</code>) polls the phone's inbox at a configurable interval:</p>
|
||
<ol>
|
||
<li>Calls <code>GET /api/sms/inbox?since=<last_sync_timestamp></code> on the phone</li>
|
||
<li>The Flask server runs <code>termux-sms-list</code> to get new messages</li>
|
||
<li>Incoming messages are matched to contacts and classified by keyword</li>
|
||
<li>Threaded conversations are maintained per contact</li>
|
||
</ol>
|
||
<h3 id="device-monitoring">Device Monitoring<a class="headerlink" href="#device-monitoring" title="Permanent link">¶</a></h3>
|
||
<p>A background service (<code>sms-device-monitor.service.ts</code>) checks phone health periodically:</p>
|
||
<ul>
|
||
<li>Battery level, charging status, temperature</li>
|
||
<li>Server uptime and total messages sent</li>
|
||
<li>Connection status (available/unreachable)</li>
|
||
<li>Results displayed on the SMS Dashboard</li>
|
||
</ul>
|
||
<hr />
|
||
<h2 id="key-features">Key Features<a class="headerlink" href="#key-features" title="Permanent link">¶</a></h2>
|
||
<ul>
|
||
<li><strong>Contact lists</strong> — import, tag, and segment contacts for targeted outreach</li>
|
||
<li><strong>Message templates</strong> — reusable templates with <code>{name}</code> variable placeholders</li>
|
||
<li><strong>BullMQ queue</strong> — serial delivery with configurable delays between messages</li>
|
||
<li><strong>Response sync</strong> — incoming SMS replies synced and classified automatically</li>
|
||
<li><strong>Device monitoring</strong> — battery, uptime, and connectivity reported in real-time</li>
|
||
<li><strong>Conversation view</strong> — threaded message history per contact</li>
|
||
<li><strong>Retry logic</strong> — configurable retry attempts for failed deliveries</li>
|
||
</ul>
|
||
<hr />
|
||
<h2 id="admin-routes">Admin Routes<a class="headerlink" href="#admin-routes" title="Permanent link">¶</a></h2>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Route</th>
|
||
<th>Description</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><code>/app/sms/setup</code></td>
|
||
<td>Guided setup wizard with Tailscale auto-discovery</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>/app/sms</code></td>
|
||
<td>SMS dashboard — campaign overview and device status</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>/app/sms/contacts</code></td>
|
||
<td>Manage contact lists and entries</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>/app/sms/campaigns</code></td>
|
||
<td>Create and monitor SMS campaigns</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>/app/sms/conversations</code></td>
|
||
<td>View threaded conversations with contacts</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<hr />
|
||
<h2 id="configuration">Configuration<a class="headerlink" href="#configuration" title="Permanent link">¶</a></h2>
|
||
<h3 id="environment-variables">Environment Variables<a class="headerlink" href="#environment-variables" title="Permanent link">¶</a></h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Variable</th>
|
||
<th>Default</th>
|
||
<th>Description</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><code>ENABLE_SMS</code></td>
|
||
<td><code>false</code></td>
|
||
<td>Feature flag (also set via setup wizard)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>TERMUX_API_URL</code></td>
|
||
<td>—</td>
|
||
<td>Phone URL, e.g. <code>http://100.x.x.x:5001</code></td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>TERMUX_API_KEY</code></td>
|
||
<td>—</td>
|
||
<td>Shared API key for authentication</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>SMS_DELAY_BETWEEN_MS</code></td>
|
||
<td><code>1000</code></td>
|
||
<td>Delay between messages in a campaign (ms)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>SMS_MAX_RETRIES</code></td>
|
||
<td><code>3</code></td>
|
||
<td>Retry attempts for failed sends</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>SMS_RESPONSE_SYNC_INTERVAL_MS</code></td>
|
||
<td><code>10000</code></td>
|
||
<td>How often to check for incoming replies (ms)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>SMS_DEVICE_MONITOR_INTERVAL_MS</code></td>
|
||
<td><code>30000</code></td>
|
||
<td>How often to check device health (ms)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>TAILSCALE_API_KEY</code></td>
|
||
<td>—</td>
|
||
<td>Tailscale API key for auto-discovery</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>TAILSCALE_TAILNET</code></td>
|
||
<td>—</td>
|
||
<td>Tailscale tailnet name (optional)</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="admonition note">
|
||
<p class="admonition-title">Note</p>
|
||
<p>When you use the setup wizard, configuration is stored in the database and takes priority over environment variables. You don't need to set env vars if you use the wizard.</p>
|
||
</div>
|
||
<h3 id="phone-side-configuration">Phone-Side Configuration<a class="headerlink" href="#phone-side-configuration" title="Permanent link">¶</a></h3>
|
||
<p>On the phone, only one environment variable is needed:</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">export</span><span class="w"> </span><span class="nv">SMS_API_SECRET</span><span class="o">=</span><span class="s1">'your-64-char-hex-key'</span>
|
||
</span></code></pre></div>
|
||
<p>The Flask server also accepts <code>TERMUX_API_KEY</code> as an alias for backwards compatibility.</p>
|
||
<hr />
|
||
<h2 id="phone-api-endpoints">Phone API Endpoints<a class="headerlink" href="#phone-api-endpoints" title="Permanent link">¶</a></h2>
|
||
<p>The Flask server running on the phone exposes these endpoints on port 5001:</p>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Method</th>
|
||
<th>Endpoint</th>
|
||
<th>Auth</th>
|
||
<th>Description</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td><code>GET</code></td>
|
||
<td><code>/health</code></td>
|
||
<td>No</td>
|
||
<td>Server status, uptime, messages sent</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GET</code></td>
|
||
<td><code>/</code></td>
|
||
<td>No</td>
|
||
<td>Web dashboard with endpoint documentation</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>POST</code></td>
|
||
<td><code>/api/sms/send</code></td>
|
||
<td>Yes</td>
|
||
<td>Send an SMS message</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>POST</code></td>
|
||
<td><code>/api/sms/send-reply</code></td>
|
||
<td>Yes</td>
|
||
<td>Send a reply with conversation tracking</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GET</code></td>
|
||
<td><code>/api/sms/inbox</code></td>
|
||
<td>No</td>
|
||
<td>Get incoming messages (with <code>since</code> filter)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GET</code></td>
|
||
<td><code>/api/sms/list</code></td>
|
||
<td>No</td>
|
||
<td>List messages with pagination</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GET</code></td>
|
||
<td><code>/api/sms/history</code></td>
|
||
<td>No</td>
|
||
<td>Get SMS history for a phone number</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GET</code></td>
|
||
<td><code>/api/device/battery</code></td>
|
||
<td>No</td>
|
||
<td>Battery level, health, temperature</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GET</code></td>
|
||
<td><code>/api/device/location</code></td>
|
||
<td>No</td>
|
||
<td>GPS coordinates (requires permission)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GET</code></td>
|
||
<td><code>/api/device/info</code></td>
|
||
<td>No</td>
|
||
<td>Device info + battery + uptime</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>GET</code></td>
|
||
<td><code>/api/contacts/list</code></td>
|
||
<td>No</td>
|
||
<td>Phone address book (with search)</td>
|
||
</tr>
|
||
<tr>
|
||
<td><code>POST</code></td>
|
||
<td><code>/api/campaign/notify</code></td>
|
||
<td>No</td>
|
||
<td>Push notification to device</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<p>Authentication uses the <code>X-API-Key</code> header with the shared secret.</p>
|
||
<hr />
|
||
<h2 id="troubleshooting">Troubleshooting<a class="headerlink" href="#troubleshooting" title="Permanent link">¶</a></h2>
|
||
<h3 id="phone-cant-be-reached">Phone can't be reached<a class="headerlink" href="#phone-cant-be-reached" title="Permanent link">¶</a></h3>
|
||
<p><strong>Symptoms:</strong> Test connection fails, "Connection refused" or timeout.</p>
|
||
<p><strong>Checks:</strong></p>
|
||
<ol>
|
||
<li><strong>Is the Flask server running?</strong> Check Termux — you should see the startup banner</li>
|
||
<li><strong>Is the IP correct?</strong> Run <code>ifconfig</code> in Termux to find the current IP</li>
|
||
<li><strong>Are they on the same network?</strong> If not using Tailscale, both must be on the same LAN</li>
|
||
<li><strong>Is Tailscale connected?</strong> Check the Tailscale app on the phone — it should show "Connected"</li>
|
||
<li><strong>Firewall?</strong> Android rarely blocks incoming connections on Termux, but check if any firewall app is installed</li>
|
||
</ol>
|
||
<div class="language-bash highlight"><pre><span></span><code><span id="__span-8-1"><a id="__codelineno-8-1" name="__codelineno-8-1" href="#__codelineno-8-1"></a><span class="c1"># Quick test from your server</span>
|
||
</span><span id="__span-8-2"><a id="__codelineno-8-2" name="__codelineno-8-2" href="#__codelineno-8-2"></a>curl<span class="w"> </span>http://PHONE_IP:5001/health
|
||
</span></code></pre></div>
|
||
<h3 id="authentication-required-errors">"Authentication required" errors<a class="headerlink" href="#authentication-required-errors" title="Permanent link">¶</a></h3>
|
||
<p><strong>Symptoms:</strong> API calls return 401 with "Authentication required".</p>
|
||
<p><strong>Fix:</strong> The API key on the phone doesn't match the one in the admin panel.</p>
|
||
<div class="language-bash highlight"><pre><span></span><code><span id="__span-9-1"><a id="__codelineno-9-1" name="__codelineno-9-1" href="#__codelineno-9-1"></a><span class="c1"># On the phone, check the current key</span>
|
||
</span><span id="__span-9-2"><a id="__codelineno-9-2" name="__codelineno-9-2" href="#__codelineno-9-2"></a><span class="nb">echo</span><span class="w"> </span><span class="nv">$SMS_API_SECRET</span>
|
||
</span><span id="__span-9-3"><a id="__codelineno-9-3" name="__codelineno-9-3" href="#__codelineno-9-3"></a>
|
||
</span><span id="__span-9-4"><a id="__codelineno-9-4" name="__codelineno-9-4" href="#__codelineno-9-4"></a><span class="c1"># If it doesn't match, update it</span>
|
||
</span><span id="__span-9-5"><a id="__codelineno-9-5" name="__codelineno-9-5" href="#__codelineno-9-5"></a><span class="nb">export</span><span class="w"> </span><span class="nv">SMS_API_SECRET</span><span class="o">=</span><span class="s1">'correct-key-from-admin-panel'</span>
|
||
</span><span id="__span-9-6"><a id="__codelineno-9-6" name="__codelineno-9-6" href="#__codelineno-9-6"></a><span class="nb">echo</span><span class="w"> </span><span class="s1">'export SMS_API_SECRET="correct-key-from-admin-panel"'</span><span class="w"> </span>>><span class="w"> </span>~/.bashrc
|
||
</span><span id="__span-9-7"><a id="__codelineno-9-7" name="__codelineno-9-7" href="#__codelineno-9-7"></a>
|
||
</span><span id="__span-9-8"><a id="__codelineno-9-8" name="__codelineno-9-8" href="#__codelineno-9-8"></a><span class="c1"># Restart the server</span>
|
||
</span><span id="__span-9-9"><a id="__codelineno-9-9" name="__codelineno-9-9" href="#__codelineno-9-9"></a>sv<span class="w"> </span>restart<span class="w"> </span>sms-api
|
||
</span><span id="__span-9-10"><a id="__codelineno-9-10" name="__codelineno-9-10" href="#__codelineno-9-10"></a><span class="c1"># Or without termux-services: pkill -f termux-sms-api-server.py && cd ~/sms-server/android && python termux-sms-api-server.py</span>
|
||
</span></code></pre></div>
|
||
<h3 id="sms-not-sending">SMS not sending<a class="headerlink" href="#sms-not-sending" title="Permanent link">¶</a></h3>
|
||
<p><strong>Symptoms:</strong> Server responds successfully but messages don't arrive.</p>
|
||
<p><strong>Checks:</strong></p>
|
||
<ol>
|
||
<li><strong>SMS permissions granted?</strong> Go to Android Settings → Apps → Termux:API → Permissions → SMS</li>
|
||
<li><strong>Active SIM card?</strong> The phone needs a working SIM with SMS capability</li>
|
||
<li><strong>Message too long?</strong> Maximum 1600 characters per message</li>
|
||
<li><strong>Rate limited?</strong> Minimum 1 second between messages (carrier may enforce longer delays)</li>
|
||
</ol>
|
||
<h3 id="termux-keeps-getting-killed">Termux keeps getting killed<a class="headerlink" href="#termux-keeps-getting-killed" title="Permanent link">¶</a></h3>
|
||
<p><strong>Symptoms:</strong> Server stops after some time, especially when phone screen is off.</p>
|
||
<p><strong>Fix:</strong></p>
|
||
<ol>
|
||
<li><strong>Install <code>termux-services</code></strong> (if not already): <code>bash ~/sms-server/android/setup-services.sh</code> — this uses runit, a proper service supervisor that auto-restarts the server immediately if it crashes</li>
|
||
<li><strong>Disable battery optimization:</strong> Android Settings → Apps → Termux → Battery → <strong>Unrestricted</strong></li>
|
||
<li><strong>Lock Termux in recent apps</strong> — long-press the app card → Lock/Pin</li>
|
||
<li>Samsung: also add Termux, Termux:API, and Termux:Boot to <strong>Settings → Device Care → Battery → Never Sleeping Apps</strong></li>
|
||
<li><strong>Acquire wake lock:</strong> Run <code>termux-wake-lock</code> in Termux (included in boot script)</li>
|
||
</ol>
|
||
<h3 id="server-wont-start-missing-sms_api_secret">Server won't start — "Missing SMS_API_SECRET"<a class="headerlink" href="#server-wont-start-missing-sms_api_secret" title="Permanent link">¶</a></h3>
|
||
<p><strong>Symptoms:</strong> Server exits immediately with a security error.</p>
|
||
<p><strong>Fix:</strong> Set the API key environment variable:</p>
|
||
<div class="language-bash highlight"><pre><span></span><code><span id="__span-10-1"><a id="__codelineno-10-1" name="__codelineno-10-1" href="#__codelineno-10-1"></a><span class="c1"># Generate a new key if you don't have one</span>
|
||
</span><span id="__span-10-2"><a id="__codelineno-10-2" name="__codelineno-10-2" href="#__codelineno-10-2"></a>python<span class="w"> </span>-c<span class="w"> </span><span class="s2">"import secrets; print(secrets.token_hex(32))"</span>
|
||
</span><span id="__span-10-3"><a id="__codelineno-10-3" name="__codelineno-10-3" href="#__codelineno-10-3"></a>
|
||
</span><span id="__span-10-4"><a id="__codelineno-10-4" name="__codelineno-10-4" href="#__codelineno-10-4"></a><span class="c1"># Set it</span>
|
||
</span><span id="__span-10-5"><a id="__codelineno-10-5" name="__codelineno-10-5" href="#__codelineno-10-5"></a><span class="nb">export</span><span class="w"> </span><span class="nv">SMS_API_SECRET</span><span class="o">=</span><span class="s1">'your-generated-key'</span>
|
||
</span><span id="__span-10-6"><a id="__codelineno-10-6" name="__codelineno-10-6" href="#__codelineno-10-6"></a><span class="nb">echo</span><span class="w"> </span><span class="s1">'export SMS_API_SECRET="your-key"'</span><span class="w"> </span>>><span class="w"> </span>~/.bashrc
|
||
</span></code></pre></div>
|
||
<h3 id="rcs-chat-features-interfering-with-replies">RCS / Chat Features interfering with replies<a class="headerlink" href="#rcs-chat-features-interfering-with-replies" title="Permanent link">¶</a></h3>
|
||
<p><strong>Symptoms:</strong> You send SMS messages successfully but some or all replies never appear in the system. Recipients say they replied, but the conversation shows no inbound messages.</p>
|
||
<p><strong>Cause:</strong> Google Messages enables <strong>RCS (Rich Communication Services)</strong> by default. When RCS is active, replies from recipients who also have RCS may be sent over data/Wi-Fi instead of the carrier SMS channel. The Termux server only reads the SMS inbox via <code>termux-sms-list</code>, so RCS messages are invisible to it.</p>
|
||
<p><strong>Fix:</strong> Disable RCS on the SMS phone:</p>
|
||
<ol>
|
||
<li>Open <strong>Google Messages</strong> on the phone</li>
|
||
<li>Tap the profile icon (top right) → <strong>Messages settings</strong></li>
|
||
<li>Tap <strong>RCS chats</strong> (or "Chat features")</li>
|
||
<li><strong>Turn off</strong> "Turn on RCS chats"</li>
|
||
</ol>
|
||
<div class="admonition warning">
|
||
<p class="admonition-title">This must be done on the phone running the SMS server</p>
|
||
<p>Disabling RCS on the server phone forces all outgoing messages to use plain SMS, and ensures replies also come back as SMS. You do not need recipients to change anything on their end — when the server phone sends a plain SMS, the reply will be plain SMS as well (unless the recipient's carrier forces RCS-only, which is rare).</p>
|
||
</div>
|
||
<p><strong>Additional checks:</strong></p>
|
||
<ul>
|
||
<li>Some carriers (e.g. Google Fi, Jio) enable RCS at the carrier level. If disabling in the app doesn't help, contact the carrier to disable RCS on the SIM.</li>
|
||
<li>If the phone has <strong>Samsung Messages</strong> instead of Google Messages, go to Samsung Messages → Settings → Chat settings → turn off.</li>
|
||
<li>After disabling RCS, restart the phone and verify by sending a test message — the send button should show an <strong>SMS</strong> label, not "Chat".</li>
|
||
</ul>
|
||
<h3 id="updating-the-sms-server">Updating the SMS server<a class="headerlink" href="#updating-the-sms-server" title="Permanent link">¶</a></h3>
|
||
<p>To pull the latest version of the server code:</p>
|
||
<div class="language-bash highlight"><pre><span></span><code><span id="__span-11-1"><a id="__codelineno-11-1" name="__codelineno-11-1" href="#__codelineno-11-1"></a><span class="nb">cd</span><span class="w"> </span>~/sms-server
|
||
</span><span id="__span-11-2"><a id="__codelineno-11-2" name="__codelineno-11-2" href="#__codelineno-11-2"></a>git<span class="w"> </span>pull
|
||
</span><span id="__span-11-3"><a id="__codelineno-11-3" name="__codelineno-11-3" href="#__codelineno-11-3"></a>
|
||
</span><span id="__span-11-4"><a id="__codelineno-11-4" name="__codelineno-11-4" href="#__codelineno-11-4"></a><span class="c1"># Restart the server</span>
|
||
</span><span id="__span-11-5"><a id="__codelineno-11-5" name="__codelineno-11-5" href="#__codelineno-11-5"></a>sv<span class="w"> </span>restart<span class="w"> </span>sms-api
|
||
</span><span id="__span-11-6"><a id="__codelineno-11-6" name="__codelineno-11-6" href="#__codelineno-11-6"></a><span class="c1"># Or without termux-services: pkill -f termux-sms-api-server.py && cd android && python termux-sms-api-server.py</span>
|
||
</span></code></pre></div>
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
</article>
|
||
</div>
|
||
|
||
|
||
<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="../email-templates/" class="md-footer__link md-footer__link--prev" aria-label="Previous: Email Templates">
|
||
<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">
|
||
Email Templates
|
||
</div>
|
||
</div>
|
||
</a>
|
||
|
||
|
||
|
||
<a href="../../web/" class="md-footer__link md-footer__link--next" aria-label="Next: Web Content">
|
||
<div class="md-footer__title">
|
||
<span class="md-footer__direction">
|
||
Next
|
||
</span>
|
||
<div class="md-ellipsis">
|
||
Web Content
|
||
</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 © 2024 The Bunker Operations – <a href="#__consent">Change cookie settings</a>
|
||
|
||
</div>
|
||
|
||
|
||
</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 512 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="M173.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9M252.8 8C114.1 8 8 113.3 8 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C436.2 457.8 504 362.9 504 252 504 113.3 391.5 8 252.8 8M105.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2"/></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>
|
||
|
||
|
||
|
||
|
||
|
||
<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.tooltips", "navigation.footer", "navigation.indexes", "navigation.path", "navigation.prune", "navigation.tabs", "navigation.tabs.sticky", "navigation.top", "navigation.tracking", "search.highlight", "search.share", "search.suggest", "toc.follow"], "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="../../../../javascripts/ad-widgets.js"></script>
|
||
|
||
<script src="../../../../javascripts/docs-comments.js"></script>
|
||
|
||
|
||
</body>
|
||
</html> |