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:
2026-08-17 10:28:59 -05:00
co-authored by Claude Sonnet 5
commit ba1f7c4c03
105 changed files with 9142 additions and 0 deletions
@@ -0,0 +1,72 @@
{{define "access_list_form"}}{{template "header" .}}
{{$d := .Data}}{{$l := $d.List}}
<div class="card"><div class="card-body">
<form method="post" action="{{if $d.IsNew}}/access-lists/new{{else}}/access-lists/{{$l.ID}}/edit{{end}}">
{{template "csrf_field" .CSRFToken}}
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Name</label>
<input class="form-control" name="name" value="{{$l.Name}}" required>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="default_allow" id="da" {{if $l.DefaultAllow}}checked{{end}}>
<label class="form-check-label" for="da">Default allow (unmatched IPs are allowed)</label>
</div>
</div>
<div class="col-12">
<label class="form-label">Comments</label>
<input class="form-control" name="comments" value="{{$l.Comments}}">
</div>
</div>
<h6>CIDR Rules</h6>
<table class="table table-sm align-middle" id="rulesTable">
<thead><tr><th style="width:10rem">Action</th><th>CIDR</th><th></th></tr></thead>
<tbody>
{{range $l.Rules}}
<tr>
<td><select class="form-select form-select-sm" name="rule_action">
<option value="allow" {{if eq .Action "allow"}}selected{{end}}>Allow</option>
<option value="deny" {{if eq .Action "deny"}}selected{{end}}>Deny</option>
</select></td>
<td><input class="form-control form-control-sm" name="rule_cidr" value="{{.CIDR}}" placeholder="10.0.0.0/8"></td>
<td><button type="button" class="btn btn-sm btn-outline-danger" data-remove-row><i class="bi bi-x"></i></button></td>
</tr>
{{end}}
</tbody>
</table>
<button type="button" class="btn btn-sm btn-outline-primary mb-4" data-add-row="rulesTable"><i class="bi bi-plus-lg"></i> Add Rule</button>
<template id="rulesTableRowTemplate"><tr>
<td><select class="form-select form-select-sm" name="rule_action"><option value="allow" selected>Allow</option><option value="deny">Deny</option></select></td>
<td><input class="form-control form-control-sm" name="rule_cidr" placeholder="10.0.0.0/8"></td>
<td><button type="button" class="btn btn-sm btn-outline-danger" data-remove-row><i class="bi bi-x"></i></button></td>
</tr></template>
<h6>HTTP Basic Auth Users <span class="text-secondary small">(optional — leave off for IP-only lists)</span></h6>
<table class="table table-sm align-middle" id="usersTable">
<thead><tr><th>Username</th><th>Password <span class="text-secondary">(leave blank to keep existing)</span></th><th></th></tr></thead>
<tbody>
{{range $l.BasicAuthUsers}}
<tr>
<td><input class="form-control form-control-sm" name="auth_username" value="{{.Username}}"></td>
<td><input class="form-control form-control-sm" type="password" name="auth_password" placeholder="••••••••"></td>
<td><button type="button" class="btn btn-sm btn-outline-danger" data-remove-row><i class="bi bi-x"></i></button></td>
</tr>
{{end}}
</tbody>
</table>
<button type="button" class="btn btn-sm btn-outline-primary mb-4" data-add-row="usersTable"><i class="bi bi-plus-lg"></i> Add User</button>
<template id="usersTableRowTemplate"><tr>
<td><input class="form-control form-control-sm" name="auth_username"></td>
<td><input class="form-control form-control-sm" type="password" name="auth_password" placeholder="••••••••"></td>
<td><button type="button" class="btn btn-sm btn-outline-danger" data-remove-row><i class="bi bi-x"></i></button></td>
</tr></template>
<div>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Save</button>
<a href="/access-lists" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div></div>
{{template "footer" .}}{{end}}
+33
View File
@@ -0,0 +1,33 @@
{{define "access_lists"}}{{template "header" .}}
<div class="d-flex justify-content-between align-items-center mb-3">
<input type="text" class="form-control w-auto" placeholder="Search access lists…" data-search-target="aclTable">
<a href="/access-lists/new" class="btn btn-primary"><i class="bi bi-plus-lg"></i> Add Access List</a>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="aclTable">
<thead class="table-light"><tr><th>Name</th><th>Default</th><th>Rules</th><th>Basic Auth Users</th><th></th></tr></thead>
<tbody>
{{range .Data}}
<tr>
<td><a href="/access-lists/{{.ID}}/edit">{{.Name}}</a>{{if .Comments}}<div class="small text-secondary">{{.Comments}}</div>{{end}}</td>
<td>{{if .DefaultAllow}}<span class="badge text-bg-success">allow</span>{{else}}<span class="badge text-bg-danger">deny</span>{{end}}</td>
<td>{{len .Rules}} rule(s)</td>
<td>{{len .BasicAuthUsers}} user(s)</td>
<td class="text-end">
<a href="/access-lists/{{.ID}}/edit" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a>
<form method="post" action="/access-lists/{{.ID}}/delete" id="del-acl-{{.ID}}" class="d-inline">
{{template "csrf_field" $.CSRFToken}}
</form>
<button type="button" class="btn btn-sm btn-outline-danger" data-confirm-delete-form="del-acl-{{.ID}}" data-confirm-message="Delete access list &quot;{{.Name}}&quot;?"><i class="bi bi-trash"></i></button>
</td>
</tr>
{{else}}
<tr><td colspan="5" class="text-center text-secondary py-4">No access lists yet.</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{template "confirm_delete_modal" .}}
{{template "footer" .}}{{end}}
+62
View File
@@ -0,0 +1,62 @@
{{define "api_tokens"}}{{template "header" .}}
<div class="card mb-4">
<div class="card-header">API Access</div>
<div class="card-body">
<p>The REST API is available under <code>/api/v1</code> on this admin listener, authenticated with a bearer token:</p>
<pre class="bg-light p-3 rounded"><code>curl -H "Authorization: Bearer vp_xxxxxxxx" https://your-server:8081/api/v1/proxy-hosts</code></pre>
<p class="mb-0 text-secondary">Resources: proxy-hosts, streams, certificates, access-lists, settings, logs/requests, logs/audit, stats, tokens.</p>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">API Tokens</h6>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newTokenModal"><i class="bi bi-plus-lg"></i> New Token</button>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light"><tr><th>Name</th><th>Prefix</th><th>Created</th><th>Last Used</th><th>Status</th><th></th></tr></thead>
<tbody>
{{range .Data}}
<tr>
<td>{{.Name}}</td>
<td><code>{{.Prefix}}…</code></td>
<td>{{fmtTime .CreatedAt}}</td>
<td>{{if .LastUsedAt}}{{timeAgo (deref .LastUsedAt)}}{{else}}never{{end}}</td>
<td>{{if .RevokedAt}}<span class="badge text-bg-secondary">revoked</span>{{else}}<span class="badge text-bg-success">active</span>{{end}}</td>
<td class="text-end">
{{if not .RevokedAt}}
<form method="post" action="/api-tokens/{{.ID}}/revoke" id="rev-tok-{{.ID}}" class="d-inline">
{{template "csrf_field" $.CSRFToken}}
</form>
<button type="button" class="btn btn-sm btn-outline-danger" data-confirm-delete-form="rev-tok-{{.ID}}" data-confirm-message="Revoke token &quot;{{.Name}}&quot;? Anything using it will stop working immediately.">Revoke</button>
{{end}}
</td>
</tr>
{{else}}
<tr><td colspan="6" class="text-center text-secondary py-4">No API tokens yet.</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="newTokenModal" tabindex="-1">
<div class="modal-dialog"><div class="modal-content">
<form method="post" action="/api-tokens/new">
{{template "csrf_field" .CSRFToken}}
<div class="modal-header"><h5 class="modal-title">New API Token</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<label class="form-label">Name</label>
<input class="form-control" name="name" placeholder="e.g. terraform, ci-pipeline" required>
<p class="small text-secondary mt-2 mb-0">The token is shown once after creation — store it securely.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Create Token</button>
</div>
</form>
</div></div>
</div>
{{template "confirm_delete_modal" .}}
{{template "footer" .}}{{end}}
+24
View File
@@ -0,0 +1,24 @@
{{define "backends"}}{{template "header" .}}
<p class="text-secondary">Live backend status from the running configuration. Add or edit backends from the owning Proxy Host.</p>
<div class="card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="backendsListTable">
<thead class="table-light"><tr><th>Proxy Host</th><th>Backend</th><th>Weight</th><th>Health Check</th><th>Config</th><th>Live Health</th></tr></thead>
<tbody>
{{range .Data}}
<tr>
<td><a href="/proxy-hosts/{{.ProxyHostID}}/edit">{{.ProxyHostName}}</a></td>
<td>{{.Host}}:{{.Port}}</td>
<td>{{.Weight}}</td>
<td>{{.HealthCheck}}</td>
<td>{{if .Enabled}}<span class="badge text-bg-success">enabled</span>{{else}}<span class="badge text-bg-secondary">disabled</span>{{end}}</td>
<td>{{if .Healthy}}<span class="badge text-bg-success"><i class="bi bi-heart-pulse"></i> healthy</span>{{else}}<span class="badge text-bg-danger"><i class="bi bi-heart-pulse"></i> unhealthy</span>{{end}}</td>
</tr>
{{else}}
<tr><td colspan="6" class="text-center text-secondary py-4">No backends configured.</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{template "footer" .}}{{end}}
+85
View File
@@ -0,0 +1,85 @@
{{define "certificates"}}{{template "header" .}}
<div class="d-flex justify-content-end gap-2 mb-3">
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#issueLEModal"><i class="bi bi-shield-plus"></i> Request Let's Encrypt</button>
<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#uploadCertModal"><i class="bi bi-upload"></i> Upload Custom</button>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light"><tr><th>Name</th><th>Domains</th><th>Type</th><th>Expires</th><th>Status</th><th></th></tr></thead>
<tbody>
{{range .Data}}
<tr>
<td>{{.Name}}</td>
<td>{{join .Domains ", "}}</td>
<td><span class="badge text-bg-{{if eq .Type "letsencrypt"}}success{{else}}secondary{{end}}">{{.Type}}</span></td>
<td>{{if .NotAfter}}{{fmtTime (deref .NotAfter)}}{{else}}—{{end}}</td>
<td>{{if .LastError}}<span class="badge text-bg-danger" title="{{.LastError}}">error</span>{{else}}<span class="badge text-bg-success">ok</span>{{end}}</td>
<td class="text-end">
{{if eq .Type "letsencrypt"}}
<form method="post" action="/certificates/{{.ID}}/renew" class="d-inline">
{{template "csrf_field" $.CSRFToken}}
<button class="btn btn-sm btn-outline-secondary" title="Renew now"><i class="bi bi-arrow-clockwise"></i></button>
</form>
{{end}}
<form method="post" action="/certificates/{{.ID}}/delete" id="del-cert-{{.ID}}" class="d-inline">
{{template "csrf_field" $.CSRFToken}}
</form>
<button type="button" class="btn btn-sm btn-outline-danger" data-confirm-delete-form="del-cert-{{.ID}}" data-confirm-message="Delete certificate &quot;{{.Name}}&quot;? Proxy hosts using it will fall back to no TLS certificate."><i class="bi bi-trash"></i></button>
</td>
</tr>
{{else}}
<tr><td colspan="6" class="text-center text-secondary py-4">No certificates yet.</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="issueLEModal" tabindex="-1">
<div class="modal-dialog"><div class="modal-content">
<form method="post" action="/certificates/letsencrypt">
{{template "csrf_field" .CSRFToken}}
<div class="modal-header"><h5 class="modal-title">Request Let's Encrypt Certificate</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<div class="mb-3"><label class="form-label">Name</label><input class="form-control" name="name" required></div>
<div class="mb-3"><label class="form-label">Domains (one per line, wildcards need DNS-01)</label><textarea class="form-control" name="domains" rows="3" required></textarea></div>
<div class="mb-3"><label class="form-label">Contact Email (optional — falls back to Settings)</label><input class="form-control" name="email" type="email"></div>
<div class="mb-3">
<label class="form-label">Challenge Type</label>
<select class="form-select" name="challenge_type">
<option value="http-01">HTTP-01 (port 80 reachable from the internet)</option>
<option value="dns-01">DNS-01 (required for wildcards)</option>
</select>
</div>
<div class="mb-3"><label class="form-label">DNS Provider (DNS-01 only)</label><input class="form-control" name="dns_provider" placeholder="cloudflare"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Request Certificate</button>
</div>
</form>
</div></div>
</div>
<div class="modal fade" id="uploadCertModal" tabindex="-1">
<div class="modal-dialog"><div class="modal-content">
<form method="post" action="/certificates/custom">
{{template "csrf_field" .CSRFToken}}
<div class="modal-header"><h5 class="modal-title">Upload Custom Certificate</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div>
<div class="modal-body">
<div class="mb-3"><label class="form-label">Name</label><input class="form-control" name="name" required></div>
<div class="mb-3"><label class="form-label">Domains (one per line)</label><textarea class="form-control" name="domains" rows="2"></textarea></div>
<div class="mb-3"><label class="form-label">Certificate (PEM, full chain)</label><textarea class="form-control font-monospace" name="cert_pem" rows="5" required placeholder="-----BEGIN CERTIFICATE-----"></textarea></div>
<div class="mb-3"><label class="form-label">Private Key (PEM)</label><textarea class="form-control font-monospace" name="key_pem" rows="5" required placeholder="-----BEGIN PRIVATE KEY-----"></textarea></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Upload</button>
</div>
</form>
</div></div>
</div>
{{template "confirm_delete_modal" .}}
{{template "footer" .}}{{end}}
+115
View File
@@ -0,0 +1,115 @@
{{define "dashboard"}}{{template "header" .}}
{{$d := .Data}}
<div class="row g-3 mb-4">
<div class="col-sm-6 col-lg-3">
<div class="card stat-card h-100"><div class="card-body">
<div class="text-secondary small">Active Proxy Hosts</div>
<div class="stat-value">{{$d.ActiveProxyHosts}}</div>
</div></div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card stat-card h-100"><div class="card-body">
<div class="text-secondary small">TCP / UDP Streams</div>
<div class="stat-value">{{$d.ActiveTCPStreams}} / {{$d.ActiveUDPStreams}}</div>
</div></div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card stat-card h-100"><div class="card-body">
<div class="text-secondary small">Requests / sec (1h avg)</div>
<div class="stat-value">{{printf "%.2f" $d.RequestsPerSecond}}</div>
</div></div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card stat-card h-100"><div class="card-body">
<div class="text-secondary small">Requests (24h)</div>
<div class="stat-value">{{$d.TotalRequestsToday}}</div>
</div></div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">Status Breakdown (24h)</div>
<div class="card-body">
{{if $d.StatusBreakdown}}
<table class="table table-sm mb-0">
<tbody>
{{range $d.StatusBreakdown}}
<tr><td class="{{statusClass .Status}}">{{.Status}}</td><td>{{.Count}}</td></tr>
{{end}}
</tbody>
</table>
{{else}}<p class="text-secondary mb-0">No requests logged yet.</p>{{end}}
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">Backend Health</div>
<div class="card-body">
{{if $d.BackendHealth}}
<table class="table table-sm mb-0">
<tbody>
{{range $d.BackendHealth}}
<tr>
<td>{{.ProxyHost}}</td><td>{{.Backend}}</td>
<td>{{if .Healthy}}<span class="badge text-bg-success">healthy</span>{{else}}<span class="badge text-bg-danger">unhealthy</span>{{end}}</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}<p class="text-secondary mb-0">No backends configured.</p>{{end}}
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">Top Hosts (24h)</div>
<div class="card-body">
{{if $d.TopHosts}}
<table class="table table-sm mb-0"><tbody>
{{range $d.TopHosts}}<tr><td>{{.Host}}</td><td>{{.Count}}</td></tr>{{end}}
</tbody></table>
{{else}}<p class="text-secondary mb-0">No data yet.</p>{{end}}
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">Top Clients (24h)</div>
<div class="card-body">
{{if $d.TopClients}}
<table class="table table-sm mb-0"><tbody>
{{range $d.TopClients}}<tr><td>{{.Host}}</td><td>{{.Count}}</td></tr>{{end}}
</tbody></table>
{{else}}<p class="text-secondary mb-0">No data yet.</p>{{end}}
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">Certificates Expiring Soon (30d)</div>
<div class="card-body">
{{if $d.CertsExpiringSoon}}
<table class="table table-sm mb-0"><tbody>
{{range $d.CertsExpiringSoon}}<tr><td>{{.Name}}</td><td>{{fmtTime (deref .NotAfter)}}</td></tr>{{end}}
</tbody></table>
{{else}}<p class="text-secondary mb-0">Nothing expiring soon.</p>{{end}}
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card h-100">
<div class="card-header">Recent Errors (24h)</div>
<div class="card-body">
{{if $d.RecentErrors}}
<table class="table table-sm mb-0"><tbody>
{{range $d.RecentErrors}}<tr><td>{{fmtTime .Timestamp}}</td><td class="{{statusClass .Status}}">{{.Status}}</td><td>{{.Host}}{{.Path}}</td></tr>{{end}}
</tbody></table>
{{else}}<p class="text-secondary mb-0">No recent errors.</p>{{end}}
</div>
</div>
</div>
</div>
{{template "footer" .}}{{end}}
+51
View File
@@ -0,0 +1,51 @@
{{define "logs"}}{{template "header" .}}
{{$d := .Data}}
<ul class="nav nav-pills mb-3">
<li class="nav-item"><a class="nav-link active" href="/logs">Requests</a></li>
<li class="nav-item"><a class="nav-link" href="/logs/audit">Audit</a></li>
</ul>
<form method="get" action="/logs" class="row g-2 mb-3">
<div class="col-auto">
<input type="text" class="form-control" name="q" value="{{$d.Query}}" placeholder="Search host / path / IP…">
</div>
<div class="col-auto">
<input type="text" class="form-control" name="status" value="{{if $d.Status}}{{$d.Status}}{{end}}" placeholder="Status code">
</div>
<div class="col-auto"><button class="btn btn-outline-primary">Filter</button></div>
</form>
<div class="card">
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light">
<tr><th>Time</th><th>Proxy Host</th><th>Client IP</th><th>Method</th><th>Host</th><th>Path</th><th>Status</th><th>Size</th><th>Backend</th><th>ms</th><th>User Agent</th></tr>
</thead>
<tbody>
{{range $d.Logs}}
<tr>
<td>{{fmtTime .Timestamp}}</td>
<td>{{.ProxyHostName}}</td>
<td>{{.ClientIP}}</td>
<td>{{.Method}}</td>
<td>{{.Host}}</td>
<td class="text-truncate" style="max-width:16rem">{{.Path}}</td>
<td class="{{statusClass .Status}}">{{.Status}}</td>
<td>{{.ResponseSize}}</td>
<td>{{.Backend}}</td>
<td>{{.ResponseTimeMS}}</td>
<td class="text-truncate" style="max-width:12rem">{{.UserAgent}}</td>
</tr>
{{else}}
<tr><td colspan="11" class="text-center text-secondary py-4">No requests logged yet.</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{if gt $d.TotalPages 1}}
<nav class="mt-3"><ul class="pagination pagination-sm">
{{range $i := iterate $d.TotalPages}}
<li class="page-item {{if eq (add $i 1) $d.Page}}active{{end}}"><a class="page-link" href="/logs?page={{add $i 1}}&q={{$d.Query}}">{{add $i 1}}</a></li>
{{end}}
</ul></nav>
{{end}}
{{template "footer" .}}{{end}}
+34
View File
@@ -0,0 +1,34 @@
{{define "logs_audit"}}{{template "header" .}}
{{$d := .Data}}
<ul class="nav nav-pills mb-3">
<li class="nav-item"><a class="nav-link" href="/logs">Requests</a></li>
<li class="nav-item"><a class="nav-link active" href="/logs/audit">Audit</a></li>
</ul>
<div class="card">
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead class="table-light"><tr><th>Time</th><th>Actor</th><th>Action</th><th>Target</th><th>Details</th></tr></thead>
<tbody>
{{range $d.Logs}}
<tr>
<td>{{fmtTime .Timestamp}}</td>
<td>{{.Actor}}</td>
<td><span class="badge text-bg-light border">{{.Action}}</span></td>
<td>{{.Target}}</td>
<td class="text-secondary">{{.Details}}</td>
</tr>
{{else}}
<tr><td colspan="5" class="text-center text-secondary py-4">No audit events yet.</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{if gt $d.TotalPages 1}}
<nav class="mt-3"><ul class="pagination pagination-sm">
{{range $i := iterate $d.TotalPages}}
<li class="page-item {{if eq (add $i 1) $d.Page}}active{{end}}"><a class="page-link" href="/logs/audit?page={{add $i 1}}">{{add $i 1}}</a></li>
{{end}}
</ul></nav>
{{end}}
{{template "footer" .}}{{end}}
@@ -0,0 +1,9 @@
{{define "footer"}}
</main>
</div>
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>
{{end}}
@@ -0,0 +1,44 @@
{{define "header"}}<!doctype html>
<html lang="en" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} · vibeproxy</title>
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-icons.min.css">
<link rel="stylesheet" href="/static/css/app.css">
</head>
<body>
<div class="vp-shell">
<nav class="vp-sidebar d-flex flex-column">
<a href="/dashboard" class="vp-brand d-flex align-items-center gap-2">
<i class="bi bi-hexagon-fill"></i>
<span>vibeproxy</span>
</a>
<ul class="nav flex-column vp-nav">
<li class="nav-item"><a class="nav-link {{if eq .Active "dashboard"}}active{{end}}" href="/dashboard"><i class="bi bi-speedometer2"></i> Dashboard</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "proxy_hosts"}}active{{end}}" href="/proxy-hosts"><i class="bi bi-globe2"></i> Proxy Hosts</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "streams"}}active{{end}}" href="/streams"><i class="bi bi-arrow-left-right"></i> TCP/UDP Streams</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "backends"}}active{{end}}" href="/backends"><i class="bi bi-hdd-network"></i> Backends</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "certificates"}}active{{end}}" href="/certificates"><i class="bi bi-shield-lock"></i> Certificates</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "access_lists"}}active{{end}}" href="/access-lists"><i class="bi bi-key"></i> Access Lists</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "logs"}}active{{end}}" href="/logs"><i class="bi bi-journal-text"></i> Logs</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "settings"}}active{{end}}" href="/settings"><i class="bi bi-gear"></i> Settings</a></li>
<li class="nav-item"><a class="nav-link {{if eq .Active "api"}}active{{end}}" href="/api-tokens"><i class="bi bi-code-slash"></i> API</a></li>
</ul>
<div class="vp-sidebar-footer text-secondary small">
<i class="bi bi-box-seam"></i> vibeproxy
</div>
</nav>
<div class="vp-main">
<header class="vp-topbar d-flex align-items-center justify-content-between">
<h1 class="h5 mb-0">{{.Title}}</h1>
</header>
<main class="vp-content">
{{if .Flash}}
<div class="alert alert-{{if .FlashKind}}{{.FlashKind}}{{else}}success{{end}} alert-dismissible fade show" role="alert">
{{.Flash}}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{{end}}
{{end}}
@@ -0,0 +1,19 @@
{{define "confirm_delete_modal"}}
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="bi bi-exclamation-triangle text-danger"></i> Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">Are you sure?</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" data-confirm-action data-bs-dismiss="modal">Delete</button>
</div>
</div>
</div>
</div>
{{end}}
{{define "csrf_field"}}<input type="hidden" name="csrf_token" value="{{.}}">{{end}}
+266
View File
@@ -0,0 +1,266 @@
{{define "proxy_host_form"}}{{template "header" .}}
{{$d := .Data}}{{$h := $d.Host}}
<form method="post" action="{{if $d.IsNew}}/proxy-hosts/new{{else}}/proxy-hosts/{{$h.ID}}/edit{{end}}">
{{template "csrf_field" .CSRFToken}}
<ul class="nav nav-tabs mb-3" role="tablist">
<li class="nav-item"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-details" type="button">Details</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-backends" type="button">Backends</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-ssl" type="button">SSL &amp; Access</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-headers" type="button">Headers</button></li>
<li class="nav-item"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-advanced" type="button">Advanced</button></li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="tab-details">
<div class="card mb-3"><div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Name</label>
<input class="form-control" name="name" value="{{$h.Name}}" required>
</div>
<div class="col-md-6">
<label class="form-label">Type</label>
<select class="form-select" name="type" id="hostType">
<option value="proxy" {{if eq $h.Type "proxy"}}selected{{end}}>Proxy</option>
<option value="redirect" {{if eq $h.Type "redirect"}}selected{{end}}>Redirect</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Domains</label>
<div data-chip-input>
<div class="vp-chip-list"></div>
<input type="text" class="form-control" placeholder="example.com — press Enter to add">
<textarea name="domains" class="d-none">{{join $h.Domains "\n"}}</textarea>
</div>
</div>
<div class="col-md-4">
<label class="form-label">Listen Address</label>
<input class="form-control" name="listen_addr" value="{{$h.ListenAddr}}" placeholder="0.0.0.0 (all interfaces)">
</div>
<div class="col-12" id="redirectFields" style="{{if ne $h.Type "redirect"}}display:none{{end}}">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Redirect Target URL</label>
<input class="form-control" name="redirect_target_url" value="{{$h.RedirectTargetURL}}" placeholder="https://new.example.com">
</div>
<div class="col-md-3">
<label class="form-label">Redirect Code</label>
<select class="form-select" name="redirect_code">
{{range $code := (slice 301 302 307 308)}}
<option value="{{$code}}" {{if eq $h.RedirectCode $code}}selected{{end}}>{{$code}}</option>
{{end}}
</select>
</div>
<div class="col-md-3 d-flex flex-column justify-content-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="redirect_preserve_path" id="rpp" {{if $h.RedirectPreservePath}}checked{{end}}>
<label class="form-check-label" for="rpp">Preserve path</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="redirect_preserve_qs" id="rpq" {{if $h.RedirectPreserveQS}}checked{{end}}>
<label class="form-check-label" for="rpq">Preserve query string</label>
</div>
</div>
</div>
</div>
<div class="col-12">
<label class="form-label">Comments</label>
<textarea class="form-control" name="comments" rows="2">{{$h.Comments}}</textarea>
</div>
<div class="col-12">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="enabled" id="enabled" {{if $h.Enabled}}checked{{end}}>
<label class="form-check-label" for="enabled">Enabled</label>
</div>
</div>
</div>
</div></div>
</div>
<div class="tab-pane fade" id="tab-backends">
<div class="card mb-3"><div class="card-body">
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Upstream Scheme</label>
<select class="form-select" name="forward_scheme">
<option value="http" {{if eq $h.ForwardScheme "http"}}selected{{end}}>http</option>
<option value="https" {{if eq $h.ForwardScheme "https"}}selected{{end}}>https</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Load Balancing Method</label>
<select class="form-select" name="load_balancing_method">
<option value="round_robin" {{if eq $h.LoadBalancingMethod "round_robin"}}selected{{end}}>Round robin</option>
<option value="least_conn" {{if eq $h.LoadBalancingMethod "least_conn"}}selected{{end}}>Least connections</option>
<option value="weighted" {{if eq $h.LoadBalancingMethod "weighted"}}selected{{end}}>Weighted</option>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="websocket_support" id="ws" {{if $h.WebsocketSupport}}checked{{end}}>
<label class="form-check-label" for="ws">WebSocket support</label>
</div>
</div>
</div>
<table class="table table-sm align-middle" id="backendsTable">
<thead><tr><th>Host</th><th>Port</th><th>Weight</th><th>On</th><th>Health Check</th><th>Path</th><th>Status</th><th>Interval(s)</th><th>Fail</th><th>Recover</th><th></th></tr></thead>
<tbody>
{{range $i, $b := $h.Backends}}
<tr>
<td><input class="form-control form-control-sm" name="backend_host" value="{{$b.Host}}"></td>
<td><input class="form-control form-control-sm" style="width:5.5rem" name="backend_port" value="{{$b.Port}}"></td>
<td><input class="form-control form-control-sm" style="width:4.5rem" name="backend_weight" value="{{$b.Weight}}"></td>
<td>
<input type="checkbox" class="form-check-input" {{if $b.Enabled}}checked{{end}} onchange="this.nextElementSibling.value=this.checked">
<input type="hidden" name="backend_enabled" value="{{if $b.Enabled}}true{{else}}false{{end}}">
</td>
<td>
<select class="form-select form-select-sm" name="backend_hc_type">
<option value="tcp" {{if eq $b.HealthCheckType "tcp"}}selected{{end}}>TCP</option>
<option value="http" {{if eq $b.HealthCheckType "http"}}selected{{end}}>HTTP</option>
<option value="none" {{if eq $b.HealthCheckType "none"}}selected{{end}}>None</option>
</select>
</td>
<td><input class="form-control form-control-sm" style="width:6rem" name="backend_hc_path" value="{{$b.HealthCheckPath}}"></td>
<td><input class="form-control form-control-sm" style="width:4.5rem" name="backend_hc_status" value="{{$b.HealthCheckStatus}}"></td>
<td><input class="form-control form-control-sm" style="width:4.5rem" name="backend_hc_interval" value="{{$b.IntervalSeconds}}"></td>
<td><input class="form-control form-control-sm" style="width:4rem" name="backend_hc_fail" value="{{$b.FailureThreshold}}"></td>
<td><input class="form-control form-control-sm" style="width:4rem" name="backend_hc_recover" value="{{$b.RecoveryThreshold}}"></td>
<td><button type="button" class="btn btn-sm btn-outline-danger" data-remove-row><i class="bi bi-x"></i></button></td>
</tr>
{{end}}
</tbody>
</table>
<button type="button" class="btn btn-sm btn-outline-primary" data-add-row="backendsTable"><i class="bi bi-plus-lg"></i> Add Backend</button>
<template id="backendsTableRowTemplate"><tr>
<td><input class="form-control form-control-sm" name="backend_host"></td>
<td><input class="form-control form-control-sm" style="width:5.5rem" name="backend_port" value="80"></td>
<td><input class="form-control form-control-sm" style="width:4.5rem" name="backend_weight" value="1"></td>
<td>
<input type="checkbox" class="form-check-input" checked onchange="this.nextElementSibling.value=this.checked">
<input type="hidden" name="backend_enabled" value="true">
</td>
<td><select class="form-select form-select-sm" name="backend_hc_type">
<option value="tcp" selected>TCP</option><option value="http">HTTP</option><option value="none">None</option>
</select></td>
<td><input class="form-control form-control-sm" style="width:6rem" name="backend_hc_path" value="/"></td>
<td><input class="form-control form-control-sm" style="width:4.5rem" name="backend_hc_status" value="200"></td>
<td><input class="form-control form-control-sm" style="width:4.5rem" name="backend_hc_interval" value="10"></td>
<td><input class="form-control form-control-sm" style="width:4rem" name="backend_hc_fail" value="3"></td>
<td><input class="form-control form-control-sm" style="width:4rem" name="backend_hc_recover" value="2"></td>
<td><button type="button" class="btn btn-sm btn-outline-danger" data-remove-row><i class="bi bi-x"></i></button></td>
</tr></template>
</div></div>
</div>
<div class="tab-pane fade" id="tab-ssl">
<div class="card mb-3"><div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">SSL Certificate</label>
<select class="form-select" name="certificate_id">
<option value="0">None</option>
{{range $d.Certificates}}<option value="{{.ID}}" {{if and $h.CertificateID (eq (deref64 $h.CertificateID) .ID)}}selected{{end}}>{{.Name}}</option>{{end}}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Access List</label>
<select class="form-select" name="access_list_id">
<option value="0">None</option>
{{range $d.AccessLists}}<option value="{{.ID}}" {{if and $h.AccessListID (eq (deref64 $h.AccessListID) .ID)}}selected{{end}}>{{.Name}}</option>{{end}}
</select>
</div>
<div class="col-md-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="force_https" id="fh" {{if $h.ForceHTTPS}}checked{{end}}>
<label class="form-check-label" for="fh">Force HTTPS</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="hsts_enabled" id="hsts" {{if $h.HSTSEnabled}}checked{{end}}>
<label class="form-check-label" for="hsts">HSTS</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="preserve_host_header" id="phh" {{if $h.PreserveHostHeader}}checked{{end}}>
<label class="form-check-label" for="phh">Preserve Host header</label>
</div>
</div>
</div>
</div></div>
</div>
<div class="tab-pane fade" id="tab-headers">
<div class="card mb-3"><div class="card-body">
<table class="table table-sm align-middle" id="headersTable">
<thead><tr><th>Direction</th><th>Action</th><th>Name</th><th>Value</th><th></th></tr></thead>
<tbody>
{{range $h.CustomHeaders}}
<tr>
<td><select class="form-select form-select-sm" name="header_direction">
<option value="request" {{if eq .Direction "request"}}selected{{end}}>Request</option>
<option value="response" {{if eq .Direction "response"}}selected{{end}}>Response</option>
</select></td>
<td><select class="form-select form-select-sm" name="header_action">
<option value="set" {{if eq .Action "set"}}selected{{end}}>Set</option>
<option value="remove" {{if eq .Action "remove"}}selected{{end}}>Remove</option>
</select></td>
<td><input class="form-control form-control-sm" name="header_name" value="{{.Name}}" placeholder="X-Frame-Options"></td>
<td><input class="form-control form-control-sm" name="header_value" value="{{.Value}}"></td>
<td><button type="button" class="btn btn-sm btn-outline-danger" data-remove-row><i class="bi bi-x"></i></button></td>
</tr>
{{end}}
</tbody>
</table>
<button type="button" class="btn btn-sm btn-outline-primary" data-add-row="headersTable"><i class="bi bi-plus-lg"></i> Add Header</button>
<template id="headersTableRowTemplate"><tr>
<td><select class="form-select form-select-sm" name="header_direction">
<option value="request" selected>Request</option><option value="response">Response</option>
</select></td>
<td><select class="form-select form-select-sm" name="header_action">
<option value="set" selected>Set</option><option value="remove">Remove</option>
</select></td>
<td><input class="form-control form-control-sm" name="header_name" placeholder="X-Frame-Options"></td>
<td><input class="form-control form-control-sm" name="header_value"></td>
<td><button type="button" class="btn btn-sm btn-outline-danger" data-remove-row><i class="bi bi-x"></i></button></td>
</tr></template>
</div></div>
</div>
<div class="tab-pane fade" id="tab-advanced">
<div class="card mb-3"><div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Connect timeout (ms)</label>
<input class="form-control" name="connect_timeout_ms" value="{{$h.ConnectTimeoutMS}}">
</div>
<div class="col-md-3">
<label class="form-label">Read timeout (ms)</label>
<input class="form-control" name="read_timeout_ms" value="{{$h.ReadTimeoutMS}}">
</div>
<div class="col-md-3">
<label class="form-label">Write timeout (ms)</label>
<input class="form-control" name="write_timeout_ms" value="{{$h.WriteTimeoutMS}}">
</div>
<div class="col-md-3">
<label class="form-label">Idle timeout (ms)</label>
<input class="form-control" name="idle_timeout_ms" value="{{$h.IdleTimeoutMS}}">
</div>
</div>
</div></div>
</div>
</div>
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Save</button>
<a href="/proxy-hosts" class="btn btn-outline-secondary">Cancel</a>
</form>
<script>
document.getElementById('hostType').addEventListener('change', function () {
document.getElementById('redirectFields').style.display = this.value === 'redirect' ? '' : 'none';
});
</script>
{{template "footer" .}}{{end}}
+45
View File
@@ -0,0 +1,45 @@
{{define "proxy_hosts"}}{{template "header" .}}
{{$d := .Data}}
<div class="d-flex justify-content-between align-items-center mb-3">
<input type="text" class="form-control w-auto" placeholder="Search proxy hosts…" data-search-target="hostsTable">
<a href="/proxy-hosts/new" class="btn btn-primary"><i class="bi bi-plus-lg"></i> Add Proxy Host</a>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="hostsTable">
<thead class="table-light">
<tr><th>Name</th><th>Domains</th><th>Type</th><th>Destination</th><th>SSL</th><th>Status</th><th></th></tr>
</thead>
<tbody>
{{range $d.Hosts}}
<tr>
<td><a href="/proxy-hosts/{{.ID}}/edit">{{.Name}}</a>{{if .Comments}}<div class="small text-secondary">{{.Comments}}</div>{{end}}</td>
<td>{{join .Domains ", "}}</td>
<td><span class="badge text-bg-{{if eq .Type "redirect"}}warning{{else}}info{{end}}">{{.Type}}</span></td>
<td>
{{if eq .Type "redirect"}}{{.RedirectTargetURL}}
{{else}}{{range .Backends}}{{.Host}}:{{.Port}} {{end}}{{end}}
</td>
<td>{{if .CertificateID}}<i class="bi bi-shield-check text-success"></i>{{else}}<i class="bi bi-shield-slash text-secondary"></i>{{end}}</td>
<td>{{if .Enabled}}<span class="badge text-bg-success">enabled</span>{{else}}<span class="badge text-bg-secondary">disabled</span>{{end}}</td>
<td class="text-end">
<form method="post" action="/proxy-hosts/{{.ID}}/toggle" class="d-inline">
{{template "csrf_field" $.CSRFToken}}
<button class="btn btn-sm btn-outline-secondary" title="Enable/disable"><i class="bi bi-power"></i></button>
</form>
<a href="/proxy-hosts/{{.ID}}/edit" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a>
<form method="post" action="/proxy-hosts/{{.ID}}/delete" id="del-host-{{.ID}}" class="d-inline">
{{template "csrf_field" $.CSRFToken}}
</form>
<button type="button" class="btn btn-sm btn-outline-danger" data-confirm-delete-form="del-host-{{.ID}}" data-confirm-message="Delete proxy host &quot;{{.Name}}&quot;?"><i class="bi bi-trash"></i></button>
</td>
</tr>
{{else}}
<tr><td colspan="7" class="text-center text-secondary py-4">No proxy hosts yet.</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{template "confirm_delete_modal" .}}
{{template "footer" .}}{{end}}
+91
View File
@@ -0,0 +1,91 @@
{{define "settings"}}{{template "header" .}}
{{$s := .Data}}
<div class="row g-3">
<div class="col-lg-7">
<div class="card mb-3">
<div class="card-header">Let's Encrypt / ACME</div>
<div class="card-body">
<form method="post" action="/settings">
{{template "csrf_field" .CSRFToken}}
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Contact Email</label>
<input class="form-control" name="acme_email" value="{{$s.ACMEEmail}}">
</div>
<div class="col-md-6">
<label class="form-label">Renewal Window (days before expiry)</label>
<input class="form-control" name="acme_renewal_window_days" value="{{$s.ACMERenewalWindowDays}}">
</div>
<div class="col-md-6">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="acme_staging" id="staging" {{if $s.ACMEStaging}}checked{{end}}>
<label class="form-check-label" for="staging">Use Let's Encrypt staging environment</label>
</div>
</div>
<div class="col-md-12">
<label class="form-label">Certificate Storage Path</label>
<input class="form-control" name="cert_storage_path" value="{{$s.CertStoragePath}}">
</div>
<div class="col-md-12">
<label class="form-label">Trusted Proxy Networks (CIDR, one per line — only these may set X-Forwarded-For)</label>
<textarea class="form-control" name="trusted_proxy_cidrs" rows="3">{{join $s.TrustedProxyCIDRs "\n"}}</textarea>
</div>
<div class="col-md-6">
<label class="form-label">Log Retention (days)</label>
<input class="form-control" name="log_retention_days" value="{{$s.LogRetentionDays}}">
</div>
</div>
<hr>
<h6>Backups</h6>
<div class="row g-3">
<div class="col-md-4">
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" name="backup_enabled" id="backupEn" {{if $s.BackupEnabled}}checked{{end}}>
<label class="form-check-label" for="backupEn">Automatic backups enabled</label>
</div>
</div>
<div class="col-md-4">
<label class="form-label">Interval (hours)</label>
<input class="form-control" name="backup_interval_hours" value="{{$s.BackupIntervalHours}}">
</div>
<div class="col-md-4">
<label class="form-label">Keep last N backups</label>
<input class="form-control" name="backup_retention_count" value="{{$s.BackupRetentionCount}}">
</div>
<div class="col-12">
<label class="form-label">Backup Directory</label>
<input class="form-control" name="backup_path" value="{{$s.BackupPath}}">
</div>
</div>
<div class="mt-4"><button class="btn btn-primary"><i class="bi bi-check-lg"></i> Save Settings</button></div>
</form>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Manual Backup</div>
<div class="card-body">
<form method="post" action="/settings/backup">
{{template "csrf_field" .CSRFToken}}
<button class="btn btn-outline-primary"><i class="bi bi-download"></i> Backup Now</button>
</form>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card mb-3">
<div class="card-header">Administrator Account</div>
<div class="card-body">
<form method="post" action="/settings/password">
{{template "csrf_field" .CSRFToken}}
<div class="mb-3"><label class="form-label">Current Password</label><input type="password" class="form-control" name="current_password" required></div>
<div class="mb-3"><label class="form-label">New Password</label><input type="password" class="form-control" name="new_password" minlength="8" required></div>
<div class="mb-3"><label class="form-label">Confirm New Password</label><input type="password" class="form-control" name="confirm_password" minlength="8" required></div>
<button class="btn btn-primary"><i class="bi bi-key"></i> Change Password</button>
</form>
</div>
</div>
</div>
</div>
{{template "footer" .}}{{end}}
+61
View File
@@ -0,0 +1,61 @@
{{define "stream_form"}}{{template "header" .}}
{{$d := .Data}}{{$s := $d.Stream}}
<div class="card"><div class="card-body">
<form method="post" action="{{if $d.IsNew}}/streams/new{{else}}/streams/{{$s.ID}}/edit{{end}}">
{{template "csrf_field" .CSRFToken}}
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Protocol</label>
<select class="form-select" name="protocol">
<option value="tcp" {{if eq $s.Protocol "tcp"}}selected{{end}}>TCP</option>
<option value="udp" {{if eq $s.Protocol "udp"}}selected{{end}}>UDP</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Listen IP</label>
<input class="form-control" name="listen_ip" value="{{$s.ListenIP}}" placeholder="0.0.0.0 (all interfaces)">
</div>
<div class="col-md-2">
<label class="form-label">Listen Port</label>
<input class="form-control" name="listen_port" value="{{$s.ListenPort}}" required>
</div>
<div class="col-md-6">
<label class="form-label">Destination Host</label>
<input class="form-control" name="destination_host" value="{{$s.DestinationHost}}" required>
</div>
<div class="col-md-3">
<label class="form-label">Destination Port</label>
<input class="form-control" name="destination_port" value="{{$s.DestinationPort}}" required>
</div>
<div class="col-md-3">
<label class="form-label">Connect Timeout (ms)</label>
<input class="form-control" name="connect_timeout_ms" value="{{$s.ConnectTimeoutMS}}">
</div>
<div class="col-md-3">
<label class="form-label">Idle Timeout (ms)</label>
<input class="form-control" name="idle_timeout_ms" value="{{$s.IdleTimeoutMS}}">
</div>
<div class="col-md-9">
<label class="form-label">Comments</label>
<input class="form-control" name="comments" value="{{$s.Comments}}">
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="proxy_protocol" id="pp" {{if $s.ProxyProtocol}}checked{{end}}>
<label class="form-check-label" for="pp">Send PROXY protocol header to backend</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="enabled" id="en" {{if $s.Enabled}}checked{{end}}>
<label class="form-check-label" for="en">Enabled</label>
</div>
</div>
</div>
<div class="mt-4">
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> Save</button>
<a href="/streams" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div></div>
{{template "footer" .}}{{end}}
+40
View File
@@ -0,0 +1,40 @@
{{define "streams"}}{{template "header" .}}
<div class="d-flex justify-content-between align-items-center mb-3">
<input type="text" class="form-control w-auto" placeholder="Search streams…" data-search-target="streamsTable">
<a href="/streams/new" class="btn btn-primary"><i class="bi bi-plus-lg"></i> Add Stream</a>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0" id="streamsTable">
<thead class="table-light">
<tr><th>Protocol</th><th>Listen</th><th>Destination</th><th>PROXY protocol</th><th>Status</th><th></th></tr>
</thead>
<tbody>
{{range .Data}}
<tr>
<td><span class="badge text-bg-{{if eq .Protocol "tcp"}}primary{{else}}purple bg-secondary{{end}}">{{.Protocol}}</span></td>
<td>{{.ListenIP}}:{{.ListenPort}}</td>
<td>{{.DestinationHost}}:{{.DestinationPort}}</td>
<td>{{if .ProxyProtocol}}<i class="bi bi-check-lg text-success"></i>{{else}}—{{end}}</td>
<td>{{if .Enabled}}<span class="badge text-bg-success">enabled</span>{{else}}<span class="badge text-bg-secondary">disabled</span>{{end}}</td>
<td class="text-end">
<form method="post" action="/streams/{{.ID}}/toggle" class="d-inline">
{{template "csrf_field" $.CSRFToken}}
<button class="btn btn-sm btn-outline-secondary"><i class="bi bi-power"></i></button>
</form>
<a href="/streams/{{.ID}}/edit" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i></a>
<form method="post" action="/streams/{{.ID}}/delete" id="del-stream-{{.ID}}" class="d-inline">
{{template "csrf_field" $.CSRFToken}}
</form>
<button type="button" class="btn btn-sm btn-outline-danger" data-confirm-delete-form="del-stream-{{.ID}}" data-confirm-message="Delete this stream?"><i class="bi bi-trash"></i></button>
</td>
</tr>
{{else}}
<tr><td colspan="6" class="text-center text-secondary py-4">No streams yet.</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
{{template "confirm_delete_modal" .}}
{{template "footer" .}}{{end}}