Add web GUI, docs, scripts, and 5G router config
- Web app (Flask): status, config, firewall, logs, users, restart - Docs: AT commands, deploy, DNS, quickstart, web GUI - Scripts: connect, deploy, diag, healthcheck, modem-status, speedtest, status, troubleshoot - Init and iptables: 5g-router, 5g-webgui, rules.v4 - CHANGELOG, TODO, REVISION; config and README updates
This commit is contained in:
39
web/templates/base.html
Normal file
39
web/templates/base.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Alpine 5G Router{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body class="page-{{ page_id }}" data-page="{{ page_id }}">
|
||||
<div class="dash">
|
||||
<header class="dash-header">
|
||||
<h1><a href="{{ url_for('status_page') }}" style="color: inherit; text-decoration: none;">Alpine 5G Router</a></h1>
|
||||
<div class="user-badge">
|
||||
<span id="userName">{{ user.username if user else '–' }}</span>
|
||||
<span id="userRole">{% if user %}({{ user.role }}){% endif %}</span>
|
||||
<a href="{{ url_for('logout_page') }}" class="btn btn-secondary" style="margin-left: 0.75rem; text-decoration: none;">Log out</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="nav-links">
|
||||
<a href="{{ url_for('status_page') }}" class="nav-link {% if page_id == 'status' %}active{% endif %}">Status</a>
|
||||
<a href="{{ url_for('logs_page') }}" class="nav-link {% if page_id == 'logs' %}active{% endif %}">Logs</a>
|
||||
<a href="{{ url_for('restart_page') }}" class="nav-link {% if page_id == 'restart' %}active{% endif %}">Restart 5G</a>
|
||||
{% if user and user.role == 'admin' %}
|
||||
<a href="{{ url_for('config_page') }}" class="nav-link {% if page_id == 'config' %}active{% endif %}">Config</a>
|
||||
<a href="{{ url_for('firewall_page') }}" class="nav-link {% if page_id == 'firewall' %}active{% endif %}">Firewall</a>
|
||||
<a href="{{ url_for('routes_page') }}" class="nav-link {% if page_id == 'routes' %}active{% endif %}">Routes</a>
|
||||
<a href="{{ url_for('users_page') }}" class="nav-link {% if page_id == 'users' %}active{% endif %}">Users</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<main class="page-content">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='app.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
12
web/templates/config.html
Normal file
12
web/templates/config.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Config – Alpine 5G Router{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Router config (/etc/5g-router.conf)</h2>
|
||||
<p style="color: var(--text-muted); margin: 0 0 1rem;">Edit and save. Keys and values only (one per line).</p>
|
||||
<textarea id="configEditor" class="config-editor" spellcheck="false"></textarea>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn btn-primary" id="saveConfig">Save config</button>
|
||||
<button type="button" class="btn btn-secondary" id="loadConfig">Reload</button>
|
||||
</div>
|
||||
<div id="configMsg" class="msg"></div>
|
||||
{% endblock %}
|
||||
32
web/templates/firewall.html
Normal file
32
web/templates/firewall.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Firewall – Alpine 5G Router{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Firewall rules (iptables)</h2>
|
||||
<p style="color: var(--text-muted); margin: 0 0 1rem;">Rules are stored in SQLite. Add rules below, then click Apply to write /etc/iptables/rules.v4 and run iptables-restore.</p>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Table</th><th>Rule</th><th>Enabled</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="firewallTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel-form" style="margin-top: 1rem;">
|
||||
<h3 style="font-size: 0.9375rem; margin: 0 0 0.5rem;">Add rule</h3>
|
||||
<div class="form-row">
|
||||
<select id="firewallTableName">
|
||||
<option value="filter">filter</option>
|
||||
<option value="nat">nat</option>
|
||||
</select>
|
||||
<input type="text" id="firewallRuleLine" placeholder="-A FORWARD -i eth0.100 -o eth1 -j ACCEPT" style="flex: 1; min-width: 200px;">
|
||||
<button type="button" class="btn btn-primary" id="firewallAddBtn">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions" style="margin-top: 1rem;">
|
||||
<button type="button" class="btn btn-primary" id="firewallApplyBtn">Apply to system</button>
|
||||
</div>
|
||||
<div id="firewallMsg" class="msg"></div>
|
||||
{% endblock %}
|
||||
68
web/templates/login.html
Normal file
68
web/templates/login.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Login – Alpine 5G Router</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-wrap">
|
||||
<div class="login-card">
|
||||
<h1>Alpine 5G Router</h1>
|
||||
<p class="sub">Sign in to manage modem and network</p>
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" autocomplete="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" autocomplete="current-password" required>
|
||||
</div>
|
||||
<div id="loginError" class="login-error"></div>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const form = document.getElementById('loginForm');
|
||||
const errorEl = document.getElementById('loginError');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
function showError(msg) {
|
||||
errorEl.textContent = msg || 'Login failed';
|
||||
errorEl.classList.add('visible');
|
||||
}
|
||||
function hideError() {
|
||||
errorEl.textContent = '';
|
||||
errorEl.classList.remove('visible');
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideError();
|
||||
submitBtn.disabled = true;
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
try {
|
||||
const res = await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (res.ok) {
|
||||
window.location.href = '{{ url_for("status_page") }}';
|
||||
return;
|
||||
}
|
||||
showError(data.error || 'Invalid username or password');
|
||||
} catch (err) {
|
||||
showError('Network error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
10
web/templates/logs.html
Normal file
10
web/templates/logs.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Logs – Alpine 5G Router{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Logs</h2>
|
||||
<div class="actions" style="margin-bottom: 0.5rem;">
|
||||
<button type="button" class="btn btn-secondary" data-log="5g">5G router log</button>
|
||||
<button type="button" class="btn btn-secondary" data-log="speedtest">Speedtest log</button>
|
||||
</div>
|
||||
<div class="log-view" id="logView">Select a log above.</div>
|
||||
{% endblock %}
|
||||
8
web/templates/restart.html
Normal file
8
web/templates/restart.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Restart 5G – Alpine 5G Router{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Restart 5G connection</h2>
|
||||
<p style="color: var(--text-muted); margin: 0 0 1rem;">Run the connection script to bring up or refresh the 5G link.</p>
|
||||
<button type="button" class="btn btn-primary" id="restart5gBtn">Restart 5G</button>
|
||||
<div id="restartMsg" class="msg"></div>
|
||||
{% endblock %}
|
||||
35
web/templates/routes.html
Normal file
35
web/templates/routes.html
Normal file
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Routes – Alpine 5G Router{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Static routes (SQLite)</h2>
|
||||
<p style="color: var(--text-muted); margin: 0 0 1rem;">Routes are stored in SQLite. Add below, then click Apply to run <code>ip route add</code> for each. Existing system routes are not removed.</p>
|
||||
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Destination</th><th>Gateway</th><th>Dev</th><th>Metric</th><th>Enabled</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="routesTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel-form" style="margin-top: 1rem;">
|
||||
<h3 style="font-size: 0.9375rem; margin: 0 0 0.5rem;">Add route</h3>
|
||||
<div class="form-row">
|
||||
<input type="text" id="routeDest" placeholder="0.0.0.0/0" style="width: 120px;">
|
||||
<input type="text" id="routeGw" placeholder="gateway (optional)" style="width: 100px;">
|
||||
<input type="text" id="routeDev" placeholder="dev (e.g. eth1)" style="width: 80px;">
|
||||
<input type="number" id="routeMetric" placeholder="metric" style="width: 70px;">
|
||||
<button type="button" class="btn btn-primary" id="routeAddBtn">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions" style="margin-top: 1rem;">
|
||||
<button type="button" class="btn btn-primary" id="routesApplyBtn">Apply to system</button>
|
||||
<button type="button" class="btn btn-secondary" id="routesRefreshLive">Refresh live view</button>
|
||||
</div>
|
||||
<div id="routesMsg" class="msg"></div>
|
||||
|
||||
<h3 style="font-size: 0.9375rem; margin: 1.5rem 0 0.5rem;">Current system routes (ip route show)</h3>
|
||||
<div class="routes-list" id="routesLive"></div>
|
||||
{% endblock %}
|
||||
22
web/templates/status.html
Normal file
22
web/templates/status.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Status – Alpine 5G Router{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Modem & network status</h2>
|
||||
<div class="status-grid" id="statusGrid"></div>
|
||||
|
||||
<h3 class="status-section">Modem details (AT)</h3>
|
||||
<div class="status-grid" id="modemGrid"></div>
|
||||
|
||||
<h3 class="status-section">Speedtest</h3>
|
||||
<p style="color: var(--text-muted); margin: 0 0 0.5rem;">Run speedtest via 5G (modem) or WAN (eth0).</p>
|
||||
<div class="actions" style="margin-bottom: 0.5rem;">
|
||||
<button type="button" class="btn btn-primary" id="speedtest5gBtn">Speedtest (5G)</button>
|
||||
<button type="button" class="btn btn-secondary" id="speedtestWanBtn">Speedtest (WAN)</button>
|
||||
</div>
|
||||
<div id="speedtestResult" class="speedtest-result"></div>
|
||||
<div id="speedtestMsg" class="msg"></div>
|
||||
|
||||
<div class="actions" style="margin-top: 1rem;">
|
||||
<button type="button" class="btn btn-secondary" id="refreshStatus">Refresh</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
17
web/templates/users.html
Normal file
17
web/templates/users.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Users – Alpine 5G Router{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Users (admin only)</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Username</th><th>Role</th><th>Actions</th></tr>
|
||||
</thead>
|
||||
<tbody id="usersTable"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="actions" style="margin-top: 1rem;">
|
||||
<button type="button" class="btn btn-primary" id="addUserBtn">Add user</button>
|
||||
</div>
|
||||
<div id="usersMsg" class="msg"></div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user