nautd/webui: scripting, labels, settings, set-location, pause fix

Session checkpoint on webui-plugin:
- engine dump (nautctl dump) + engine endgame integration
- per-file move locations persistence; torrent-level "Set location"
  with reset/keep-relative/leave-separate handling + residual prune
- Lua: naut.get_labels, define_settings/get_setting (script_host struct)
- daemon-owned labels (category+tags) + taxonomy persistence; webui write-through
- fix: pausing a completed/seeding torrent now sticks (stop wins over result)
- automation tab responsive layout; anime_sort label gating + settings

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ookami125 2026-06-21 23:19:41 -04:00
parent 6dc711cf57
commit b633b7d216
40 changed files with 3305 additions and 3267 deletions

View file

@ -442,6 +442,13 @@ static uint64_t json_u64(const json_t *obj, const char *key) {
? (uint64_t)json_integer_value(value) : 0;
}
static int64_t json_i64_or(const json_t *obj, const char *key,
int64_t fallback) {
json_t *value = json_object_get(obj, key);
return json_is_integer(value) ? (int64_t)json_integer_value(value)
: fallback;
}
static double json_number_or(const json_t *obj, const char *key,
double fallback) {
json_t *value = json_object_get(obj, key);
@ -456,6 +463,11 @@ static const char *base_name(const char *path) {
}
static char *torrent_name(const json_t *torrent) {
/* The daemon persists the display name captured at add time; prefer it so
* restored torrents (where the in-process name cache is empty) read right
* instead of falling back to an upload path's basename. */
const char *saved = json_string_value(json_object_get(torrent, "name"));
if (saved && *saved) return strdup(saved);
const char *source = json_string_or(torrent, "source", "torrent");
if (strncmp(source, "magnet:", 7) == 0) {
const char *dn = strstr(source, "dn=");
@ -475,9 +487,13 @@ static char *torrent_name(const json_t *torrent) {
static const char *ui_state(const char *state, double progress) {
if (!state) return "stalledDL";
if (strcmp(state, "complete") == 0) return "uploading";
if (strcmp(state, "paused") == 0)
return progress >= 1.0 ? "pausedUP" : "pausedDL";
if (strcmp(state, "stopped") == 0)
return progress >= 1.0 ? "pausedUP" : "pausedDL";
if (strcmp(state, "stopping") == 0) return "pausedDL";
if (strcmp(state, "stalled") == 0)
return progress >= 1.0 ? "stalledUP" : "stalledDL";
if (strcmp(state, "queued") == 0) return "queuedDL";
if (strcmp(state, "error") == 0) return "error";
return "downloading";
@ -786,6 +802,89 @@ static void store_set_name(uint64_t id, const char *name) {
pthread_mutex_unlock(&g_webui.meta_lock);
}
static bool parse_id(const char *text, uint64_t *id);
/* Mirror a torrent's category + tags into the daemon (which persists them and
* exposes the flattened set to Lua via naut.get_labels). The web layer is the
* editing surface; the daemon is the source of truth. Snapshots the assignment
* under meta_lock, then RPCs without it held. */
static void webui_sync_labels(uint64_t id) {
char key[32];
snprintf(key, sizeof key, "%llu", (unsigned long long)id);
pthread_mutex_lock(&g_webui.meta_lock);
json_t *entry = json_object_get(g_webui.assignments, key);
char *category = strdup(entry ? json_string_or(entry, "category", "") : "");
json_t *tags_src = entry ? json_object_get(entry, "tags") : NULL;
json_t *tags = tags_src ? json_deep_copy(tags_src) : json_array();
pthread_mutex_unlock(&g_webui.meta_lock);
json_t *params = json_pack("{s:I,s:s,s:o}", "torrent_id", (json_int_t)id,
"category", category ? category : "",
"tags", tags);
free(category);
if (!params) { json_decref(tags); return; }
json_t *reply = rpc_call_json("set_labels", params);
json_decref(params);
if (reply) json_decref(reply);
}
/* Re-push every torrent's labels (after a global category/tag removal that can
* touch many assignments at once). */
static void webui_sync_all_labels(void) {
pthread_mutex_lock(&g_webui.meta_lock);
size_t n = json_object_size(g_webui.assignments);
uint64_t *ids = n ? malloc(n * sizeof *ids) : NULL;
size_t count = 0;
if (ids) {
const char *key;
json_t *entry;
json_object_foreach(g_webui.assignments, key, entry) {
uint64_t id = 0;
if (parse_id(key, &id)) ids[count++] = id;
}
}
pthread_mutex_unlock(&g_webui.meta_lock);
for (size_t i = 0; i < count; i++) webui_sync_labels(ids[i]);
free(ids);
}
/* Persist the full category + tag lists (including unassigned ones) to the
* daemon so they survive restarts. */
static void webui_sync_taxonomy(void) {
pthread_mutex_lock(&g_webui.meta_lock);
json_t *cats = json_deep_copy(g_webui.categories);
json_t *tags = json_deep_copy(g_webui.tags);
pthread_mutex_unlock(&g_webui.meta_lock);
json_t *params = json_pack("{s:o,s:o}",
"categories", cats ? cats : json_array(),
"tags", tags ? tags : json_array());
if (!params) { json_decref(cats); json_decref(tags); return; }
json_t *reply = rpc_call_json("set_label_taxonomy", params);
json_decref(params);
if (reply) json_decref(reply);
}
/* Seed the category + tag lists from the daemon's persisted copy at startup. */
static void webui_load_taxonomy(void) {
json_t *params = json_object();
json_t *reply = rpc_call_json("get_label_taxonomy", params);
json_decref(params);
if (!json_is_object(reply)) { json_decref(reply); return; }
json_t *cats = json_object_get(reply, "categories");
json_t *tags = json_object_get(reply, "tags");
pthread_mutex_lock(&g_webui.meta_lock);
if (json_is_array(cats)) {
json_decref(g_webui.categories);
g_webui.categories = json_deep_copy(cats);
}
if (json_is_array(tags)) {
json_decref(g_webui.tags);
g_webui.tags = json_deep_copy(tags);
}
pthread_mutex_unlock(&g_webui.meta_lock);
json_decref(reply);
}
static bool store_get_name(uint64_t id, char *out, size_t out_size) {
bool found = false;
pthread_mutex_lock(&g_webui.meta_lock);
@ -808,6 +907,35 @@ static void store_forget(uint64_t id) {
pthread_mutex_unlock(&g_webui.meta_lock);
}
/* On first sight of a torrent (e.g. right after a restart, when the in-memory
* store is empty), seed its assignment + the global category/tag lists from the
* daemon's persisted category/tags. Only creates a missing entry, so live web
* edits are never clobbered. */
static void seed_assignment_from_daemon(uint64_t id, json_t *torrent) {
const char *category = json_string_or(torrent, "category", "");
json_t *tags = json_object_get(torrent, "tags");
char key[32];
snprintf(key, sizeof key, "%llu", (unsigned long long)id);
pthread_mutex_lock(&g_webui.meta_lock);
if (!json_object_get(g_webui.assignments, key)) {
json_t *entry = json_pack(
"{s:s,s:o}", "category", category,
"tags", json_is_array(tags) ? json_deep_copy(tags) : json_array());
if (entry) json_object_set_new(g_webui.assignments, key, entry);
if (category && *category && find_category(category) < 0)
json_array_append_new(g_webui.categories, json_pack(
"{s:s,s:s}", "name", category, "savePath", ""));
size_t i;
json_t *v;
json_array_foreach(tags, i, v) {
const char *t = json_string_value(v);
if (t && *t && find_tag(t) < 0)
json_array_append_new(g_webui.tags, json_string(t));
}
}
pthread_mutex_unlock(&g_webui.meta_lock);
}
/* Fill in category + tags for a torrent from the assignment store. */
static void apply_assignment(json_t *out, uint64_t id) {
pthread_mutex_lock(&g_webui.meta_lock);
@ -838,9 +966,13 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
char *better = strdup(override);
if (better) { free(name); name = better; }
}
bool force_start =
json_boolean_value(json_object_get(torrent, "force_start"));
const char *state = ui_state(json_string_value(json_object_get(torrent,
"state")),
progress);
if (force_start && strcmp(state, "downloading") == 0)
state = progress >= 1.0 ? "forcedUP" : "forcedDL";
json_t *trackers = NULL;
json_t *files = NULL;
@ -899,18 +1031,24 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
json_object_set_new(out, "downloaded", json_integer((json_int_t)done));
json_object_set_new(out, "uploaded", json_integer(0));
json_object_set_new(out, "availability", json_real(1.0));
json_object_set_new(out, "priority", json_integer(1));
int64_t queue_pos = json_i64_or(torrent, "queue_pos", 0);
json_object_set_new(out, "priority",
json_integer((json_int_t)(queue_pos < 0
? 1 : queue_pos + 1)));
json_object_set_new(out, "queuePos", json_integer((json_int_t)queue_pos));
json_object_set_new(out, "trackerHosts", hosts ? hosts : json_array());
json_object_set_new(out, "seqDl", json_false());
json_object_set_new(out, "superSeeding", json_false());
json_object_set_new(out, "forceStart", json_false());
json_object_set_new(out, "forceStart", json_boolean(force_start));
json_object_set_new(out, "timeActive",
json_integer((json_int_t)json_u64(torrent, "elapsed_seconds")));
json_object_set_new(out, "pieceSize",
json_integer(pieces ? (json_int_t)(total / pieces) : 0));
json_object_set_new(out, "state", json_string(state));
json_object_set_new(out, "contentPath", json_string(output));
/* category + tags come from the web-layer assignment store */
/* Seed the web-layer store from the daemon's persisted category/tags the
* first time we see a torrent (survives restarts), then apply it. */
seed_assignment_from_daemon(id, torrent);
apply_assignment(out, id);
if (detail) {
json_object_set_new(out, "comment", json_string(""));
@ -934,6 +1072,8 @@ static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
return out;
}
static json_t *preferences_json(void);
/* Build a fresh snapshot (grid + global stats) with live download rates. */
static json_t *build_snapshot(void) {
json_t *params = json_object();
@ -945,6 +1085,7 @@ static json_t *build_snapshot(void) {
}
speed_retain(torrents);
json_t *prefs = preferences_json();
json_t *items = json_array();
uint64_t active = 0;
uint64_t total_rate = 0;
@ -958,20 +1099,35 @@ static json_t *build_snapshot(void) {
json_t *mapped = map_torrent(torrent, false, dlspeed);
if (!mapped) continue;
const char *state = json_string_value(json_object_get(mapped, "state"));
if (state && strcmp(state, "downloading") == 0) active++;
if (state && (strcmp(state, "downloading") == 0 ||
strcmp(state, "forcedDL") == 0)) active++;
total_rate += (uint64_t)dlspeed;
total_data += done;
json_array_append_new(items, mapped);
int64_t q = json_i64_or(mapped, "queuePos", 0);
size_t pos = 0;
for (; pos < json_array_size(items); pos++) {
json_t *cur = json_array_get(items, pos);
if (q < json_i64_or(cur, "queuePos", 0)) break;
}
if (json_array_insert_new(items, pos, mapped) != 0)
json_decref(mapped);
}
json_decref(torrents);
bool alt_speed = prefs &&
json_boolean_value(json_object_get(prefs, "alt_speed_enabled"));
uint64_t dl_limit = prefs ? json_u64(prefs, alt_speed ? "alt_dl_limit"
: "dl_limit") : 0;
uint64_t up_limit = prefs ? json_u64(prefs, alt_speed ? "alt_up_limit"
: "up_limit") : 0;
json_t *server = json_pack(
"{s:I,s:i,s:I,s:i,s:i,s:i,s:f,s:i,s:s,s:i,s:I,s:i,s:i,s:s,s:i}",
"{s:I,s:i,s:I,s:i,s:I,s:I,s:b,s:f,s:i,s:s,s:i,s:I,s:i,s:i,s:s,s:i}",
"dl_info_speed", (json_int_t)total_rate,
"up_info_speed", 0,
"dl_info_data", (json_int_t)total_data,
"up_info_data", 0,
"dl_rate_limit", 0,
"up_rate_limit", 0,
"dl_rate_limit", (json_int_t)dl_limit,
"up_rate_limit", (json_int_t)up_limit,
"alt_speed_enabled", alt_speed,
"global_ratio", 0.0,
"dht_nodes", 0,
"connection_status", "connected",
@ -981,6 +1137,7 @@ static json_t *build_snapshot(void) {
"total_torrents", (int)json_array_size(items),
"read_cache_hits", "0.0",
"queued_io_jobs", 0);
json_decref(prefs);
return json_pack("{s:I,s:o,s:o}", "ts", (json_int_t)time(NULL) * 1000,
"server", server, "torrents", items);
}
@ -1043,9 +1200,49 @@ static json_t *full_torrent_by_hash(const char *hash) {
return mapped;
}
static json_t *preferences_json(void) {
json_t *params = json_object();
json_t *prefs = rpc_call_json("get_preferences", params);
json_decref(params);
if (!json_is_object(prefs)) {
json_decref(prefs);
prefs = json_object();
}
if (!prefs) return NULL;
json_t *max_active = json_object_get(prefs, "max_active");
if (json_is_integer(max_active) &&
!json_object_get(prefs, "max_active_downloads")) {
json_object_set_new(prefs, "max_active_downloads",
json_integer(json_integer_value(max_active)));
}
if (!json_object_get(prefs, "save_path"))
json_object_set_new(prefs, "save_path",
json_string(getenv("NAUT_WEBUI_SAVE_PATH")
? getenv("NAUT_WEBUI_SAVE_PATH") : "."));
if (!json_object_get(prefs, "dl_limit"))
json_object_set_new(prefs, "dl_limit", json_integer(0));
if (!json_object_get(prefs, "up_limit"))
json_object_set_new(prefs, "up_limit", json_integer(0));
if (!json_object_get(prefs, "alt_dl_limit"))
json_object_set_new(prefs, "alt_dl_limit", json_integer(0));
if (!json_object_get(prefs, "alt_up_limit"))
json_object_set_new(prefs, "alt_up_limit", json_integer(0));
if (!json_object_get(prefs, "alt_speed_enabled"))
json_object_set_new(prefs, "alt_speed_enabled", json_false());
json_object_set_new(prefs, "max_connec", json_integer(500));
json_object_set_new(prefs, "max_connec_per_torrent", json_integer(100));
json_object_set_new(prefs, "max_uploads", json_integer(20));
json_object_set_new(prefs, "max_active_uploads", json_integer(10));
json_object_set_new(prefs, "max_active_torrents",
json_integer((json_int_t)json_i64_or(
prefs, "max_active_downloads", 5)));
return prefs;
}
static void api_meta(int fd) {
json_t *json = json_object();
json_t *preferences = json_object();
json_t *preferences = preferences_json();
if (!json || !preferences) {
json_decref(json);
json_decref(preferences);
@ -1063,12 +1260,6 @@ static void api_meta(int fd) {
json_t *trackers = tracker_summary(torrents);
json_decref(torrents);
json_object_set_new(json, "trackers", trackers ? trackers : json_array());
json_object_set_new(preferences, "save_path",
json_string(getenv("NAUT_WEBUI_SAVE_PATH")
? getenv("NAUT_WEBUI_SAVE_PATH") : "."));
json_object_set_new(preferences, "dl_limit", json_integer(0));
json_object_set_new(preferences, "up_limit", json_integer(0));
json_object_set_new(preferences, "alt_speed_enabled", json_false());
json_object_set_new(json, "preferences", preferences);
json_object_set_new(json, "searchPlugins", json_array());
http_json(fd, 200, json);
@ -1190,6 +1381,13 @@ static void api_add(int fd, const char *body, size_t len) {
json_object_set_new(params, "output", json_string(save_path));
if (source && *source) json_object_set_new(params, "source", json_string(source));
if (data && *data) json_object_set_new(params, "data", json_string(data));
if (json_object_get(req, "paused"))
json_object_set_new(params, "paused",
json_boolean(json_boolean_value(
json_object_get(req, "paused"))));
/* Forward the display name so the daemon persists it for restore. */
if (display_name[0])
json_object_set_new(params, "name", json_string(display_name));
json_t *result = rpc_call_json("add_torrent", params);
json_decref(params);
json_decref(req);
@ -1236,10 +1434,33 @@ static void api_delete(int fd, const char *body, size_t len) {
if (removed) publish_snapshot();
}
/* Category/tag assignment is web-layer state the plugin owns, so those verbs
* are honored here. Engine-level verbs (pause/resume/recheck/queue/rate
* limits) have no daemon support yet, so we return 501 instead of pretending
* they worked; the front end shows that as an honest "Action failed" toast. */
static bool rpc_for_torrent(const char *method, uint64_t id, json_t *extra) {
json_t *params = json_object();
if (!params) return false;
json_object_set_new(params, "torrent_id", json_integer((json_int_t)id));
if (json_is_object(extra)) {
const char *key;
json_t *value;
json_object_foreach(extra, key, value)
json_object_set(params, key, value);
}
json_t *result = rpc_call_json(method, params);
json_decref(params);
if (!result) return false;
json_decref(result);
return true;
}
static const char *queue_op_for_action(const char *action) {
if (strcmp(action, "topPriority") == 0) return "top";
if (strcmp(action, "bottomPriority") == 0) return "bottom";
if (strcmp(action, "increasePriority") == 0) return "up";
if (strcmp(action, "decreasePriority") == 0) return "down";
return NULL;
}
/* Category/tag assignment is web-layer state. Engine-backed verbs delegate to
* nautd RPCs so toolbar actions mutate the real queue/lifecycle state. */
static void api_action(int fd, const char *body, size_t len) {
json_t *req = read_body_json(body, len);
const char *raw_action = json_string_value(json_object_get(req, "action"));
@ -1248,6 +1469,7 @@ static void api_action(int fd, const char *body, size_t len) {
json_t *hashes = json_object_get(req, "hashes");
json_t *params = json_object_get(req, "params");
bool handled = false;
int affected = 0;
if (json_is_array(hashes) &&
(strcmp(action, "setCategory") == 0 ||
strcmp(action, "addTags") == 0 ||
@ -1263,12 +1485,54 @@ static void api_action(int fd, const char *body, size_t len) {
else
store_update_tags(id, json_object_get(params, "tags"),
strcmp(action, "addTags") == 0);
webui_sync_labels(id); /* mirror to the daemon (persist + Lua) */
affected++;
}
handled = true;
}
if (json_is_array(hashes) && !handled) {
const char *rpc = NULL;
json_t *extra = NULL;
if (strcmp(action, "pause") == 0) {
rpc = "pause_torrent";
} else if (strcmp(action, "resume") == 0) {
rpc = "resume_torrent";
} else if (strcmp(action, "forceStart") == 0) {
rpc = "resume_torrent";
extra = json_pack("{s:b}", "force", 1);
} else if (strcmp(action, "recheck") == 0) {
rpc = "recheck_torrent";
} else if (strcmp(action, "setSavePath") == 0) {
const char *sp =
json_string_value(json_object_get(params, "savePath"));
if (sp && *sp) {
rpc = "set_save_path";
extra = json_pack("{s:s,s:b}", "savePath", sp, "reset",
json_boolean_value(
json_object_get(params, "reset")));
}
} else {
const char *op = queue_op_for_action(action);
if (op) {
rpc = "queue_move";
extra = json_pack("{s:s}", "op", op);
}
}
if (rpc) {
size_t index;
json_t *hash;
json_array_foreach(hashes, index, hash) {
uint64_t id = 0;
if (!parse_id(json_string_value(hash), &id)) continue;
if (rpc_for_torrent(rpc, id, extra)) affected++;
}
json_decref(extra);
handled = true;
}
}
json_decref(req);
if (handled) {
json_t *json = json_pack("{s:b}", "ok", 1);
json_t *json = json_pack("{s:b,s:i}", "ok", 1, "affected", affected);
http_json(fd, 200, json);
json_decref(json);
publish_snapshot();
@ -1282,14 +1546,143 @@ static void api_action(int fd, const char *body, size_t len) {
json_decref(json);
}
static void api_preferences(int fd, const char *method,
const char *body, size_t len) {
if (strcmp(method, "GET") == 0) {
json_t *prefs = preferences_json();
if (!prefs) {
http_text(fd, 502, "Bad Gateway", "get_preferences failed");
return;
}
http_json(fd, 200, prefs);
json_decref(prefs);
return;
}
if (strcmp(method, "POST") != 0) {
http_text(fd, 405, "Method Not Allowed", "method not allowed");
return;
}
json_t *req = read_body_json(body, len);
json_t *params = json_object();
if (!req || !params) {
json_decref(req);
json_decref(params);
http_text(fd, 500, "Internal Server Error", "oom");
return;
}
const char *keys[] = {
"dl_limit", "up_limit", "alt_dl_limit", "alt_up_limit",
"alt_speed_enabled", "max_active"
};
for (size_t i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
json_t *v = json_object_get(req, keys[i]);
if (v) json_object_set(params, keys[i], v);
}
json_t *max = json_object_get(req, "max_active_downloads");
if (max) json_object_set(params, "max_active", max);
json_t *result = rpc_call_json("set_preferences", params);
json_decref(params);
json_decref(req);
if (!json_is_object(result)) {
json_decref(result);
http_text(fd, 502, "Bad Gateway", "set_preferences failed");
return;
}
json_decref(result);
json_t *prefs = preferences_json();
http_json(fd, 200, prefs);
json_decref(prefs);
publish_snapshot();
}
static void api_altspeed(int fd) {
json_t *params = json_object();
json_t *result = rpc_call_json("toggle_altspeed", params);
json_decref(params);
if (!json_is_object(result)) {
json_decref(result);
http_text(fd, 502, "Bad Gateway", "toggle_altspeed failed");
return;
}
http_json(fd, 200, result);
json_decref(result);
publish_snapshot();
}
/* POST /api/script/settings — persist user-edited script setting values. Body:
* { "settings": { "<key>": "<value>", ... } }. */
static void api_script_settings(int fd, const char *method, const char *body,
size_t len) {
if (strcmp(method, "POST") != 0) {
http_text(fd, 405, "Method Not Allowed", "method not allowed");
return;
}
json_t *req = read_body_json(body, len);
json_t *settings = req ? json_object_get(req, "settings") : NULL;
if (!json_is_object(settings)) {
json_decref(req);
http_text(fd, 400, "Bad Request", "missing settings object");
return;
}
json_t *params = json_object();
json_object_set(params, "settings", settings);
json_decref(req);
json_t *result = rpc_call_json("set_script_settings", params);
json_decref(params);
if (!json_is_object(result)) {
json_decref(result);
http_text(fd, 502, "Bad Gateway", "set_script_settings failed");
return;
}
http_json(fd, 200, result);
json_decref(result);
}
static void api_script(int fd, const char *method, const char *body, size_t len) {
json_t *params = NULL;
json_t *result = NULL;
const char *rpc_name = "script_status";
if (strcmp(method, "GET") == 0) {
params = json_object();
result = rpc_call_json("script_status", params);
} else if (strcmp(method, "POST") == 0) {
rpc_name = "update_script";
json_t *req = read_body_json(body, len);
const char *source = json_string_value(json_object_get(req, "source"));
if (!source) {
json_decref(req);
http_text(fd, 400, "Bad Request", "missing source");
return;
}
params = json_object();
json_object_set(params, "source", json_object_get(req, "source"));
json_decref(req);
result = rpc_call_json("update_script", params);
} else {
http_text(fd, 405, "Method Not Allowed", "method not allowed");
return;
}
json_decref(params);
if (!json_is_object(result)) {
json_decref(result);
char msg[96];
snprintf(msg, sizeof msg, "%s failed", rpc_name);
http_text(fd, 502, "Bad Gateway", msg);
return;
}
http_json(fd, 200, result);
json_decref(result);
}
/* POST /api/categories and /api/categories/delete */
static void api_categories(int fd, const char *body, size_t len, bool remove) {
json_t *req = read_body_json(body, len);
const char *name = json_string_value(json_object_get(req, "name"));
if (name && *name) {
if (remove) store_remove_category(name);
if (remove) { store_remove_category(name); webui_sync_all_labels(); }
else store_add_category(name,
json_string_value(json_object_get(req, "savePath")));
webui_sync_taxonomy(); /* persist the category list via the daemon */
}
json_decref(req);
pthread_mutex_lock(&g_webui.meta_lock);
@ -1304,8 +1697,9 @@ static void api_tags(int fd, const char *body, size_t len, bool remove) {
json_t *req = read_body_json(body, len);
const char *name = json_string_value(json_object_get(req, "name"));
if (name && *name) {
if (remove) store_remove_tag(name);
if (remove) { store_remove_tag(name); webui_sync_all_labels(); }
else store_add_tag(name);
webui_sync_taxonomy(); /* persist the tag list via the daemon */
}
json_decref(req);
pthread_mutex_lock(&g_webui.meta_lock);
@ -1428,11 +1822,13 @@ static void handle_api(int fd, const char *method, char *path,
} else if (strcmp(path, "/api/meta") == 0 && strcmp(method, "GET") == 0) {
api_meta(fd);
} else if (strcmp(path, "/api/preferences") == 0) {
api_meta(fd);
api_preferences(fd, method, body, body_len);
} else if (strcmp(path, "/api/altspeed") == 0 && strcmp(method, "POST") == 0) {
json_t *json = json_pack("{s:b}", "alt_speed_enabled", 0);
http_json(fd, 200, json);
json_decref(json);
api_altspeed(fd);
} else if (strcmp(path, "/api/script/settings") == 0) {
api_script_settings(fd, method, body, body_len);
} else if (strcmp(path, "/api/script") == 0) {
api_script(fd, method, body, body_len);
} else if (strcmp(path, "/api/categories") == 0 &&
strcmp(method, "POST") == 0) {
api_categories(fd, body, body_len, false);
@ -1720,6 +2116,7 @@ naut_err naut_plugin_register(const naut_host_api *host) {
if (error != NAUT_OK) goto fail_store;
error = start_server();
if (error != NAUT_OK) goto fail_store;
webui_load_taxonomy(); /* restore category + tag lists from the daemon */
return NAUT_OK;
fail_store: