Initial vibeproxy implementation
Self-contained HTTP/HTTPS/TCP/UDP reverse proxy manager written in Go, with SQLite storage, a Bootstrap 5 admin UI (Go templates, no JS framework), and a REST API under /api/v1. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QTXFrNP1imCW7jtfL5SZrw
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
// vibeproxy admin UI — minimal vanilla JS, no framework.
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
// Generic delete-confirmation modal. Any element with
|
||||
// data-confirm-delete-form="<form id>" and data-confirm-message="..."
|
||||
// opens #confirmDeleteModal and, on confirm, submits the named form.
|
||||
document.addEventListener("click", function (e) {
|
||||
var trigger = e.target.closest("[data-confirm-delete-form]");
|
||||
if (!trigger) return;
|
||||
e.preventDefault();
|
||||
var formId = trigger.getAttribute("data-confirm-delete-form");
|
||||
var message = trigger.getAttribute("data-confirm-message") || "Are you sure? This cannot be undone.";
|
||||
var modalEl = document.getElementById("confirmDeleteModal");
|
||||
if (!modalEl) return;
|
||||
modalEl.querySelector(".modal-body").textContent = message;
|
||||
var confirmBtn = modalEl.querySelector("[data-confirm-action]");
|
||||
confirmBtn.onclick = function () {
|
||||
var form = document.getElementById(formId);
|
||||
if (form) form.submit();
|
||||
};
|
||||
var modal = bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||
modal.show();
|
||||
});
|
||||
|
||||
// Dynamic add/remove rows for repeatable form sections (backends,
|
||||
// custom headers), driven by a <template id="{tableId}RowTemplate">.
|
||||
document.querySelectorAll("[data-add-row]").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
var tableId = btn.getAttribute("data-add-row");
|
||||
var table = document.getElementById(tableId);
|
||||
var tmpl = document.getElementById(tableId + "RowTemplate");
|
||||
if (!table || !tmpl) return;
|
||||
var tbody = table.querySelector("tbody");
|
||||
tbody.appendChild(tmpl.content.firstElementChild.cloneNode(true));
|
||||
});
|
||||
});
|
||||
document.addEventListener("click", function (e) {
|
||||
var btn = e.target.closest("[data-remove-row]");
|
||||
if (!btn) return;
|
||||
var tr = btn.closest("tr");
|
||||
if (tr) tr.remove();
|
||||
});
|
||||
|
||||
// Live client-side filter for any table with data-searchable="true",
|
||||
// driven by an <input data-search-target="tableId">.
|
||||
document.querySelectorAll("[data-search-target]").forEach(function (input) {
|
||||
input.addEventListener("input", function () {
|
||||
var table = document.getElementById(input.getAttribute("data-search-target"));
|
||||
if (!table) return;
|
||||
var q = input.value.trim().toLowerCase();
|
||||
table.querySelectorAll("tbody tr").forEach(function (row) {
|
||||
row.style.display = row.textContent.toLowerCase().indexOf(q) === -1 ? "none" : "";
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Domain / CIDR chip inputs: comma or newline separated list rendered
|
||||
// into a hidden textarea, e.g. for proxy host domains.
|
||||
document.querySelectorAll("[data-chip-input]").forEach(function (wrap) {
|
||||
var hidden = wrap.querySelector("textarea");
|
||||
var display = wrap.querySelector(".vp-chip-list");
|
||||
var entry = wrap.querySelector("input[type=text]");
|
||||
if (!hidden || !display || !entry) return;
|
||||
|
||||
function items() {
|
||||
return hidden.value.split("\n").map(function (s) { return s.trim(); }).filter(Boolean);
|
||||
}
|
||||
function renderChips() {
|
||||
display.innerHTML = "";
|
||||
items().forEach(function (item, idx) {
|
||||
var chip = document.createElement("span");
|
||||
chip.className = "badge text-bg-secondary vp-chip";
|
||||
chip.textContent = item;
|
||||
var x = document.createElement("button");
|
||||
x.type = "button";
|
||||
x.className = "btn-close btn-close-white btn-sm ms-2";
|
||||
x.setAttribute("aria-label", "Remove");
|
||||
x.addEventListener("click", function () {
|
||||
var list = items();
|
||||
list.splice(idx, 1);
|
||||
hidden.value = list.join("\n");
|
||||
renderChips();
|
||||
});
|
||||
chip.appendChild(x);
|
||||
display.appendChild(chip);
|
||||
});
|
||||
}
|
||||
entry.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault();
|
||||
var v = entry.value.trim().replace(/,$/, "");
|
||||
if (v) {
|
||||
var list = items();
|
||||
list.push(v);
|
||||
hidden.value = list.join("\n");
|
||||
entry.value = "";
|
||||
renderChips();
|
||||
}
|
||||
}
|
||||
});
|
||||
renderChips();
|
||||
});
|
||||
})();
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user