upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/main/captive_portal.c
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-18 14:50:41 +0530
committerYour Name <you@example.com>2026-05-18 14:50:41 +0530
commit2a86bec93273e2f4ceeab60683058c65dbb1da3d (patch)
tree56e66acc4eaa0b5a947bb78d28e494a0adc857e2 /main/captive_portal.c
parent0c3f08ac7cf8e970369ec137153107ca8edc3326 (diff)
feat: multi-mint health tracker, discovery, portal, multi-wallet (Phase 3-8)
- mint_health.h/c: FreeRTOS probing task, GET /v1/info every 5min, recovery threshold 3, immediate failure, mutex-protected state - cashu.c: health-gated acceptance (config match AND reachable) - tollgate_api.c: one price_per_step tag per reachable mint in discovery - captive_portal.c: mint list with green/grey indicators, /mints API, auto-refresh every 30s via JS - nucula_wallet.h/cpp: multi-wallet (up to 4), route receive to correct wallet by mint URL, balance sums across all wallets - tollgate_main.c: init health tracker + multi-wallet on service start - CMakeLists.txt: add mint_health.c
Diffstat (limited to 'main/captive_portal.c')
-rw-r--r--main/captive_portal.c86
1 files changed, 75 insertions, 11 deletions
diff --git a/main/captive_portal.c b/main/captive_portal.c
index 1a3d5ce..57ef65a 100644
--- a/main/captive_portal.c
+++ b/main/captive_portal.c
@@ -2,6 +2,7 @@
2#include "firewall.h" 2#include "firewall.h"
3#include "session.h" 3#include "session.h"
4#include "config.h" 4#include "config.h"
5#include "mint_health.h"
5#include "esp_log.h" 6#include "esp_log.h"
6#include "esp_wifi.h" 7#include "esp_wifi.h"
7#include "cJSON.h" 8#include "cJSON.h"
@@ -42,9 +43,14 @@ static const char PORTAL_HTML_TEMPLATE[] = \
42".btn:disabled{background:#333;color:#666;cursor:not-allowed}" 43".btn:disabled{background:#333;color:#666;cursor:not-allowed}"
43".mints{background:#252525;border-radius:12px;padding:12px;margin-top:16px;text-align:left}" 44".mints{background:#252525;border-radius:12px;padding:12px;margin-top:16px;text-align:left}"
44".mints-title{color:#888;font-size:12px;margin-bottom:8px}" 45".mints-title{color:#888;font-size:12px;margin-bottom:8px}"
45".mint-url{font-family:monospace;font-size:11px;color:#f7931a;word-break:break-all;" 46".mint-item{display:flex;align-items:center;padding:6px 8px;margin-bottom:4px;"
46"background:#1a1a1a;padding:8px;border-radius:6px;cursor:pointer}" 47"background:#1a1a1a;border-radius:6px;cursor:pointer}"
47".mint-url:active{opacity:0.7}" 48".mint-item:active{opacity:0.7}"
49".mint-dot{width:8px;height:8px;border-radius:50%;margin-right:8px;flex-shrink:0}"
50".mint-dot.green{background:#4caf50}"
51".mint-dot.grey{background:#666}"
52".mint-url{font-family:monospace;font-size:11px;color:#f7931a;word-break:break-all}"
53".mint-url.dim{color:#666}"
48".mint-hint{color:#666;font-size:10px;margin-top:4px}" 54".mint-hint{color:#666;font-size:10px;margin-top:4px}"
49"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}" 55"#status{margin-top:16px;padding:12px;border-radius:8px;display:none;font-size:14px}"
50"#status.success{display:block;background:#1a472a;color:#4caf50}" 56"#status.success{display:block;background:#1a472a;color:#4caf50}"
@@ -63,20 +69,21 @@ static const char PORTAL_HTML_TEMPLATE[] = \
63"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>" 69"<button class='btn' id='payBtn' onclick='payToken()'>Pay & Connect</button>"
64"<div class='mints'>" 70"<div class='mints'>"
65"<div class='mints-title'>SUPPORTED MINTS</div>" 71"<div class='mints-title'>SUPPORTED MINTS</div>"
66"<div class='mint-url' id='mintUrl' onclick='copyMint()'>__MINT_URL__</div>" 72"<div id='mintList'>__MINT_LIST__</div>"
67"<div class='mint-hint'>Tap to copy &bull; Mint tokens at this URL before paying</div>" 73"<div class='mint-hint'>Tap to copy &bull; Green = reachable</div>"
68"</div>" 74"</div>"
69"<div id='status'></div>" 75"<div id='status'></div>"
70"</div>" 76"</div>"
71"<script>" 77"<script>"
72"const mintUrlEl=document.getElementById('mintUrl');" 78"const mintListEl=document.getElementById('mintList');"
73"const mintUrl=mintUrlEl.textContent;"
74"const statusEl=document.getElementById('status');" 79"const statusEl=document.getElementById('status');"
75"const payBtn=document.getElementById('payBtn');" 80"const payBtn=document.getElementById('payBtn');"
76"const tokenInput=document.getElementById('tokenInput');" 81"const tokenInput=document.getElementById('tokenInput');"
77"function copyMint(){" 82"function copyMint(url){"
78"if(navigator.clipboard){navigator.clipboard.writeText(mintUrl);" 83"if(navigator.clipboard){navigator.clipboard.writeText(url);"
79"mintUrlEl.textContent='Copied!';setTimeout(()=>{mintUrlEl.textContent=mintUrl;},1000);}" 84"const el=event.currentTarget;const u=el.querySelector('.mint-url');"
85"const orig=u.textContent;u.textContent='Copied!';"
86"setTimeout(()=>{u.textContent=orig;},1000);}"
80"}" 87"}"
81"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}" 88"function showStatus(msg,type){statusEl.textContent=msg;statusEl.className=type;}"
82"function payToken(){" 89"function payToken(){"
@@ -93,6 +100,20 @@ static const char PORTAL_HTML_TEMPLATE[] = \
93"else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}" 100"else if(d.kind===21023){showStatus('Error: '+(d.content||'Unknown error'),'error');payBtn.disabled=false;}"
94"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});" 101"}).catch(e=>{showStatus(e.message||'Connection error','error');payBtn.disabled=false;});"
95"}" 102"}"
103"function refreshMints(){"
104"fetch('http://__AP_IP__:2121/mints').then(r=>r.json()).then(data=>{"
105"let html='';"
106"for(const m of data){"
107"const cls=m.reachable?'green':'grey';"
108"const urlCls=m.reachable?'mint-url':'mint-url dim';"
109"html+='<div class=\"mint-item\" onclick=\"copyMint(\\''+m.url+'\\')\">';"
110"html+='<span class=\"mint-dot '+cls+'\"></span>';"
111"html+='<span class=\"'+urlCls+'\">'+m.url+'</span></div>';"
112"}"
113"if(html)mintListEl.innerHTML=html;"
114"}).catch(()=>{});"
115"}"
116"setInterval(refreshMints,30000);"
96"</script>" 117"</script>"
97"</body></html>"; 118"</body></html>";
98 119
@@ -122,10 +143,32 @@ static esp_err_t portal_handler(httpd_req_t *req)
122 const char *tpl = PORTAL_HTML_TEMPLATE; 143 const char *tpl = PORTAL_HTML_TEMPLATE;
123 size_t tpl_len = strlen(tpl); 144 size_t tpl_len = strlen(tpl);
124 145
146 char mint_list_html[4096];
147 mint_list_html[0] = '\0';
148 int mint_count = 0;
149 const mint_status_t *mints = mint_health_get_all(&mint_count);
150 for (int i = 0; i < mint_count; i++) {
151 const char *cls = mints[i].reachable ? "green" : "grey";
152 const char *url_cls = mints[i].reachable ? "mint-url" : "mint-url dim";
153 char item[512];
154 snprintf(item, sizeof(item),
155 "<div class='mint-item' onclick='copyMint(\"%s\")'>"
156 "<span class='mint-dot %s'></span>"
157 "<span class='%s'>%s</span></div>",
158 mints[i].url, cls, url_cls, mints[i].url);
159 strncat(mint_list_html, item, sizeof(mint_list_html) - strlen(mint_list_html) - 1);
160 }
161 if (mint_count == 0) {
162 const tollgate_config_t *cfg = tollgate_config_get();
163 snprintf(mint_list_html, sizeof(mint_list_html),
164 "<div class='mint-item'><span class='mint-dot grey'></span>"
165 "<span class='mint-url dim'>%s</span></div>", cfg->mint_url);
166 }
167
125 struct { const char *key; const char *val; } subs[] = { 168 struct { const char *key; const char *val; } subs[] = {
126 { "__AP_IP__", s_ap_ip_str }, 169 { "__AP_IP__", s_ap_ip_str },
127 { "__PRICE__", price_str }, 170 { "__PRICE__", price_str },
128 { "__MINT_URL__", cfg->mint_url }, 171 { "__MINT_LIST__", mint_list_html },
129 }; 172 };
130 int nsubs = sizeof(subs) / sizeof(subs[0]); 173 int nsubs = sizeof(subs) / sizeof(subs[0]);
131 174
@@ -190,6 +233,25 @@ static esp_err_t grant_access_handler(httpd_req_t *req)
190 return ESP_OK; 233 return ESP_OK;
191} 234}
192 235
236static esp_err_t mints_handler(httpd_req_t *req)
237{
238 int mint_count = 0;
239 const mint_status_t *mints = mint_health_get_all(&mint_count);
240 cJSON *arr = cJSON_CreateArray();
241 for (int i = 0; i < mint_count; i++) {
242 cJSON *obj = cJSON_CreateObject();
243 cJSON_AddStringToObject(obj, "url", mints[i].url);
244 cJSON_AddBoolToObject(obj, "reachable", mints[i].reachable);
245 cJSON_AddItemToArray(arr, obj);
246 }
247 char *json = cJSON_PrintUnformatted(arr);
248 httpd_resp_set_type(req, "application/json");
249 httpd_resp_send(req, json, strlen(json));
250 cJSON_free(json);
251 cJSON_Delete(arr);
252 return ESP_OK;
253}
254
193static esp_err_t status_handler(httpd_req_t *req) 255static esp_err_t status_handler(httpd_req_t *req)
194{ 256{
195 const tollgate_config_t *cfg = tollgate_config_get(); 257 const tollgate_config_t *cfg = tollgate_config_get();
@@ -290,6 +352,7 @@ static esp_err_t catchall_handler(httpd_req_t *req)
290 352
291static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler }; 353static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler };
292static const httpd_uri_t uri_grant = { .uri = "/grant_access", .method = HTTP_GET, .handler = grant_access_handler }; 354static const httpd_uri_t uri_grant = { .uri = "/grant_access", .method = HTTP_GET, .handler = grant_access_handler };
355static const httpd_uri_t uri_mints = { .uri = "/mints", .method = HTTP_GET, .handler = mints_handler };
293static const httpd_uri_t uri_status = { .uri = "/api/status", .method = HTTP_GET, .handler = status_handler }; 356static const httpd_uri_t uri_status = { .uri = "/api/status", .method = HTTP_GET, .handler = status_handler };
294static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = whoami_handler }; 357static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = whoami_handler };
295static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = usage_handler }; 358static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = usage_handler };
@@ -320,6 +383,7 @@ esp_err_t captive_portal_start(const char *ap_ip_str)
320 383
321 httpd_register_uri_handler(s_server, &uri_portal); 384 httpd_register_uri_handler(s_server, &uri_portal);
322 httpd_register_uri_handler(s_server, &uri_grant); 385 httpd_register_uri_handler(s_server, &uri_grant);
386 httpd_register_uri_handler(s_server, &uri_mints);
323 httpd_register_uri_handler(s_server, &uri_status); 387 httpd_register_uri_handler(s_server, &uri_status);
324 httpd_register_uri_handler(s_server, &uri_whoami); 388 httpd_register_uri_handler(s_server, &uri_whoami);
325 httpd_register_uri_handler(s_server, &uri_usage); 389 httpd_register_uri_handler(s_server, &uri_usage);