feat: add Jinja2 admin UI templates
This commit is contained in:
+41
-1
@@ -1 +1,41 @@
|
|||||||
<!DOCTYPE html><html><body>{% block content %}{% endblock %}</body></html>
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Mihomo Expander</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: system-ui, sans-serif; margin: 0; background: #f5f5f5; color: #222; }
|
||||||
|
header { background: #1a1a2e; color: #fff; padding: 12px 24px; display: flex; align-items: center; gap: 16px; }
|
||||||
|
header a { color: #aad4f5; text-decoration: none; font-weight: bold; }
|
||||||
|
main { max-width: 960px; margin: 24px auto; padding: 0 16px; }
|
||||||
|
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.1); }
|
||||||
|
th, td { padding: 10px 14px; text-align: left; border-bottom: 1px solid #eee; }
|
||||||
|
th { background: #f0f0f0; font-size: .85rem; text-transform: uppercase; letter-spacing: .05em; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
.btn { display: inline-block; padding: 7px 14px; border-radius: 4px; border: none; cursor: pointer; font-size: .9rem; text-decoration: none; }
|
||||||
|
.btn-primary { background: #2563eb; color: #fff; }
|
||||||
|
.btn-danger { background: #dc2626; color: #fff; }
|
||||||
|
.btn-sm { padding: 4px 10px; font-size: .8rem; }
|
||||||
|
form.inline { display: inline; }
|
||||||
|
.card { background: #fff; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,.1); padding: 20px; margin-bottom: 20px; }
|
||||||
|
label { display: block; margin-bottom: 4px; font-weight: 500; font-size: .9rem; }
|
||||||
|
input[type=text], input[type=url], textarea { width: 100%; padding: 8px 10px; border: 1px solid #ccc; border-radius: 4px; font-size: .95rem; }
|
||||||
|
textarea { font-family: monospace; font-size: .85rem; min-height: 200px; }
|
||||||
|
.token-url { font-family: monospace; font-size: .85rem; background: #f0f4ff; padding: 6px 10px; border-radius: 4px; word-break: break-all; }
|
||||||
|
.success { color: #16a34a; }
|
||||||
|
.error { color: #dc2626; }
|
||||||
|
h1 { margin-top: 0; }
|
||||||
|
h2 { margin-top: 0; font-size: 1.1rem; color: #444; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<a href="/">Mihomo Expander</a>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -1 +1,65 @@
|
|||||||
{% extends "base.html" %}{% block content %}detail{% endblock %}
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
||||||
|
<h1>{{ config.name }}</h1>
|
||||||
|
<div>
|
||||||
|
<form class="inline" method="post" action="/configs/{{ config.id }}/refresh">
|
||||||
|
<button type="submit" class="btn btn-primary">↻ Force Refresh</button>
|
||||||
|
</form>
|
||||||
|
<form class="inline" method="post" action="/configs/{{ config.id }}/delete"
|
||||||
|
onsubmit="return confirm('Delete this config?')">
|
||||||
|
<button type="submit" class="btn btn-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Client URL</h2>
|
||||||
|
<div class="token-url">{{ request.url.scheme }}://{{ request.url.netloc }}/config/{{ config.token }}.yaml</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Base YAML</h2>
|
||||||
|
<form method="post" action="/configs/{{ config.id }}">
|
||||||
|
<textarea name="base_yaml" style="min-height:300px;">{{ config.base_yaml }}</textarea>
|
||||||
|
<div style="margin-top:10px;">
|
||||||
|
<button type="submit" class="btn btn-primary">Save YAML</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px;">
|
||||||
|
<h2 style="margin:0;">Subscriptions</h2>
|
||||||
|
<a href="/configs/{{ config.id }}/subscriptions/new" class="btn btn-primary btn-sm">+ Add</a>
|
||||||
|
</div>
|
||||||
|
{% if subscriptions %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr><th>Name</th><th>URL</th><th>Last Fetched</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for sub in subscriptions %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ sub.name }}</td>
|
||||||
|
<td style="font-family:monospace;font-size:.8rem;">{{ sub.url }}</td>
|
||||||
|
<td>{{ sub.last_fetched_at.strftime('%Y-%m-%d %H:%M') if sub.last_fetched_at else '—' }}</td>
|
||||||
|
<td>
|
||||||
|
<form class="inline" method="post" action="/subscriptions/{{ sub.id }}/delete">
|
||||||
|
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p style="color:#666;">No subscriptions. Add one above.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:8px;">
|
||||||
|
<a href="/configs/{{ config.id }}/logs" class="btn" style="background:#6b7280;color:#fff;">View Export Logs</a>
|
||||||
|
<a href="/" class="btn" style="background:#6b7280;color:#fff;margin-left:8px;">← Back</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -1 +1,20 @@
|
|||||||
{% extends "base.html" %}{% block content %}form{% endblock %}
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ "Edit Config" if config else "New Config" }}</h1>
|
||||||
|
<div class="card">
|
||||||
|
<form method="post">
|
||||||
|
{% if not config %}
|
||||||
|
<div style="margin-bottom:14px;">
|
||||||
|
<label for="name">Name</label>
|
||||||
|
<input type="text" id="name" name="name" required placeholder="My Config">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div style="margin-bottom:14px;">
|
||||||
|
<label for="base_yaml">Base YAML</label>
|
||||||
|
<textarea id="base_yaml" name="base_yaml" required>{{ config.base_yaml if config else "" }}</textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
<a href="/" class="btn" style="background:#6b7280;color:#fff;">Cancel</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -1 +1,50 @@
|
|||||||
{% extends "base.html" %}{% block content %}index{% endblock %}
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
||||||
|
<h1>Configs</h1>
|
||||||
|
<a href="/configs/new" class="btn btn-primary">+ New Config</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if configs %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Client URL</th>
|
||||||
|
<th>Last Export</th>
|
||||||
|
<th>Nodes</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for config in configs %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="/configs/{{ config.id }}">{{ config.name }}</a></td>
|
||||||
|
<td>
|
||||||
|
<span class="token-url">{{ request.url.scheme }}://{{ request.url.netloc }}/config/{{ config.token }}.yaml</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% set log = last_logs[config.id] %}
|
||||||
|
{% if log %}
|
||||||
|
<span class="{{ 'success' if log.success else 'error' }}">
|
||||||
|
{{ log.fetched_at.strftime('%Y-%m-%d %H:%M') }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="color:#999">Never</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ last_logs[config.id].node_count if last_logs[config.id] else '—' }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/configs/{{ config.id }}" class="btn btn-sm btn-primary">Edit</a>
|
||||||
|
<a href="/configs/{{ config.id }}/logs" class="btn btn-sm" style="background:#6b7280;color:#fff;">Logs</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<p style="color:#666; text-align:center;">No configs yet. <a href="/configs/new">Create one.</a></p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
+40
-1
@@ -1 +1,40 @@
|
|||||||
{% extends "base.html" %}{% block content %}logs{% endblock %}
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
||||||
|
<h1>Export Logs — {{ config.name }}</h1>
|
||||||
|
<a href="/configs/{{ config.id }}" class="btn" style="background:#6b7280;color:#fff;">← Back</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if logs %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Nodes</th>
|
||||||
|
<th>Error</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for log in logs %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ log.fetched_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||||
|
<td>
|
||||||
|
{% if log.success %}
|
||||||
|
<span class="success">✓ OK</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="error">✗ Failed</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ log.node_count }}</td>
|
||||||
|
<td style="font-size:.8rem;color:#666;">{{ log.error_message or '' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<p style="color:#666;text-align:center;">No export logs yet.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -1 +1,19 @@
|
|||||||
{% extends "base.html" %}{% block content %}sub{% endblock %}
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>Add Subscription to "{{ config.name }}"</h1>
|
||||||
|
<div class="card">
|
||||||
|
<form method="post">
|
||||||
|
<div style="margin-bottom:14px;">
|
||||||
|
<label for="name">Provider Name</label>
|
||||||
|
<input type="text" id="name" name="name" required
|
||||||
|
placeholder="Must match proxy-provider name in base_yaml">
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:14px;">
|
||||||
|
<label for="url">Subscription URL</label>
|
||||||
|
<input type="url" id="url" name="url" required placeholder="https://...">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add Subscription</button>
|
||||||
|
<a href="/configs/{{ config.id }}" class="btn" style="background:#6b7280;color:#fff;">Cancel</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user