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:
nearxos
2026-02-02 09:38:23 +02:00
parent 1136a332b5
commit 160ad641ce
46 changed files with 4320 additions and 40 deletions

39
web/templates/base.html Normal file
View 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
View 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 %}

View 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
View 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
View 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 %}

View 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
View 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
View File

@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}Status Alpine 5G Router{% endblock %}
{% block content %}
<h2>Modem &amp; 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
View 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 %}