Mercurial
changeset 135:ffb764d2fcc5
[HgWeb] Updated hg web so it works
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Fri, 09 Jan 2026 11:17:20 -0800 |
| parents | 902e29c38d66 |
| children | 75c144fd6964 |
| files | hg-web/BUILD hg-web/main.c hg-web/src/index.html hg-web/src/index.js seobeo/s_web.c |
| diffstat | 5 files changed, 465 insertions(+), 300 deletions(-) [+] |
line wrap: on
line diff
--- a/hg-web/BUILD Fri Jan 09 08:30:35 2026 -0800 +++ b/hg-web/BUILD Fri Jan 09 11:17:20 2026 -0800 @@ -26,3 +26,13 @@ name = "hg_web_server_bundle", binary = ":hg_web_server", ) + + +cc_binary( + name = "hg_web_server_debug", + srcs = ["main.c"], + deps = ["//seobeo:seobeo_tcp_server_ws_debug"], + data = [":src_files"], + defines = ["REPO_ROOT=\\\"\"/home/mrjunejune/zenbu\"\\\""], +) +
--- a/hg-web/main.c Fri Jan 09 08:30:35 2026 -0800 +++ b/hg-web/main.c Fri Jan 09 11:17:20 2026 -0800 @@ -4,9 +4,14 @@ #include <stdlib.h> #include <string.h> #include <ctype.h> -#include <dirent.h> -#include <sys/stat.h> #include <unistd.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <netdb.h> + +#define HG_SERVE_HOST "127.0.0.1" +#define HG_SERVE_PORT 4444 #define MAX_PATH 4096 @@ -37,19 +42,6 @@ *dst = '\0'; } -static int is_directory(const char *path) -{ - struct stat st; - if (stat(path, &st) != 0) return 0; - return S_ISDIR(st.st_mode); -} - -static int file_exists(const char *path) -{ - struct stat st; - return stat(path, &st) == 0; -} - static char* sanitize_path(const char *input_path, Dowa_Arena *arena) { if (!input_path || strlen(input_path) == 0) @@ -87,6 +79,148 @@ return result; } +// Helper to connect to hg serve +static int hg_proxy_connect(void) +{ + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) + { + Seobeo_Log(SEOBEO_DEBUG, "Failed to create socket\n"); + return -1; + } + + struct sockaddr_in server_addr; + memset(&server_addr, 0, sizeof(server_addr)); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(HG_SERVE_PORT); + inet_pton(AF_INET, HG_SERVE_HOST, &server_addr.sin_addr); + + if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) + { + Seobeo_Log(SEOBEO_DEBUG, "Failed to connect to hg serve at %s:%d\n", HG_SERVE_HOST, HG_SERVE_PORT); + close(sock); + return -1; + } + + return sock; +} + +// Generic helper to proxy a request to hg serve and get the response body +// Returns allocated body on success, NULL on failure +// out_status and out_content_type are optional output parameters +// out_body_len returns the actual body length (for binary content) +static char* hg_proxy_request( + const char *method, + const char *path, + const char *req_body, + size_t body_len, + char *out_status, // should be at least 4 bytes + char *out_content_type, // should be at least 256 bytes + size_t *out_body_len, // optional: returns actual body length + Dowa_Arena *arena) +{ + int sock = hg_proxy_connect(); + if (sock < 0) return NULL; + + // Build HTTP request + char http_request[MAX_PATH * 2]; + snprintf(http_request, sizeof(http_request), + "%s %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Connection: close\r\n" + "Accept: application/json, text/plain, */*\r\n" + "Content-Length: %zu\r\n" + "\r\n", + method, path, HG_SERVE_HOST, HG_SERVE_PORT, body_len); + + Seobeo_Log(SEOBEO_DEBUG, "HG Proxy request: %s %s\n", method, path); + + if (send(sock, http_request, strlen(http_request), 0) < 0) + { + close(sock); + return NULL; + } + + if (body_len > 0 && req_body) + { + send(sock, req_body, body_len, 0); + } + + // Read response + int buffer_size = 1024 * 1024 * 5; // 5MB + char *response_buf = Dowa_Arena_Allocate(arena, buffer_size); + size_t total_read = 0; + ssize_t bytes_read; + + while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0) + { + total_read += bytes_read; + if (total_read >= (size_t)(buffer_size - 1)) break; + } + response_buf[total_read] = '\0'; + close(sock); + + Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read); + + // Parse response headers - use memmem to handle binary content + char *headers_end = NULL; + for (size_t i = 0; i + 3 < total_read; i++) + { + if (response_buf[i] == '\r' && response_buf[i+1] == '\n' && + response_buf[i+2] == '\r' && response_buf[i+3] == '\n') + { + headers_end = response_buf + i; + break; + } + } + if (!headers_end) return NULL; + + // Extract status + if (out_status && strncmp(response_buf, "HTTP/", 5) == 0) + { + char *status_start = strchr(response_buf, ' '); + if (status_start) + { + strncpy(out_status, status_start + 1, 3); + out_status[3] = '\0'; + } + } + + // Extract content-type + if (out_content_type) + { + out_content_type[0] = '\0'; + char *ct_header = strcasestr(response_buf, "Content-Type:"); + if (ct_header && ct_header < headers_end) + { + ct_header += 13; + while (*ct_header == ' ') ct_header++; + char *ct_end = strpbrk(ct_header, "\r\n"); + if (ct_end) + { + size_t ct_len = ct_end - ct_header; + if (ct_len < 256) + { + strncpy(out_content_type, ct_header, ct_len); + out_content_type[ct_len] = '\0'; + } + } + } + } + + // Return body (copy to fresh allocation for clean pointer) + char *body = headers_end + 4; + size_t body_size = total_read - (body - response_buf); + + if (out_body_len) *out_body_len = body_size; + + char *result = Dowa_Arena_Allocate(arena, body_size + 1); + memcpy(result, body, body_size); + result[body_size] = '\0'; + + return result; +} + Seobeo_Request_Entry* ApiListDirectory(Seobeo_Request_Entry *req, Dowa_Arena *arena) { Seobeo_Request_Entry *resp = NULL; @@ -99,76 +233,30 @@ char *safe_path = sanitize_path(decoded_path, arena); - Seobeo_Log(SEOBEO_INFO, "rel_path: %s\n", rel_path); - Seobeo_Log(SEOBEO_INFO, "decoded_path: %s\n", decoded_path); - Seobeo_Log(SEOBEO_INFO, "safe path: %s\n", safe_path); - Seobeo_Log(SEOBEO_INFO, "REPO_ROOT: %s\n", REPO_ROOT); - fflush(stdout); + Seobeo_Log(SEOBEO_INFO, "ApiListDirectory: safe_path='%s'\n", safe_path); + + char hg_path[MAX_PATH]; + if (strlen(safe_path) > 0) + snprintf(hg_path, sizeof(hg_path), "/file/tip/%s?style=json", safe_path); + else + snprintf(hg_path, sizeof(hg_path), "/file/tip/?style=json"); - char full_path[MAX_PATH]; - if (strlen(safe_path) > 0) - snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path); - else - snprintf(full_path, sizeof(full_path), "%s", REPO_ROOT); + char status[4] = "200"; + char content_type[256] = ""; + size_t body_len = 0; + char *hg_response = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena); - if (!is_directory(full_path)) + Seobeo_Log(SEOBEO_DEBUG, "ApiListDirectory: status=%s body_len=%zu\n", status, body_len); + + if (!hg_response || status[0] != '2') { - char *error_json = Dowa_Arena_Allocate(arena, 256); - snprintf(error_json, 256, "{\"error\":\"Directory not found\"}"); - - Dowa_HashMap_Push_Arena(resp, "status", "404", arena); + Seobeo_Log(SEOBEO_DEBUG, "Failed to get directory from hg serve\n"); + Dowa_HashMap_Push_Arena(resp, "status", "502", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); - Dowa_HashMap_Push_Arena(resp, "body", error_json, arena); - return resp; - } - - DIR *dir = opendir(full_path); - if (!dir) - { - char *error_json = Dowa_Arena_Allocate(arena, 256); - snprintf(error_json, 256, "{\"error\":\"Cannot open directory\"}"); - - Dowa_HashMap_Push_Arena(resp, "status", "500", arena); - Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); - Dowa_HashMap_Push_Arena(resp, "body", error_json, arena); + Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to connect to hg serve\"}", arena); return resp; } - - char *json = Dowa_Arena_Allocate(arena, 1024 * 100); - strcpy(json, "{\"files\":["); - - struct dirent *entry; - int first = 1; - - while ((entry = readdir(dir)) != NULL) - { - if (entry->d_name[0] == '.') continue; - - char entry_path[MAX_PATH]; - snprintf(entry_path, sizeof(entry_path), "%s/%s", full_path, entry->d_name); - - int is_dir = is_directory(entry_path); - - char entry_rel_path[MAX_PATH]; - if (strlen(safe_path) > 0) - snprintf(entry_rel_path, sizeof(entry_rel_path), "%s/%s", safe_path, entry->d_name); - else - snprintf(entry_rel_path, sizeof(entry_rel_path), "%s", entry->d_name); - - if (!first) strcat(json, ","); - first = 0; - - char entry_json[MAX_PATH * 2]; - snprintf(entry_json, sizeof(entry_json), - "{\"name\":\"%s\",\"type\":\"%s\",\"path\":\"%s\"}", - entry->d_name, - is_dir ? "directory" : "file", - entry_rel_path); - strcat(json, entry_json); - } - - closedir(dir); - strcat(json, "]}"); + char *json = hg_response; Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); @@ -187,79 +275,59 @@ url_decode(decoded_path, rel_path); char *safe_path = sanitize_path(decoded_path, arena); - Seobeo_Log(SEOBEO_INFO, "rel_path: %s\n", rel_path); - Seobeo_Log(SEOBEO_INFO, "decoded_path: %s\n", decoded_path); - Seobeo_Log(SEOBEO_INFO, "safe path: %s\n", safe_path); - Seobeo_Log(SEOBEO_INFO, "REPO_ROOT: %s\n", REPO_ROOT); - fflush(stdout); + Seobeo_Log(SEOBEO_INFO, "ApiGetFile: safe_path='%s'\n", safe_path); if (strlen(safe_path) == 0) { - char *error = Dowa_Arena_Allocate(arena, 64); - strcpy(error, "File path required"); - Dowa_HashMap_Push_Arena(resp, "status", "400", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); - Dowa_HashMap_Push_Arena(resp, "body", error, arena); - return resp; - } - - char full_path[MAX_PATH]; - snprintf(full_path, sizeof(full_path), "%s/%s", REPO_ROOT, safe_path); - FILE *file = fopen(full_path, "rb"); - if (!file) - { - char *error_msg = "File not found."; - Dowa_HashMap_Push_Arena(resp, "status", "404", arena); - Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); - Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); + Dowa_HashMap_Push_Arena(resp, "body", "File path required", arena); return resp; } - fseek(file, 0, SEEK_END); - size_t file_size = ftell(file); - fseek(file, 0, SEEK_SET); + // Build hg serve URL: /raw-file/tip/<path> + char hg_path[MAX_PATH]; + snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", safe_path); - char *file_data = malloc(file_size + 1); - if (!file_data) + char status[4] = "200"; + char content_type[256] = ""; + size_t body_len = 0; + char *body = hg_proxy_request("GET", hg_path, NULL, 0, status, content_type, &body_len, arena); + + Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: status=%s body_len=%zu\n", status, body_len); + + if (!body) { - fclose(file); - char *error_msg = "Memory allocation failed"; - Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Dowa_HashMap_Push_Arena(resp, "status", "502", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); - Dowa_HashMap_Push_Arena(resp, "body", error_msg, arena); + Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); return resp; } - fread(file_data, 1, file_size, file); - file_data[file_size] = '\0'; - fclose(file); - - char *body = Dowa_Arena_Allocate(arena, file_size + 1); - memcpy(body, file_data, file_size); - body[file_size] = '\0'; - free(file_data); - - if (!body) + if (status[0] != '2') { - char *error = Dowa_Arena_Allocate(arena, 64); - strcpy(error, "Cannot read file"); - - Dowa_HashMap_Push_Arena(resp, "status", "500", arena); + Seobeo_Log(SEOBEO_DEBUG, "ApiGetFile: error response: %s\n", body); + Dowa_HashMap_Push_Arena(resp, "status", status, arena); Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); - Dowa_HashMap_Push_Arena(resp, "body", error, arena); + // Return actual error from hg serve if available + Dowa_HashMap_Push_Arena(resp, "body", body_len > 0 ? body : "File not found", arena); return resp; } - const char *content_type = "text/plain"; - if (strstr(safe_path, ".md")) content_type = "text/markdown"; - else if (strstr(safe_path, ".html")) content_type = "text/html"; - else if (strstr(safe_path, ".css")) content_type = "text/css"; - else if (strstr(safe_path, ".js")) content_type = "application/javascript"; - else if (strstr(safe_path, ".json")) content_type = "application/json"; + // Use content-type from hg serve, or determine from extension + const char *final_content_type = content_type; + if (strlen(content_type) == 0 || strcmp(content_type, "application/octet-stream") == 0) + { + final_content_type = "text/plain"; + if (strstr(safe_path, ".md")) final_content_type = "text/markdown"; + else if (strstr(safe_path, ".html")) final_content_type = "text/html"; + else if (strstr(safe_path, ".css")) final_content_type = "text/css"; + else if (strstr(safe_path, ".js")) final_content_type = "application/javascript"; + else if (strstr(safe_path, ".json")) final_content_type = "application/json"; + } Dowa_HashMap_Push_Arena(resp, "status", "200", arena); - Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena); + Dowa_HashMap_Push_Arena(resp, "content-type", final_content_type, arena); Dowa_HashMap_Push_Arena(resp, "body", body, arena); return resp; @@ -273,47 +341,133 @@ { Seobeo_Request_Entry *resp = NULL; - void *cmd_kv = Dowa_HashMap_Get_Ptr(req, "query_cmd"); - const char *cmd = cmd_kv ? ((Seobeo_Request_Entry*)cmd_kv)->value : ""; - if (strlen(cmd) == 0) + // Get method + void *method_kv = Dowa_HashMap_Get_Ptr(req, "HTTP_Method"); + const char *method = method_kv ? ((Seobeo_Request_Entry*)method_kv)->value : "GET"; + + // Get query string + void *query_kv = Dowa_HashMap_Get_Ptr(req, "QueryString"); + const char *query_string = query_kv ? ((Seobeo_Request_Entry*)query_kv)->value : ""; + + // Get request body for POST + void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body"); + const char *req_body = body_kv ? ((Seobeo_Request_Entry*)body_kv)->value : ""; + size_t body_len = strlen(req_body); + + Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: method=%s query=%s body_len=%zu\n", method, query_string, body_len); + + // Connect to hg serve + int sock = hg_proxy_connect(); + if (sock < 0) { - Dowa_HashMap_Push_Arena(resp, "status", "404", arena); + Dowa_HashMap_Push_Arena(resp, "status", "502", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); + Dowa_HashMap_Push_Arena(resp, "body", "Failed to connect to hg serve", arena); return resp; } - Seobeo_Log(SEOBEO_DEBUG, "cmd: %s\n", cmd); - char command[MAX_PATH]; - snprintf(command, sizeof(command), "hg -R %s serve --stdio 2>&1", REPO_ROOT); - - FILE *hg_pipe = popen(command, "r+"); - if (!hg_pipe) + // Build the HTTP request to forward to hg serve + char http_request[MAX_PATH * 2]; + if (strlen(query_string) > 0) + { + snprintf(http_request, sizeof(http_request), + "%s /?%s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Connection: close\r\n" + "Content-Length: %zu\r\n" + "\r\n", + method, query_string, HG_SERVE_HOST, HG_SERVE_PORT, body_len); + } + else { - Seobeo_Log(SEOBEO_DEBUG, "Failed to open pipe\n"); + snprintf(http_request, sizeof(http_request), + "%s / HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Connection: close\r\n" + "Content-Length: %zu\r\n" + "\r\n", + method, HG_SERVE_HOST, HG_SERVE_PORT, body_len); + } + + // Send HTTP request headers + if (send(sock, http_request, strlen(http_request), 0) < 0) + { + Seobeo_Log(SEOBEO_DEBUG, "Failed to send request to hg serve\n"); + close(sock); + Dowa_HashMap_Push_Arena(resp, "status", "502", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); + Dowa_HashMap_Push_Arena(resp, "body", "Failed to send to hg serve", arena); return resp; } - - // 2. Write the command - fprintf(hg_pipe, "capabilities\n"); - fflush(hg_pipe); - - // 3. Read the response - int buffer_size = 1024 * 1024 * 5; - char *output = Dowa_Arena_Allocate(arena, buffer_size); - if (fgets(output, buffer_size, hg_pipe) != NULL) { - Seobeo_Log(SEOBEO_DEBUG, "SUCCESS! Received: %s\n", output); - } else { - Seobeo_Log(SEOBEO_DEBUG, "FAILURE: No output received from hg.\n"); + + // Send body if present + if (body_len > 0) + { + send(sock, req_body, body_len, 0); + } + + // Read response from hg serve + int buffer_size = 1024 * 1024 * 5; // 5MB + char *response_buf = Dowa_Arena_Allocate(arena, buffer_size); + size_t total_read = 0; + ssize_t bytes_read; + + while ((bytes_read = recv(sock, response_buf + total_read, buffer_size - total_read - 1, 0)) > 0) + { + total_read += bytes_read; + if (total_read >= (size_t)(buffer_size - 1)) break; + } + response_buf[total_read] = '\0'; + close(sock); + + Seobeo_Log(SEOBEO_DEBUG, "HG Proxy: received %zu bytes\n", total_read); + + // Parse HTTP response - find headers end + char *headers_end = strstr(response_buf, "\r\n\r\n"); + if (!headers_end) + { + Dowa_HashMap_Push_Arena(resp, "status", "502", arena); + Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain", arena); + Dowa_HashMap_Push_Arena(resp, "body", "Invalid response from hg serve", arena); + return resp; } - - // 4. Close and check exit code - int status = pclose(hg_pipe); - Seobeo_Log(SEOBEO_DEBUG, "Process exited with status: %d\n", status); + + // Extract status code from first line (e.g., "HTTP/1.1 200 OK") + char status_code[4] = "200"; + if (strncmp(response_buf, "HTTP/", 5) == 0) + { + char *status_start = strchr(response_buf, ' '); + if (status_start) + { + strncpy(status_code, status_start + 1, 3); + status_code[3] = '\0'; + } + } - Seobeo_Log(SEOBEO_DEBUG, "body: %s\n", output); + // Extract content-type from headers + const char *content_type = "application/mercurial-0.1"; + char *ct_header = strcasestr(response_buf, "Content-Type:"); + if (ct_header && ct_header < headers_end) + { + ct_header += 13; // Skip "Content-Type:" + while (*ct_header == ' ') ct_header++; + char *ct_end = strpbrk(ct_header, "\r\n"); + if (ct_end) + { + size_t ct_len = ct_end - ct_header; + char *ct_copy = Dowa_Arena_Allocate(arena, ct_len + 1); + strncpy(ct_copy, ct_header, ct_len); + ct_copy[ct_len] = '\0'; + content_type = ct_copy; + } + } - Dowa_HashMap_Push_Arena(resp, "status", "200", arena); - Dowa_HashMap_Push_Arena(resp, "content-type", "application/mercurial-0.2", arena); - Dowa_HashMap_Push_Arena(resp, "body", output, arena); + // Body starts after \r\n\r\n + char *body = headers_end + 4; + + Dowa_HashMap_Push_Arena(resp, "status", status_code, arena); + Dowa_HashMap_Push_Arena(resp, "content-type", content_type, arena); + Dowa_HashMap_Push_Arena(resp, "body", body, arena); return resp; }
--- a/hg-web/src/index.html Fri Jan 09 08:30:35 2026 -0800 +++ b/hg-web/src/index.html Fri Jan 09 11:17:20 2026 -0800 @@ -34,142 +34,6 @@ </main> <script src="/markdown_to_html.js"></script> - <script> - const API_BASE = '/api/repo'; - - // Get current path from URL - function getCurrentPath() { - const params = new URLSearchParams(window.location.search); - return params.get('path') || ''; - } - - // Update URL without reloading - function updateURL(path) { - const url = path ? `?path=${encodeURIComponent(path)}` : '/'; - window.history.pushState({path}, '', url); - } - - // Render breadcrumb - function renderBreadcrumb(path) { - const breadcrumb = document.getElementById('breadcrumb'); - if (!path) { - breadcrumb.innerHTML = '<a href="/">Root</a>'; - return; - } - - const parts = path.split('/').filter(p => p); - let currentPath = ''; - let html = '<a href="/">Root</a>'; - - parts.forEach((part, index) => { - currentPath += (currentPath ? '/' : '') + part; - html += ` <span>/</span> `; - if (index === parts.length - 1) { - html += `<span>${part}</span>`; - } else { - html += `<a href="?path=${encodeURIComponent(currentPath)}">${part}</a>`; - } - }); - - breadcrumb.innerHTML = html; - } - - // Render file list - function renderFiles(files) { - const fileList = document.getElementById('fileList'); - const emptyState = document.getElementById('emptyState'); - - if (!files || files.length === 0) { - fileList.style.display = 'none'; - emptyState.style.display = 'block'; - return; - } - - emptyState.style.display = 'none'; - fileList.style.display = 'block'; - - // Sort: directories first, then files, alphabetically - files.sort((a, b) => { - if (a.type !== b.type) { - return a.type === 'directory' ? -1 : 1; - } - return a.name.localeCompare(b.name); - }); - - let html = ''; - files.forEach(file => { - const icon = file.type === 'directory' ? '📁' : '📄'; - const className = file.type; - const href = file.type === 'directory' - ? `?path=${encodeURIComponent(file.path)}` - : `/api/repo/file?path=${encodeURIComponent(file.path)}`; - const target = file.type === 'directory' ? '' : 'target="_blank"'; - - html += ` - <div class="file-item ${className}"> - <span class="icon">${icon}</span> - <span class="name"> - <a href="${href}" ${target}>${file.name}</a> - </span> - </div> - `; - }); - - fileList.innerHTML = html; - } - - // Load and render README - async function loadReadme(path) { - const readmeSection = document.getElementById('readmeSection'); - const readmeContent = document.getElementById('readmeContent'); - - try { - const readmePath = path ? `${path}/README.md` : 'README.md'; - const response = await fetch(`/api/repo/readme?path=${encodeURIComponent(readmePath)}`); - - if (response.ok) { - const markdown = await response.text(); - readmeSection.style.display = 'block'; - renderMarkdown(readmeContent, markdown); - } else { - readmeSection.style.display = 'none'; - } - } catch (error) { - readmeSection.style.display = 'none'; - } - } - - // Load directory contents - async function loadDirectory(path) { - try { - const url = path ? `${API_BASE}/list?path=${encodeURIComponent(path)}` : `${API_BASE}/list`; - const response = await fetch(url); - const data = await response.json(); - - if (data.error) { - throw new Error(data.error); - } - - renderBreadcrumb(path); - renderFiles(data.files); - loadReadme(path); - } catch (error) { - console.error('Error loading directory:', error); - document.getElementById('fileList').innerHTML = ` - <div class="error-message">Error loading directory: ${error.message}</div> - `; - } - } - - // Handle browser back/forward - window.addEventListener('popstate', (event) => { - const path = event.state?.path || ''; - loadDirectory(path); - }); - - // Initial load - const currentPath = getCurrentPath(); - loadDirectory(currentPath); - </script> + <script src="/index.js"></script> </body> </html>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg-web/src/index.js Fri Jan 09 11:17:20 2026 -0800 @@ -0,0 +1,126 @@ +const API_BASE = '/api/repo'; + +function getCurrentPath() { + const params = new URLSearchParams(window.location.search); + return params.get('path') || ''; +} + +function renderBreadcrumb(path) { + const breadcrumb = document.getElementById('breadcrumb'); + if (!path) { + breadcrumb.innerHTML = '<a href="/">Root</a>'; + return; + } + + const parts = path.split('/').filter(p => p); + let currentPath = ''; + let html = '<a href="/">Root</a>'; + + parts.forEach((part, index) => { + currentPath += (currentPath ? '/' : '') + part; + html += ` <span>/</span> `; + if (index === parts.length - 1) { + html += `<span>${part}</span>`; + } else { + html += `<a href="?path=${encodeURIComponent(currentPath)}">${part}</a>`; + } + }); + + breadcrumb.innerHTML = html; +} + +function renderFiles(files, directories) { + if (!files || files.length === 0) { + fileList.style.display = 'none'; + emptyState.style.display = 'block'; + return; + } + + emptyState.style.display = 'none'; + fileList.style.display = 'block'; + + let html = ''; + directories.forEach(file => { + const icon = '📁'; + const className = file.type; + const href = `?path=${encodeURIComponent(file.abspath)}`; + const target = ''; + + html += ` + <div class="file-item ${className}"> + <span class="icon">${icon}</span> + <span class="name"> + <a href="${href}" ${target}>${file.basename}</a> + </span> + </div> + `; + }); + files.forEach(file => { + const icon = '📄'; + const className = file.type; + const href = `/api/repo/file?path=${encodeURIComponent(file.abspath)}`; + const target = 'target="_blank"'; + + html += ` + <div class="file-item ${className}"> + <span class="icon">${icon}</span> + <span class="name"> + <a href="${href}" ${target}>${file.basename}</a> + </span> + </div> + `; + }); + + fileList.innerHTML = html; +} + +async function loadReadme(path) { + const readmeSection = document.getElementById('readmeSection'); + const readmeContent = document.getElementById('readmeContent'); + + try { + const readmePath = path ? `${path}/README.md` : 'README.md'; + const response = await fetch(`/api/repo/readme?path=${encodeURIComponent(readmePath)}`); + + if (response.ok) { + const markdown = await response.text(); + readmeSection.style.display = 'block'; + renderMarkdown(readmeContent, markdown); + } else { + readmeSection.style.display = 'none'; + } + } catch (error) { + readmeSection.style.display = 'none'; + } +} + +async function loadDirectory(path) { + try { + const url = path ? `${API_BASE}/list?path=${encodeURIComponent(path)}` : `${API_BASE}/list`; + const response = await fetch(url); + const data = await response.json(); + + if (data.error) { + throw new Error(data.error); + } + + const { files, directories } = data; + + renderBreadcrumb(path); + renderFiles(files, directories); + loadReadme(path); + } catch (error) { + console.error('Error loading directory:', error); + document.getElementById('fileList').innerHTML = ` + <div class="error-message">Error loading directory: ${error.message}</div> + `; + } +} + +window.addEventListener('popstate', (event) => { + const path = event.state?.path || ''; + loadDirectory(path); +}); + +const currentPath = getCurrentPath(); +loadDirectory(currentPath);
--- a/seobeo/s_web.c Fri Jan 09 08:30:35 2026 -0800 +++ b/seobeo/s_web.c Fri Jan 09 11:17:20 2026 -0800 @@ -351,7 +351,18 @@ if (!path_copy) return -1; strcpy(path_copy, raw_path); Dowa_HashMap_Push_Arena(*pp_map, "Path", path_copy, p_arena); - + + // Store full query string for proxying + if (query_str && *query_str) + { + char *qs_copy = Dowa_Arena_Allocate(p_arena, strlen(query_str) + 1); + if (qs_copy) + { + strcpy(qs_copy, query_str); + Dowa_HashMap_Push_Arena(*pp_map, "QueryString", qs_copy, p_arena); + } + } + // GET Params if (query_str && *query_str) {