@@ -223,6 +223,22 @@ void datum_api_var_STRATUM_JOB_SIGOPS(char *buffer, size_t buffer_size, const T_
223223void datum_api_var_STRATUM_JOB_TXNCOUNT (char * buffer , size_t buffer_size , const T_DATUM_API_DASH_VARS * vardata ) {
224224 snprintf (buffer , buffer_size , "%u" , (unsigned )vardata -> sjob -> block_template -> txn_count );
225225}
226+ void datum_api_var_BITCOIND_ACTIVE_NODE_URL (char * buffer , size_t buffer_size , const T_DATUM_API_DASH_VARS * vardata ) {
227+ const int active_index = datum_config .bitcoind_current_node_index ;
228+ if (active_index >= 0 && active_index < datum_config .bitcoind_node_count ) {
229+ snprintf (buffer , buffer_size , "%s" , datum_config .bitcoind_nodes [active_index ].rpcurl );
230+ } else {
231+ snprintf (buffer , buffer_size , "N/A" );
232+ }
233+ }
234+ void datum_api_var_BITCOIND_ACTIVE_NODE_PRIORITY (char * buffer , size_t buffer_size , const T_DATUM_API_DASH_VARS * vardata ) {
235+ const int active_index = datum_config .bitcoind_current_node_index ;
236+ if (active_index >= 0 && active_index < datum_config .bitcoind_node_count ) {
237+ snprintf (buffer , buffer_size , "%d" , datum_config .bitcoind_nodes [active_index ].priority );
238+ } else {
239+ snprintf (buffer , buffer_size , "N/A" );
240+ }
241+ }
226242
227243
228244DATUM_API_VarEntry var_entries [] = {
@@ -256,7 +272,10 @@ DATUM_API_VarEntry var_entries[] = {
256272 {"STRATUM_JOB_WEIGHT" , datum_api_var_STRATUM_JOB_WEIGHT },
257273 {"STRATUM_JOB_SIGOPS" , datum_api_var_STRATUM_JOB_SIGOPS },
258274 {"STRATUM_JOB_TXNCOUNT" , datum_api_var_STRATUM_JOB_TXNCOUNT },
259-
275+
276+ {"BITCOIND_ACTIVE_NODE_URL" , datum_api_var_BITCOIND_ACTIVE_NODE_URL },
277+ {"BITCOIND_ACTIVE_NODE_PRIORITY" , datum_api_var_BITCOIND_ACTIVE_NODE_PRIORITY },
278+
260279 {NULL , NULL } // Mark the end of the array
261280};
262281
@@ -395,13 +414,32 @@ static void http_resp_prevent_caching(struct MHD_Response * const response) {
395414
396415static enum MHD_Result datum_api_formdata_to_json_cb (void * const cls , const enum MHD_ValueKind kind , const char * const key , const char * const filename , const char * const content_type , const char * const transfer_encoding , const char * const data , const uint64_t off , const size_t size ) {
397416 if (!key ) return MHD_YES ;
398- if (off ) return MHD_YES ;
399-
417+
400418 assert (cls );
401419 json_t * const j = cls ;
402-
403- json_object_set_new (j , key , json_stringn (data , size ));
404-
420+
421+ if (off == 0 ) {
422+ // First chunk - create new field
423+ json_object_set_new (j , key , json_stringn (data , size ));
424+ } else {
425+ // Continuation chunk - append to existing field
426+ json_t * existing = json_object_get (j , key );
427+ if (existing && json_is_string (existing )) {
428+ const char * existing_str = json_string_value (existing );
429+ size_t existing_len = json_string_length (existing );
430+
431+ // Create new buffer with combined data
432+ char * combined = malloc (existing_len + size + 1 );
433+ if (combined ) {
434+ memcpy (combined , existing_str , existing_len );
435+ memcpy (combined + existing_len , data , size );
436+ combined [existing_len + size ] = '\0' ;
437+ json_object_set_new (j , key , json_string (combined ));
438+ free (combined );
439+ }
440+ }
441+ }
442+
405443 return MHD_YES ;
406444}
407445
@@ -1076,17 +1114,50 @@ size_t datum_api_fill_config_var(const char *var_start, const size_t var_name_le
10761114 return snprintf (replacement , replacement_max_len , "%d" , val );
10771115}
10781116
1117+ int datum_api_bitcoind_nodes_json (struct MHD_Connection * connection ) {
1118+ struct MHD_Response * response ;
1119+ json_t * nodes_array = json_array ();
1120+
1121+ for (int i = 0 ; i < datum_config .bitcoind_node_count ; i ++ ) {
1122+ T_BITCOIND_NODE_CONFIG * node = & datum_config .bitcoind_nodes [i ];
1123+ json_t * node_obj = json_object ();
1124+
1125+ json_object_set_new (node_obj , "index" , json_integer (i ));
1126+ json_object_set_new (node_obj , "rpcurl" , json_string (node -> rpcurl ));
1127+ json_object_set_new (node_obj , "rpcuser" , json_string (node -> rpcuser ));
1128+ json_object_set_new (node_obj , "priority" , json_integer (node -> priority ));
1129+ json_object_set_new (node_obj , "enabled" , json_boolean (node -> enabled ));
1130+ json_object_set_new (node_obj , "is_active" , json_boolean (i == datum_config .bitcoind_current_node_index ));
1131+ json_object_set_new (node_obj , "consecutive_failures" , json_integer (node -> consecutive_failures ));
1132+ json_object_set_new (node_obj , "total_failures" , json_integer (node -> total_failures ));
1133+ json_object_set_new (node_obj , "total_successes" , json_integer (node -> total_successes ));
1134+
1135+ json_array_append_new (nodes_array , node_obj );
1136+ }
1137+
1138+ char * json_str = json_dumps (nodes_array , JSON_INDENT (2 ));
1139+ json_decref (nodes_array );
1140+
1141+ if (!json_str ) {
1142+ return datum_api_do_error (connection , MHD_HTTP_INTERNAL_SERVER_ERROR );
1143+ }
1144+
1145+ response = MHD_create_response_from_buffer (strlen (json_str ), json_str , MHD_RESPMEM_MUST_FREE );
1146+ MHD_add_response_header (response , "Content-Type" , "application/json" );
1147+ return datum_api_submit_uncached_response (connection , MHD_HTTP_OK , response );
1148+ }
1149+
10791150int datum_api_config_dashboard (struct MHD_Connection * connection ) {
10801151 struct MHD_Response * response ;
10811152 size_t sz = 0 , max_sz = 0 ;
10821153 char * output = NULL ;
1083-
1154+
10841155 max_sz = www_config_html_sz * 2 ;
10851156 output = malloc (max_sz );
10861157 if (!output ) {
10871158 return MHD_NO ;
10881159 }
1089-
1160+
10901161 sz += datum_api_fill_vars (www_config_html , output , max_sz , datum_api_fill_config_var , NULL );
10911162
10921163 response = MHD_create_response_from_buffer (sz , output , MHD_RESPMEM_MUST_FREE );
@@ -1325,6 +1396,91 @@ bool datum_api_config_set(const char * const key, const char * const val, struct
13251396 }
13261397 // TODO: apply change without restarting (and don't interfere with existing jobs)
13271398 status -> need_restart = true;
1399+ } else if (0 == strcmp (key , "bitcoind_failover_cooldown_seconds" )) {
1400+ const int val_int = datum_atoi_strict (val , strlen (val ));
1401+ if (val_int == datum_config .bitcoind_failover_cooldown_sec ) return true;
1402+ if (val_int > 300 || val_int < 5 ) {
1403+ json_array_append_new (errors , json_string_nocheck ("failover cooldown must be between 5 and 300 seconds" ));
1404+ return false;
1405+ }
1406+ datum_config .bitcoind_failover_cooldown_sec = val_int ;
1407+ datum_api_json_modify_new ("bitcoind" , "failover_cooldown_seconds" , json_integer (val_int ));
1408+ status -> modified_config = true;
1409+ status -> need_restart = true;
1410+ } else if (0 == strcmp (key , "bitcoind_max_consecutive_failures" )) {
1411+ const int val_int = datum_atoi_strict (val , strlen (val ));
1412+ if (val_int == datum_config .bitcoind_max_consecutive_failures ) return true;
1413+ if (val_int > 100 || val_int < 1 ) {
1414+ json_array_append_new (errors , json_string_nocheck ("max consecutive failures must be between 1 and 100" ));
1415+ return false;
1416+ }
1417+ datum_config .bitcoind_max_consecutive_failures = val_int ;
1418+ datum_api_json_modify_new ("bitcoind" , "max_consecutive_failures" , json_integer (val_int ));
1419+ status -> modified_config = true;
1420+ status -> need_restart = true;
1421+ } else if (0 == strcmp (key , "bitcoind_try_higher_priority_nodes" )) {
1422+ const bool val_bool = datum_atoi_strict (val , strlen (val ));
1423+ if (val_bool == datum_config .bitcoind_try_higher_priority ) return true;
1424+ datum_config .bitcoind_try_higher_priority = val_bool ;
1425+ datum_api_json_modify_new ("bitcoind" , "try_higher_priority_nodes" , json_boolean (val_bool ));
1426+ status -> modified_config = true;
1427+ status -> need_restart = true;
1428+ } else if (0 == strcmp (key , "bitcoind_nodes_json" )) {
1429+ json_error_t jerror ;
1430+ json_t * const nodes_array = json_loads (val , 0 , & jerror );
1431+ if (!nodes_array || !json_is_array (nodes_array )) {
1432+ DLOG_ERROR ("Failed to parse bitcoind nodes JSON: %s (line %d, column %d)" , jerror .text , jerror .line , jerror .column );
1433+ json_array_append_new (errors , json_string_nocheck ("Invalid bitcoind nodes JSON" ));
1434+ return false;
1435+ }
1436+
1437+ // Get existing nodes to preserve passwords marked with ***KEEP_EXISTING***
1438+ json_t * const config = datum_config .config_json ;
1439+ json_t * bitcoind_obj = json_object_get (config , "bitcoind" );
1440+ json_t * existing_nodes = NULL ;
1441+ if (bitcoind_obj && json_is_object (bitcoind_obj )) {
1442+ existing_nodes = json_object_get (bitcoind_obj , "nodes" );
1443+ }
1444+
1445+ // Process each node in the new array
1446+ size_t index ;
1447+ json_t * node ;
1448+ json_array_foreach (nodes_array , index , node ) {
1449+ json_t * password = json_object_get (node , "rpcpassword" );
1450+ if (password && json_is_string (password )) {
1451+ const char * pass_str = json_string_value (password );
1452+ if (0 == strcmp (pass_str , "***KEEP_EXISTING***" )) {
1453+ // Find matching node in existing config by index
1454+ json_t * index_obj = json_object_get (node , "index" );
1455+ if (index_obj && json_is_integer (index_obj ) && existing_nodes && json_is_array (existing_nodes )) {
1456+ json_int_t node_index = json_integer_value (index_obj );
1457+ if (node_index >= 0 && (size_t )node_index < json_array_size (existing_nodes )) {
1458+ json_t * existing_node = json_array_get (existing_nodes , node_index );
1459+ if (existing_node ) {
1460+ // Found matching node by index, copy password
1461+ json_t * existing_pass = json_object_get (existing_node , "rpcpassword" );
1462+ if (existing_pass && json_is_string (existing_pass )) {
1463+ json_object_set (node , "rpcpassword" , existing_pass );
1464+ }
1465+ }
1466+ }
1467+ }
1468+ }
1469+ }
1470+ // Remove the index field before saving (it's only used for matching)
1471+ json_object_del (node , "index" );
1472+ }
1473+
1474+ // Save to config
1475+ if (!bitcoind_obj || !json_is_object (bitcoind_obj )) {
1476+ bitcoind_obj = json_object ();
1477+ json_object_set_new (config , "bitcoind" , bitcoind_obj );
1478+ }
1479+ json_object_set (bitcoind_obj , "nodes" , nodes_array );
1480+
1481+ status -> modified_config = true;
1482+ status -> need_restart = true;
1483+ json_decref (nodes_array );
13281484 } else if (0 == strcmp (key , "bitcoind_rpcurl" )) {
13291485 if (0 == strcmp (val , datum_config .bitcoind_rpcurl )) return true;
13301486 if (strlen (val ) > 128 ) {
@@ -1753,6 +1909,8 @@ enum MHD_Result datum_api_answer(void *cls, struct MHD_Connection *connection, c
17531909 return datum_api_asset (connection , "image/x-icon" , www_assets_icons_favicon_ico , www_assets_icons_favicon_ico_sz , www_assets_icons_favicon_ico_etag );
17541910 } else if (!strcmp (url , "/assets/style.css" )) {
17551911 return datum_api_asset (connection , "text/css" , www_assets_style_css , www_assets_style_css_sz , www_assets_style_css_etag );
1912+ } else if (!strcmp (url , "/api/bitcoind_nodes" )) {
1913+ return datum_api_bitcoind_nodes_json (connection );
17561914 }
17571915 break ;
17581916 }
0 commit comments