Initial Awawawa

This commit is contained in:
mueller_minki
2026-10-04 09:08:27 +02:00
commit 8e83a98ef4
76 changed files with 8225 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{% block title %}PatchBay{% endblock %} - PatchBay</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body class="{% block bodyclass %}{% endblock %}">
{% if user %}
<header class="topbar">
<span class="brand">PatchBay</span>
<nav>
{% set ep = request.endpoint %}
<a href="{{ url_for('views.patch') }}" class="{{ 'active' if ep == 'views.patch' }}">Patch</a>
<a href="{{ url_for('views.services') }}" class="{{ 'active' if ep == 'views.services' }}">Services</a>
<a href="{{ url_for('views.stats') }}" class="{{ 'active' if ep == 'views.stats' }}">Stats</a>
<a href="{{ url_for('views.settings') }}" class="{{ 'active' if ep == 'views.settings' }}">Settings</a>
</nav>
{% block toolbar %}{% endblock %}
<form method="post" action="{{ url_for('auth.logout') }}" class="logout">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<span class="who">{{ user.username }}{% if user.sysop %} (sysop){% endif %}</span>
<button type="submit" class="btn small">Log out</button>
</form>
</header>
{% endif %}
{% with msgs = get_flashed_messages(with_categories=true) %}
{% if msgs %}
<div class="flashes">
{% for cat, msg in msgs %}<div class="flash {{ cat }}">{{ msg }}</div>{% endfor %}
</div>
{% endif %}
{% endwith %}
<main class="{% block mainclass %}page{% endblock %}">
{% block content %}{% endblock %}
</main>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}Log in{% endblock %}
{% block bodyclass %}auth{% endblock %}
{% block content %}
<form method="post" class="card auth-card">
<h1>PatchBay</h1>
<p class="muted">Log in to manage this gateway.</p>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label>Username <input name="username" autocomplete="username" required autofocus></label>
<label>Password <input name="password" type="password" autocomplete="current-password" required></label>
<button type="submit" class="btn primary">Continue</button>
<p class="muted small">A one-time code will be sent to your email address.</p>
</form>
{% endblock %}

View File

@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}Patch{% endblock %}
{% block mainclass %}editor-main{% endblock %}
{% block toolbar %}
<div class="tools">
<div class="dropdown" id="add-menu">
<button type="button" class="btn" id="add-btn" aria-haspopup="true" aria-expanded="false">Add &#9662;</button>
<div class="menu" role="menu">
<button type="button" role="menuitem" data-add="client_source">Add Client Source</button>
<button type="button" role="menuitem" data-add="client_sink">Add Client Sink</button>
<button type="button" role="menuitem" data-add="public_sink">Add Public Sink</button>
<button type="button" role="menuitem" data-add="tunnel_source">Add Tunnel Source</button>
<button type="button" role="menuitem" data-add="tunnel_sink">Add Tunnel Sink</button>
<button type="button" role="menuitem" data-add="splitter">Add Splitter</button>
</div>
</div>
<button type="button" class="btn" id="fit-btn" title="Fit all nodes into view">Fit</button>
<span id="save-state" class="save-state"></span>
</div>
{% endblock %}
{% block content %}
<div id="editor" tabindex="0">
<svg id="wires" aria-hidden="true"><g id="wire-layer"></g></svg>
<div id="canvas"></div>
<div class="editor-hint">Drag from an output socket to an input socket to patch. Drag empty space to pan, scroll to zoom. Click a wire and use its x button (or double-click it) to remove it; drag a wire off an input to reconnect it. Selected nodes are removed with Delete.</div>
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='common.js') }}"></script>
<script src="{{ url_for('static', filename='patch.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block title %}Services{% endblock %}
{% block content %}
<div class="page-head">
<h1>Services</h1>
<button type="button" class="btn" id="refresh-services">Refresh now</button>
</div>
{% if not daemon_ok %}<div class="flash error">patchbayd is not reachable; showing the last reported data.</div>{% endif %}
<p class="muted">Listening TCP ports and bound UDP ports on every host, with the process that owns them. Clients report every few seconds while connected.</p>
{% for h in hosts %}
<section class="card">
<h2><span class="dot {{ 'on' if h.online else 'off' }}" title="{{ 'online' if h.online else 'offline' }}"></span>{{ h.name }}</h2>
{% if h.ifaces %}
<p class="small">Tunnel interfaces:
{% for i in h.ifaces %}<span class="mono">{{ i.name }}</span>{% if i.addr != '-' %} <span class="muted mono">{{ i.addr }}</span>{% endif %}{{ ', ' if not loop.last }}{% endfor %}
</p>
{% endif %}
{% if h.services %}
<table class="grid">
<thead><tr><th>Proto</th><th>Address</th><th class="num">Port</th><th>Process</th><th class="num">PID</th></tr></thead>
<tbody>
{% for s in h.services %}
<tr><td>{{ s.proto | upper }}</td><td class="mono">{{ s.addr }}</td><td class="num mono">{{ s.port }}</td>
<td>{{ s.process }}</td><td class="num mono">{{ s.pid or '-' }}</td></tr>
{% endfor %}
</tbody>
</table>
<p class="muted small" data-ts="{{ h.updated }}">Updated <span class="ts"></span></p>
{% else %}
<p class="muted">No data reported yet.</p>
{% endif %}
</section>
{% endfor %}
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='common.js') }}"></script>
<script src="{{ url_for('static', filename='services.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,122 @@
{% extends "base.html" %}
{% block title %}Settings{% endblock %}
{% macro csrf() %}<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">{% endmacro %}
{% block content %}
<h1>Settings</h1>
<section class="card" id="account">
<h2>My account</h2>
<form method="post" action="{{ url_for('views.settings_account') }}" class="form-grid">
{{ csrf() }}<input type="hidden" name="anchor" value="#account">
<label>Email <input name="email" type="email" value="{{ user.email }}" required></label>
<label>Current password <input name="current" type="password" autocomplete="current-password" required></label>
<label>New password <input name="new" type="password" autocomplete="new-password" minlength="10" placeholder="leave empty to keep"></label>
<label>Confirm new password <input name="confirm" type="password" autocomplete="new-password"></label>
<div class="actions"><button class="btn primary">Save</button></div>
</form>
</section>
<section class="card" id="clients">
<h2>Clients</h2>
<p class="muted">A machine can only connect once its public key is listed here. On the client, run <code>patchbayd --pubkey</code> and paste the output below.</p>
{% if clients %}
<table class="grid">
<thead><tr><th>Name</th><th>Host</th><th>Key</th><th>Last seen</th><th>Added by</th><th></th></tr></thead>
<tbody>
{% for c in clients %}
<tr>
<td>{{ c.name }}</td>
<td>{{ c.hostname or '-' }}{% if c.last_addr %} <span class="muted mono">{{ c.last_addr }}</span>{% endif %}</td>
<td class="mono key" title="{{ c.pubkey }}">{{ c.pubkey.split(' ')[0] }} ...{{ c.pubkey[-12:] }}</td>
<td data-ts="{{ c.last_seen }}"><span class="ts"></span></td>
<td>{{ c.added_by }}</td>
<td class="right">
<form method="post" action="{{ url_for('views.clients_delete', cid=c.id) }}" data-confirm="Remove client {{ c.name }}? It will be disconnected and its nodes lose their client.">
{{ csrf() }}<input type="hidden" name="anchor" value="#clients">
<button class="btn small danger">Remove</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<form method="post" action="{{ url_for('views.clients_add') }}" class="form-grid">
{{ csrf() }}<input type="hidden" name="anchor" value="#clients">
<label>Name <input name="name" required pattern="[A-Za-z0-9._\-]{1,64}" placeholder="e.g. homeserver"></label>
<label class="wide">Public key <textarea name="pubkey" rows="2" required placeholder="ssh-ed25519 AAAA... root@host"></textarea></label>
<div class="actions"><button class="btn primary">Add client</button></div>
</form>
</section>
{% if user.sysop %}
<section class="card" id="users">
<h2>Users</h2>
<p class="muted">All users can edit the patch and manage clients. Only the sysop ({{ conf.get('sysopuser') }}, defined in patchbay.conf) manages users and server settings.</p>
{% for u in users %}
<div class="user-row">
<form method="post" action="{{ url_for('views.users_edit', uid=u.id) }}" class="inline-form">
{{ csrf() }}<input type="hidden" name="anchor" value="#users">
<strong>{{ u.username }}</strong>
<input name="email" type="email" value="{{ u.email }}" required aria-label="Email">
<input name="password" type="password" minlength="10" placeholder="new password" aria-label="New password" autocomplete="new-password">
<button class="btn small">Save</button>
</form>
<form method="post" action="{{ url_for('views.users_delete', uid=u.id) }}" data-confirm="Delete user {{ u.username }}?">
{{ csrf() }}<input type="hidden" name="anchor" value="#users">
<button class="btn small danger">Delete</button>
</form>
</div>
{% else %}
<p class="muted">No other users yet.</p>
{% endfor %}
<h3>New user</h3>
<form method="post" action="{{ url_for('views.users_add') }}" class="form-grid">
{{ csrf() }}<input type="hidden" name="anchor" value="#users">
<label>Username <input name="username" required pattern="[A-Za-z0-9._\-]{1,64}"></label>
<label>Email <input name="email" type="email" required></label>
<label>Password <input name="password" type="password" minlength="10" required autocomplete="new-password"></label>
<div class="actions"><button class="btn primary">Create user</button></div>
</form>
</section>
<section class="card" id="mail">
<h2>Mail</h2>
<p class="muted">Used to send login codes. Saved to patchbay.conf.</p>
<form method="post" action="{{ url_for('views.settings_mail') }}" class="form-grid">
{{ csrf() }}<input type="hidden" name="anchor" value="#mail">
<label>SMTP server <input name="host" value="{{ conf.get('mailhost') }}" placeholder="smtp.example.org"></label>
<label>Port <input name="port" type="number" min="1" max="65535" value="{{ conf.get('mailport') }}"></label>
<label>Security
<select name="security">
{% for v, l in [('ssl', 'SSL/TLS'), ('starttls', 'STARTTLS'), ('none', 'None')] %}
<option value="{{ v }}" {{ 'selected' if conf.get('mailsecurity') == v }}>{{ l }}</option>
{% endfor %}
</select>
</label>
<label>Username <input name="user" value="{{ conf.get('mailuser') }}" autocomplete="off"></label>
<label>Password <input name="password" type="password" placeholder="{{ 'unchanged' if conf.get('mailpassword') else '' }}" autocomplete="new-password"></label>
<label>From address <input name="from" value="{{ conf.get('mailfrom') }}"></label>
<div class="actions"><button class="btn primary">Save</button></div>
</form>
<form method="post" action="{{ url_for('views.settings_mail_test') }}">
{{ csrf() }}<input type="hidden" name="anchor" value="#mail">
<button class="btn">Send test mail to {{ user.email }}</button>
</form>
</section>
<section class="card" id="retention">
<h2>Statistics retention</h2>
<form method="post" action="{{ url_for('views.settings_retention') }}" class="form-grid">
{{ csrf() }}<input type="hidden" name="anchor" value="#retention">
<label>Per-minute data (hours) <input name="stats_minute_hours" type="number" min="1" value="{{ retention.stats_minute_hours }}"></label>
<label>Hourly data (days) <input name="stats_hour_days" type="number" min="1" value="{{ retention.stats_hour_days }}"></label>
<label>Daily data (days, 0 = forever) <input name="stats_day_days" type="number" min="0" value="{{ retention.stats_day_days }}"></label>
<div class="actions"><button class="btn primary">Save</button></div>
</form>
</section>
{% endif %}
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='common.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,61 @@
{% extends "base.html" %}
{% block title %}Stats{% endblock %}
{% block content %}
<h1>Stats</h1>
{% if not sinks %}
<p class="muted">No connections yet. Patch a source into a sink on the Patch page first.</p>
{% else %}
<div class="filter-row">
<label>Connection
<select id="sink-select">
{% for s in sinks %}
<option value="{{ s.id }}">#{{ s.id }} {% if s.label %}{{ s.label }} - {% endif %}{% if s.type == 'public_sink' %}public {{ s.host or '0.0.0.0' }}{% elif s.type == 'tunnel_sink' %}{{ s.client or 'target' }} {{ s.iface or '?' }}{% else %}{{ s.client or '?' }} {{ s.host or '127.0.0.1' }}{% endif %}:{{ s.port }}/{{ s.proto }}</option>
{% endfor %}
</select>
</label>
<div class="segmented" role="group" aria-label="Time range" id="range-group">
{% for r in ['1h', '24h', '7d', '30d', '1y', 'all'] %}
<button type="button" data-range="{{ r }}" class="{{ 'selected' if r == '24h' }}">{{ r }}</button>
{% endfor %}
</div>
</div>
<div class="tiles">
<div class="tile"><div class="tile-value" id="t-in">-</div><div class="tile-label">In now (towards source)</div></div>
<div class="tile"><div class="tile-value" id="t-out">-</div><div class="tile-label">Out now (back to peer)</div></div>
<div class="tile"><div class="tile-value" id="t-active">-</div><div class="tile-label">Open connections</div></div>
<div class="tile"><div class="tile-value" id="t-total">-</div><div class="tile-label">Transferred in selected range</div></div>
</div>
<section class="card chart-card" id="chart-rate">
<div class="chart-head">
<h2>Throughput</h2>
<div class="legend">
<span class="key"><span class="swatch-line s1"></span>In</span>
<span class="key"><span class="swatch-line s2"></span>Out</span>
</div>
</div>
<div class="chart" data-kind="rate"></div>
</section>
<section class="card chart-card" id="chart-conns">
<div class="chart-head"><h2>New connections</h2></div>
<div class="chart" data-kind="conns"></div>
</section>
<details class="card">
<summary>Table view</summary>
<div class="table-wrap">
<table class="grid" id="stats-table">
<thead><tr><th>Time</th><th class="num">In</th><th class="num">Out</th><th class="num">Connections</th></tr></thead>
<tbody></tbody>
</table>
</div>
</details>
<div id="chart-tooltip" class="tooltip" hidden></div>
{% endif %}
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='common.js') }}"></script>
<script src="{{ url_for('static', filename='stats.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}Login code{% endblock %}
{% block bodyclass %}auth{% endblock %}
{% block content %}
<form method="post" class="card auth-card">
<h1>Check your email</h1>
<p class="muted">Enter the 6-digit code we sent you. It is valid for 10 minutes.</p>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<label>Code <input name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6"
autocomplete="one-time-code" required autofocus class="code"></label>
<button type="submit" class="btn primary">Log in</button>
<p class="small"><a href="{{ url_for('auth.login') }}">Start over</a></p>
</form>
{% endblock %}