diff options
| author | Your Name <you@example.com> | 2026-05-18 14:50:41 +0530 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-05-18 14:50:41 +0530 |
| commit | 2a86bec93273e2f4ceeab60683058c65dbb1da3d (patch) | |
| tree | 56e66acc4eaa0b5a947bb78d28e494a0adc857e2 /main/captive_portal.c | |
| parent | 0c3f08ac7cf8e970369ec137153107ca8edc3326 (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.c | 86 |
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 • Mint tokens at this URL before paying</div>" | 73 | "<div class='mint-hint'>Tap to copy • 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 | ||
| 236 | static 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 | |||
| 193 | static esp_err_t status_handler(httpd_req_t *req) | 255 | static 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 | ||
| 291 | static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler }; | 353 | static const httpd_uri_t uri_portal = { .uri = "/", .method = HTTP_GET, .handler = portal_handler }; |
| 292 | static const httpd_uri_t uri_grant = { .uri = "/grant_access", .method = HTTP_GET, .handler = grant_access_handler }; | 354 | static const httpd_uri_t uri_grant = { .uri = "/grant_access", .method = HTTP_GET, .handler = grant_access_handler }; |
| 355 | static const httpd_uri_t uri_mints = { .uri = "/mints", .method = HTTP_GET, .handler = mints_handler }; | ||
| 293 | static const httpd_uri_t uri_status = { .uri = "/api/status", .method = HTTP_GET, .handler = status_handler }; | 356 | static const httpd_uri_t uri_status = { .uri = "/api/status", .method = HTTP_GET, .handler = status_handler }; |
| 294 | static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = whoami_handler }; | 357 | static const httpd_uri_t uri_whoami = { .uri = "/whoami", .method = HTTP_GET, .handler = whoami_handler }; |
| 295 | static const httpd_uri_t uri_usage = { .uri = "/usage", .method = HTTP_GET, .handler = usage_handler }; | 358 | static 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); |