From 1b19afaa339f2f68520d61b634600ddeb04effb7 Mon Sep 17 00:00:00 2001 From: Jesper Jensen Date: Sat, 21 Feb 2026 11:14:42 +0100 Subject: Split up the rendering from the logic --- src/html.c | 599 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 599 insertions(+) create mode 100644 src/html.c (limited to 'src/html.c') diff --git a/src/html.c b/src/html.c new file mode 100644 index 0000000..10a6848 --- /dev/null +++ b/src/html.c @@ -0,0 +1,599 @@ +#include "html.h" +#include "mytime.h" + +#include +#include + +static void emit_stylesheet(FILE *f) { + fprintf(f, + "\n" + ); +} + +static void mask_secret(const char *secret, char *out, size_t out_len) { + size_t len = strlen(secret); + if (len >= 8 && out_len >= 13) { + snprintf(out, out_len, "%.4s....%.4s", secret, secret + len - 4); + } else if (len > 0) { + snprintf(out, out_len, "****"); + } else { + out[0] = '\0'; + } +} + +static const char *status_to_badge_class(const char *status) { + if (strcmp(status, "OK") == 0) return "ok"; + if (strcmp(status, "RUNNING") == 0) return "run"; + if (strcmp(status, "STALE") == 0) return "stale"; + if (strcmp(status, "INTERRUPTED") == 0) return "fail"; + if (strcmp(status, "FINISHED") == 0) return "ok"; + return "ok"; +} + +static const char *status_to_display(const char *status) { + if (strcmp(status, "OK") == 0) return "OK"; + if (strcmp(status, "RUNNING") == 0) return "Running"; + if (strcmp(status, "STALE") == 0) return "Stale"; + if (strcmp(status, "INTERRUPTED") == 0) return "Failed"; + if (strcmp(status, "FINISHED") == 0) return "Done"; + return status; +} + +char *html_render_clients_page(const char *host, const struct ListClientResult *result, size_t *out_len) { + // Count stale clients + int stale_count = 0; + for (int i = 0; i < result->clients_len; i++) { + if (result->clients[i].is_stale) stale_count++; + } + + char *buf = NULL; + size_t buf_len = 0; + FILE *f = open_memstream(&buf, &buf_len); + if (!f) { + return NULL; + } + + fprintf(f, + "\n" + "\n" + "\n" + " \n" + " \n" + " BorgFlag - Clients\n"); + emit_stylesheet(f); + fprintf(f, + "\n" + "\n" + "
\n" + " BorgFlag\n" + "
\n" + "
\n" + "
\n" + "

\n" + " Clients\n" + " %d registered", result->clients_len); + + if (stale_count > 0) { + fprintf(f, "%d stale", stale_count); + } + + fprintf(f, + "\n" + " \n" + "

\n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + "
\n" + "
\n" + " Name\n" + " Secret\n" + " Last Backup\n" + " Status\n" + " \n" + "
\n" + ); + + for (int i = 0; i < result->clients_len; i++) { + char masked_secret[16]; + mask_secret(result->clients[i].secret, masked_secret, sizeof(masked_secret)); + + fprintf(f, + " \n" + " %s\n" + " %s\n" + " %s\n" + " %s\n" + " \n" + " \n", + result->clients[i].id, + result->clients[i].is_stale ? "stale" : "", + result->clients[i].name, + masked_secret, + result->clients[i].last_backup.tv_sec > 0.0 ? format_time(result->clients[i].last_backup).buf : "-", + status_to_badge_class(result->clients[i].status), + status_to_display(result->clients[i].status), + result->clients[i].id + ); + } + + fprintf(f, + "
\n" + "
\n" + "\n" + "\n"); + + fclose(f); + *out_len = buf_len; + return buf; +} + +char *html_render_client_detail_page(const char *host, const struct GetClientOnlyResult *client_result, const struct GetBackupsResult *backups_result, size_t *out_len) { + // Determine status and staleness + int is_stale = 0; + const char *current_status = "OK"; + if (backups_result->backups_len > 0) { + const char *last_status = backups_result->backups[0].status; + if (strcmp(last_status, "RUNNING") == 0) { + current_status = "RUNNING"; + } else if (strcmp(last_status, "INTERRUPTED") == 0) { + current_status = "INTERRUPTED"; + } + } + + char *buf = NULL; + size_t buf_len = 0; + FILE *f = open_memstream(&buf, &buf_len); + if (!f) { + return NULL; + } + + fprintf(f, + "\n" + "\n" + "\n" + " \n" + " \n" + " BorgFlag - %s\n", client_result->name); + emit_stylesheet(f); + fprintf(f, + "\n" + "\n" + "\n" + "
\n" + " BorgFlag\n" + "
\n" + "
\n" + "
\n" + " ← Back to clients\n" + " \n" + "

%s

\n" + "

%d backups %s

\n", + is_stale ? " class=\"stale\"" : "", + client_result->name, + backups_result->backups_len, + status_to_badge_class(current_status), + status_to_display(current_status) + ); + + if (is_stale) { + fprintf(f, + "

Warning: Last backup was more than 24 hours ago. Expected every 24 hours.

\n" + ); + } + + fprintf(f, + " Secret: %s\n" + " \n" + "
\n" + " API Snippets\n" + "
\n" + "
\n" + "

Start backup

\n" + "
curl -X POST http://%s/api/report \\\n"
+		"     -H \"Content-Type: application/json\" \\\n"
+		"     -d '{\n"
+		"           \"client\": \"%s\",\n"
+		"           \"secret\": \"%s\",\n"
+		"           \"event\":  \"begin_backup\"\n"
+		"         }'
\n" + "
\n" + "
\n" + "

End backup

\n" + "
curl -X POST http://%s/api/report \\\n"
+		"     -H \"Content-Type: application/json\" \\\n"
+		"     -d '{\n"
+		"           \"client\": \"%s\",\n"
+		"           \"secret\": \"%s\",\n"
+		"           \"event\":  \"end_backup\"\n"
+		"         }'
\n" + "
\n" + "
\n" + "
\n", + client_result->secret, + host ? host : "localhost", + client_result->name, + client_result->secret, + host ? host : "localhost", + client_result->name, + client_result->secret + ); + + fprintf(f, "

Recent Backups

\n"); + + if(backups_result->backups_len == 0) { + fprintf(f, "

No backups recorded.

\n"); + } else { + fprintf(f, "
\n"); + + for (int i = 0; i < backups_result->backups_len; i++) { + const char *badge_class; + const char *badge_text; + if (strcmp(backups_result->backups[i].status, "RUNNING") == 0) { + badge_class = "run"; + badge_text = "Running"; + } else if (strcmp(backups_result->backups[i].status, "INTERRUPTED") == 0) { + badge_class = "fail"; + badge_text = "Failed"; + } else { + badge_class = "ok"; + badge_text = "Done"; + } + + struct timespec duration = time_sub(&backups_result->backups[i].completed, &backups_result->backups[i].started); + fprintf(f, + "
%s%s
\n", + format_time(backups_result->backups[i].started).buf, + format_duration(duration).buf, + badge_class, + badge_text + ); + } + + fprintf(f, "
\n"); + } + + fprintf(f, + "
\n" + "\n" + "\n"); + + fclose(f); + *out_len = buf_len; + return buf; +} + +char *html_render_login_page(const char *error_msg, size_t *out_len) { + char *buf = NULL; + size_t buf_len = 0; + FILE *f = open_memstream(&buf, &buf_len); + if (!f) return NULL; + + fprintf(f, + "\n" + "\n" + "\n" + " \n" + " \n" + " BorgFlag - Login\n"); + emit_stylesheet(f); + fprintf(f, + "\n" + "\n" + "
\n" + "
\n" + "

BorgFlag

\n"); + + if (error_msg) { + fprintf(f, "
%s
\n", error_msg); + } + + fprintf(f, + "
\n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \n" + "
\n" + " \n" + "
\n" + "
\n" + "\n" + "\n"); + + fclose(f); + *out_len = buf_len; + return buf; +} -- cgit v1.2.3