#include "dashboard.h"
#include "buffer.h"
#include "monitor.h"
#include "connection.h"
#include "../cJSON.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
static const char *DASHBOARD_HTML =
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
" <title>Reverse Proxy Monitor</title>\n"
" <style>\n"
" * { margin: 0; padding: 0; box-sizing: border-box; }\n"
" body { font-family: -apple-system, system-ui, sans-serif; background: #000; color: #fff; padding: 20px; }\n"
" .header { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); margin-bottom: 30px; gap: 20px; }\n"
" .metric { text-align: center; background: #000; border-radius: 8px; padding: 15px; }\n"
" .metric-value { font-size: 36px; font-weight: 300; }\n"
" .metric-label { font-size: 14px; opacity: 0.7; text-transform: uppercase; margin-top: 5px; }\n"
" .chart-container { background: #000; border-radius: 8px; padding: 20px; margin-bottom: 20px; height: 250px; position: relative; }\n"
" .chart-title { position: absolute; top: 10px; left: 20px; font-size: 14px; opacity: 0.7; z-index: 10; }\n"
" canvas { width: 100% !important; height: 100% !important; }\n"
" .process-table { background: #000; border-radius: 8px; padding: 20px; }\n"
" table { width: 100%; border-collapse: collapse; }\n"
" th, td { padding: 10px; text-align: left; border-bottom: 1px solid #2a2e3e; }\n"
" th { font-weight: 500; opacity: 0.7; }\n"
" .legend { position: absolute; top: 10px; right: 20px; display: flex; gap: 20px; font-size: 12px; z-index: 10; }\n"
" .legend-item { display: flex; align-items: center; gap: 5px; }\n"
" .legend-color { width: 12px; height: 12px; border-radius: 2px; }\n"
" .vhost-chart-container { height: 200px; background: #000; border-radius: 8px; position: relative; padding: 20px; margin-bottom: 10px; }\n"
" .load-values { display: flex; gap: 10px; font-size: 12px; opacity: 0.8; }\n"
" </style>\n"
" <script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n"
"</head>\n"
"<body>\n"
" <div class=\"header\">\n"
" <div class=\"metric\">\n"
" <div class=\"metric-value\" id=\"connections\">0</div>\n"
" <div class=\"metric-label\">Connections</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-value\" id=\"memory\">0</div>\n"
" <div class=\"metric-label\">Memory</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-value\" id=\"cpu\">0</div>\n"
" <div class=\"metric-label\">CPU %</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-value\" id=\"load1\">0.00</div>\n"
" <div class=\"metric-label\">Load 1m</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-value\" id=\"load5\">0.00</div>\n"
" <div class=\"metric-label\">Load 5m</div>\n"
" </div>\n"
" <div class=\"metric\">\n"
" <div class=\"metric-value\" id=\"load15\">0.00</div>\n"
" <div class=\"metric-label\">Load 15m</div>\n"
" </div>\n"
" </div>\n"
"\n"
" <div class=\"chart-container\">\n"
" <div class=\"chart-title\">CPU Usage</div>\n"
" <canvas id=\"cpuChart\"></canvas>\n"
" </div>\n"
"\n"
" <div class=\"chart-container\">\n"
" <div class=\"chart-title\">Memory Usage</div>\n"
" <canvas id=\"memChart\"></canvas>\n"
" </div>\n"
"\n"
" <div class=\"chart-container\">\n"
" <div class=\"chart-title\">Network I/O</div>\n"
" <div class=\"legend\">\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #3498db\"></div><span>RX</span></div>\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #2ecc71\"></div><span>TX</span></div>\n"
" </div>\n"
" <canvas id=\"netChart\"></canvas>\n"
" </div>\n"
"\n"
" <div class=\"chart-container\">\n"
" <div class=\"chart-title\">Disk I/O</div>\n"
" <div class=\"legend\">\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #9b59b6\"></div><span>Read</span></div>\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #e67e22\"></div><span>Write</span></div>\n"
" </div>\n"
" <canvas id=\"diskChart\"></canvas>\n"
" </div>\n"
"\n"
" <div class=\"chart-container\">\n"
" <div class=\"chart-title\">Load Average</div>\n"
" <div class=\"legend\">\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #e74c3c\"></div><span>1 min</span></div>\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #f39c12\"></div><span>5 min</span></div>\n"
" <div class=\"legend-item\"><div class=\"legend-color\" style=\"background: #3498db\"></div><span>15 min</span></div>\n"
" </div>\n"
" <canvas id=\"loadChart\"></canvas>\n"
" </div>\n"
"\n"
" <div class=\"process-table\">\n"
" <table>\n"
" <thead>\n"
" <tr>\n"
" <th>Virtual Host</th>\n"
" <th>HTTP Req</th>\n"
" <th>WS Req</th>\n"
" <th>Total Req</th>\n"
" <th>Avg Resp (ms)</th>\n"
" <th>Sent</th>\n"
" <th>Received</th>\n"
" </tr>\n"
" </thead>\n"
" <tbody id=\"processTable\"></tbody>\n"
" </table>\n"
" </div>\n"
"\n"
" <script>\n"
" const formatTimeTick = (value) => {\n"
" const seconds = Math.abs(Math.round(value / 1000));\n"
" if (seconds === 0) return 'now';\n"
" const m = Math.floor(seconds / 60);\n"
" const s = seconds % 60;\n"
" return `-${m > 0 ? `${m}m ` : ''}${s}s`;\n"
" };\n"
"\n"
" const formatSizeTick = (value) => {\n"
" if (value >= 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(1)} GB/s`;\n"
" if (value >= 1024) return `${(value / 1024).toFixed(1)} MB/s`;\n"
" return `${value.toFixed(0)} KB/s`;\n"
" };\n"
"\n"
" const formatDiskTick = (value) => {\n"
" if (value >= 1024) return `${(value / 1024).toFixed(1)} GB/s`;\n"
" return `${value.toFixed(1)} MB/s`;\n"
" };\n"
"\n"
" const formatBytes = (bytes) => {\n"
" if (bytes === 0) return '0 B';\n"
" const k = 1024;\n"
" const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n"
" const i = Math.floor(Math.log(bytes) / Math.log(k));\n"
" return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n"
" };\n"
"\n"
" const createBaseChartOptions = (historySeconds, yTickCallback) => ({\n"
" responsive: true,\n"
" maintainAspectRatio: false,\n"
" animation: false,\n"
" layout: { padding: { top: 30 } },\n"
" interaction: { mode: 'nearest', axis: 'x', intersect: false },\n"
" scales: {\n"
" x: { type: 'linear', display: true, grid: { color: '#2a2e3e' }, ticks: { color: '#666', maxTicksLimit: 7, callback: formatTimeTick }, min: -historySeconds * 1000, max: 0 },\n"
" y: { display: true, grid: { color: '#2a2e3e' }, ticks: { color: '#666', beginAtZero: true, callback: yTickCallback } }\n"
" },\n"
" plugins: { legend: { display: false }, tooltip: { displayColors: false } },\n"
" elements: { point: { radius: 0 }, line: { borderWidth: 2, tension: 0.4, fill: true } }\n"
" });\n"
"\n"
" const cpuChart = new Chart(document.getElementById('cpuChart'), {\n"
" type: 'line',\n"
" data: { datasets: [{ label: 'CPU %', data: [], borderColor: '#f39c12', backgroundColor: 'rgba(243, 156, 18, 0.1)' }] },\n"
" options: { ...createBaseChartOptions(300, v => `${v}%`), scales: { ...createBaseChartOptions(300).scales, y: { ...createBaseChartOptions(300).scales.y, max: 100, ticks: { ...createBaseChartOptions(300).scales.y.ticks, callback: v => `${v}%` } } } }\n"
" });\n"
"\n"
" const memChart = new Chart(document.getElementById('memChart'), {\n"
" type: 'line',\n"
" data: { datasets: [{ label: 'Memory GB', data: [], borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)' }] },\n"
" options: createBaseChartOptions(300, v => `${v.toFixed(2)} GiB`)\n"
" });\n"
"\n"
" const netChart = new Chart(document.getElementById('netChart'), {\n"
" type: 'line',\n"
" data: {\n"
" datasets: [\n"
" { label: 'RX KB/s', data: [], borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)' },\n"
" { label: 'TX KB/s', data: [], borderColor: '#2ecc71', backgroundColor: 'rgba(46, 204, 113, 0.1)' }\n"
" ]\n"
" },\n"
" options: createBaseChartOptions(300, formatSizeTick)\n"
" });\n"
"\n"
" const diskChart = new Chart(document.getElementById('diskChart'), {\n"
" type: 'line',\n"
" data: {\n"
" datasets: [\n"
" { label: 'Read MB/s', data: [], borderColor: '#9b59b6', backgroundColor: 'rgba(155, 89, 182, 0.1)' },\n"
" { label: 'Write MB/s', data: [], borderColor: '#e67e22', backgroundColor: 'rgba(230, 126, 34, 0.1)' }\n"
" ]\n"
" },\n"
" options: createBaseChartOptions(300, formatDiskTick)\n"
" });\n"
"\n"
" const loadChart = new Chart(document.getElementById('loadChart'), {\n"
" type: 'line',\n"
" data: {\n"
" datasets: [\n"
" { label: 'Load 1m', data: [], borderColor: '#e74c3c', backgroundColor: 'rgba(231, 76, 60, 0.1)' },\n"
" { label: 'Load 5m', data: [], borderColor: '#f39c12', backgroundColor: 'rgba(243, 156, 18, 0.1)' },\n"
" { label: 'Load 15m', data: [], borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)' }\n"
" ]\n"
" },\n"
" options: createBaseChartOptions(300, v => v.toFixed(2))\n"
" });\n"
"\n"
" window.vhostCharts = {};\n"
" let prevProcessNames = [];\n"
"\n"
" async function updateStats() {\n"
" try {\n"
" const response = await fetch('/rproxy/api/stats');\n"
" const data = await response.json();\n"
"\n"
" document.getElementById('connections').textContent = data.current.active_connections;\n"
" document.getElementById('memory').textContent = data.current.memory_gb + ' GiB';\n"
" document.getElementById('cpu').textContent = data.current.cpu_percent + '%';\n"
" document.getElementById('load1').textContent = data.current.load_1m.toFixed(2);\n"
" document.getElementById('load5').textContent = data.current.load_5m.toFixed(2);\n"
" document.getElementById('load15').textContent = data.current.load_15m.toFixed(2);\n"
"\n"
" cpuChart.data.datasets[0].data = data.cpu_history;\n"
" memChart.data.datasets[0].data = data.memory_history;\n"
" netChart.data.datasets[0].data = data.network_rx_history;\n"
" netChart.data.datasets[1].data = data.network_tx_history;\n"
" diskChart.data.datasets[0].data = data.disk_read_history;\n"
" diskChart.data.datasets[1].data = data.disk_write_history;\n"
" loadChart.data.datasets[0].data = data.load1_history;\n"
" loadChart.data.datasets[1].data = data.load5_history;\n"
" loadChart.data.datasets[2].data = data.load15_history;\n"
"\n"
" cpuChart.update('none');\n"
" memChart.update('none');\n"
" netChart.update('none');\n"
" diskChart.update('none');\n"
" loadChart.update('none');\n"
"\n"
" const tbody = document.getElementById('processTable');\n"
" const processNames = data.processes.map(p => p.name).join(',');\n"
" if (processNames !== prevProcessNames.join(',')) {\n"
" Object.values(window.vhostCharts).forEach(chart => chart.destroy());\n"
" window.vhostCharts = {};\n"
" tbody.innerHTML = '';\n"
" data.processes.forEach((p, index) => {\n"
" const colors = ['#3498db', '#2ecc71', '#f39c12', '#e74c3c', '#9b59b6', '#1abc9c'];\n"
" const chartColor = colors[index % colors.length];\n"
" const mainRow = document.createElement('tr');\n"
" mainRow.innerHTML = `<td style='color: ${chartColor}'>${p.name}</td><td>${p.http_requests}</td><td>${p.websocket_requests}</td><td>${p.total_requests}</td><td>${p.avg_request_time_ms.toFixed(2)}</td><td>${formatBytes(p.bytes_sent)}</td><td>${formatBytes(p.bytes_recv)}</td>`;\n"
" tbody.appendChild(mainRow);\n"
" \n"
" const chartRow = document.createElement('tr');\n"
" chartRow.innerHTML = `<td colspan='7' style='padding: 10px 0; border: none;'><div class='vhost-chart-container'><div class='chart-title'>Live Throughput - ${p.name}</div><canvas id='vhostChart${index}'></canvas></div></td>`;\n"
" tbody.appendChild(chartRow);\n"
" });\n"
" prevProcessNames = data.processes.map(p => p.name);\n"
" }\n"
"\n"
" data.processes.forEach((p, index) => {\n"
" const chartId = `vhostChart${index}`;\n"
" const canvas = document.getElementById(chartId);\n"
" if (!canvas) return;\n"
" if (!window.vhostCharts[chartId]) {\n"
" const colors = [\n"
" { border: '#3498db', bg: 'rgba(52, 152, 219, 0.1)' }, { border: '#2ecc71', bg: 'rgba(46, 204, 113, 0.1)' },\n"
" { border: '#f39c12', bg: 'rgba(243, 156, 18, 0.1)' }, { border: '#e74c3c', bg: 'rgba(231, 76, 60, 0.1)' },\n"
" { border: '#9b59b6', bg: 'rgba(155, 89, 182, 0.1)' }, { border: '#1abc9c', bg: 'rgba(26, 188, 156, 0.1)' }\n"
" ];\n"
" const color = colors[index % colors.length];\n"
" window.vhostCharts[chartId] = new Chart(canvas.getContext('2d'), {\n"
" type: 'line',\n"
" data: { datasets: [{ label: 'Throughput KB/s', data: p.throughput_history || [], borderColor: color.border, backgroundColor: color.bg }] },\n"
" options: createBaseChartOptions(60, formatSizeTick)\n"
" });\n"
" } else {\n"
" window.vhostCharts[chartId].data.datasets[0].data = p.throughput_history || [];\n"
" window.vhostCharts[chartId].update('none');\n"
" }\n"
" });\n"
" } catch (e) {\n"
" console.error('Failed to fetch stats:', e);\n"
" }\n"
" }\n"
"\n"
" updateStats();\n"
" setInterval(updateStats, 1000);\n"
" </script>\n"
"</body>\n"
"</html>\n";
void dashboard_serve(connection_t *conn) {
if (!conn) return;
size_t content_len = strlen(DASHBOARD_HTML);
char header[512];
int len = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Content-Length: %zu\r\n"
"Connection: %s\r\n"
"Cache-Control: no-cache\r\n"
"\r\n",
content_len,
conn->request.keep_alive ? "keep-alive" : "close");
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + len + content_len) < 0) {
connection_send_error_response(conn, 500, "Internal Server Error", "Memory allocation failed");
return;
}
memcpy(conn->write_buf.data + conn->write_buf.tail, header, len);
conn->write_buf.tail += len;
memcpy(conn->write_buf.data + conn->write_buf.tail, DASHBOARD_HTML, content_len);
conn->write_buf.tail += content_len;
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
}
static cJSON* format_history(history_deque_t *dq, int window_seconds) {
cJSON *arr = cJSON_CreateArray();
if (!arr || !dq || !dq->points || dq->count == 0) return arr;
double current_time = time(NULL);
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
for (int i = 0; i < dq->count; ++i) {
int current_index = (start_index + i) % dq->capacity;
history_point_t *p = &dq->points[current_index];
if ((current_time - p->time) <= window_seconds) {
cJSON *pt = cJSON_CreateObject();
if (pt) {
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
cJSON_AddNumberToObject(pt, "y", p->value);
cJSON_AddItemToArray(arr, pt);
}
}
}
return arr;
}
static cJSON* format_network_history(network_history_deque_t *dq, int window_seconds, const char *key) {
cJSON *arr = cJSON_CreateArray();
if (!arr || !dq || !dq->points || !key || dq->count == 0) return arr;
double current_time = time(NULL);
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
for (int i = 0; i < dq->count; ++i) {
int current_index = (start_index + i) % dq->capacity;
network_history_point_t *p = &dq->points[current_index];
if ((current_time - p->time) <= window_seconds) {
cJSON *pt = cJSON_CreateObject();
if (pt) {
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
cJSON_AddNumberToObject(pt, "y", strcmp(key, "rx_kbps") == 0 ? p->rx_kbps : p->tx_kbps);
cJSON_AddItemToArray(arr, pt);
}
}
}
return arr;
}
static cJSON* format_disk_history(disk_history_deque_t *dq, int window_seconds, const char *key) {
cJSON *arr = cJSON_CreateArray();
if (!arr || !dq || !dq->points || !key || dq->count == 0) return arr;
double current_time = time(NULL);
int start_index = (dq->head - dq->count + dq->capacity) % dq->capacity;
for (int i = 0; i < dq->count; ++i) {
int current_index = (start_index + i) % dq->capacity;
disk_history_point_t *p = &dq->points[current_index];
if ((current_time - p->time) <= window_seconds) {
cJSON *pt = cJSON_CreateObject();
if (pt) {
cJSON_AddNumberToObject(pt, "x", (long)((p->time - current_time) * 1000));
cJSON_AddNumberToObject(pt, "y", strcmp(key, "read_mbps") == 0 ? p->read_mbps : p->write_mbps);
cJSON_AddItemToArray(arr, pt);
}
}
}
return arr;
}
void dashboard_serve_stats_api(connection_t *conn) {
if (!conn) return;
cJSON *root = cJSON_CreateObject();
if (!root) {
connection_send_error_response(conn, 500, "Internal Server Error", "JSON creation failed");
return;
}
cJSON *current = cJSON_CreateObject();
if (!current) {
cJSON_Delete(root);
connection_send_error_response(conn, 500, "Internal Server Error", "JSON creation failed");
return;
}
cJSON_AddItemToObject(root, "current", current);
char buffer[64];
double last_cpu = 0, last_mem = 0;
double load1 = 0, load5 = 0, load15 = 0;
if (monitor.cpu_history.count > 0) {
int last_idx = (monitor.cpu_history.head - 1 + monitor.cpu_history.capacity) % monitor.cpu_history.capacity;
last_cpu = monitor.cpu_history.points[last_idx].value;
}
if (monitor.memory_history.count > 0) {
int last_idx = (monitor.memory_history.head - 1 + monitor.memory_history.capacity) % monitor.memory_history.capacity;
last_mem = monitor.memory_history.points[last_idx].value;
}
if (monitor.load1_history.count > 0) {
int idx = (monitor.load1_history.head - 1 + monitor.load1_history.capacity) % monitor.load1_history.capacity;
load1 = monitor.load1_history.points[idx].value;
}
if (monitor.load5_history.count > 0) {
int idx = (monitor.load5_history.head - 1 + monitor.load5_history.capacity) % monitor.load5_history.capacity;
load5 = monitor.load5_history.points[idx].value;
}
if (monitor.load15_history.count > 0) {
int idx = (monitor.load15_history.head - 1 + monitor.load15_history.capacity) % monitor.load15_history.capacity;
load15 = monitor.load15_history.points[idx].value;
}
snprintf(buffer, sizeof(buffer), "%.2f", last_cpu);
cJSON_AddStringToObject(current, "cpu_percent", buffer);
snprintf(buffer, sizeof(buffer), "%.2f", last_mem);
cJSON_AddStringToObject(current, "memory_gb", buffer);
cJSON_AddNumberToObject(current, "active_connections", monitor.active_connections);
cJSON_AddNumberToObject(current, "load_1m", load1);
cJSON_AddNumberToObject(current, "load_5m", load5);
cJSON_AddNumberToObject(current, "load_15m", load15);
cJSON_AddItemToObject(root, "cpu_history", format_history(&monitor.cpu_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "memory_history", format_history(&monitor.memory_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "network_rx_history", format_network_history(&monitor.network_history, HISTORY_SECONDS, "rx_kbps"));
cJSON_AddItemToObject(root, "network_tx_history", format_network_history(&monitor.network_history, HISTORY_SECONDS, "tx_kbps"));
cJSON_AddItemToObject(root, "disk_read_history", format_disk_history(&monitor.disk_history, HISTORY_SECONDS, "read_mbps"));
cJSON_AddItemToObject(root, "disk_write_history", format_disk_history(&monitor.disk_history, HISTORY_SECONDS, "write_mbps"));
cJSON_AddItemToObject(root, "throughput_history", format_history(&monitor.throughput_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "load1_history", format_history(&monitor.load1_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "load5_history", format_history(&monitor.load5_history, HISTORY_SECONDS));
cJSON_AddItemToObject(root, "load15_history", format_history(&monitor.load15_history, HISTORY_SECONDS));
cJSON *processes = cJSON_CreateArray();
if (processes) {
cJSON_AddItemToObject(root, "processes", processes);
for (vhost_stats_t *s = monitor.vhost_stats_head; s; s = s->next) {
cJSON *p = cJSON_CreateObject();
if (p) {
cJSON_AddStringToObject(p, "name", s->vhost_name);
cJSON_AddNumberToObject(p, "http_requests", s->http_requests);
cJSON_AddNumberToObject(p, "websocket_requests", s->websocket_requests);
cJSON_AddNumberToObject(p, "total_requests", s->total_requests);
cJSON_AddNumberToObject(p, "avg_request_time_ms", s->avg_request_time_ms);
cJSON_AddNumberToObject(p, "bytes_sent", s->bytes_sent);
cJSON_AddNumberToObject(p, "bytes_recv", s->bytes_recv);
cJSON_AddItemToObject(p, "throughput_history", format_history(&s->throughput_history, 60));
cJSON_AddItemToArray(processes, p);
}
}
}
char *json_string = cJSON_PrintUnformatted(root);
if (!json_string) {
cJSON_Delete(root);
connection_send_error_response(conn, 500, "Internal Server Error", "JSON serialization failed");
return;
}
char header[512];
int hlen = snprintf(header, sizeof(header),
"HTTP/1.1 200 OK\r\n"
"Content-Type: application/json; charset=utf-8\r\n"
"Content-Length: %zu\r\n"
"Connection: %s\r\n"
"Cache-Control: no-cache\r\n"
"\r\n",
strlen(json_string),
conn->request.keep_alive ? "keep-alive" : "close");
if (buffer_ensure_capacity(&conn->write_buf, conn->write_buf.tail + hlen + strlen(json_string)) < 0) {
free(json_string);
cJSON_Delete(root);
connection_send_error_response(conn, 500, "Internal Server Error", "Memory allocation failed");
return;
}
memcpy(conn->write_buf.data + conn->write_buf.tail, header, hlen);
conn->write_buf.tail += hlen;
memcpy(conn->write_buf.data + conn->write_buf.tail, json_string, strlen(json_string));
conn->write_buf.tail += strlen(json_string);
struct epoll_event event = { .data.fd = conn->fd, .events = EPOLLIN | EPOLLOUT };
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &event);
cJSON_Delete(root);
free(json_string);
}