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,118 @@
|
||||
:root {
|
||||
--vp-sidebar-w: 240px;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f4f6f9;
|
||||
}
|
||||
|
||||
.vp-shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.vp-sidebar {
|
||||
width: var(--vp-sidebar-w);
|
||||
flex-shrink: 0;
|
||||
background-color: #1b2430;
|
||||
color: #cfd6e2;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.vp-brand {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
padding: 1.1rem 1.25rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.vp-brand i {
|
||||
color: #4dabf7;
|
||||
}
|
||||
|
||||
.vp-nav {
|
||||
padding: 0.75rem 0.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vp-nav .nav-link {
|
||||
color: #a9b4c4;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.55rem 0.85rem;
|
||||
margin-bottom: 0.15rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.vp-nav .nav-link i {
|
||||
width: 1.25rem;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.vp-nav .nav-link:hover {
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vp-nav .nav-link.active {
|
||||
background-color: #2c5cc5;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vp-sidebar-footer {
|
||||
padding: 0.85rem 1.25rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.vp-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.vp-topbar {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e5e9f0;
|
||||
padding: 0.9rem 1.5rem;
|
||||
}
|
||||
|
||||
.vp-content {
|
||||
padding: 1.5rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.vp-chip-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.vp-chip {
|
||||
font-weight: 500;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
border: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 1.7rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.table-hover tbody tr {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
code.vp-token {
|
||||
word-break: break-all;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+6
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -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