This commit is contained in:
Jonas Bengtsson
2026-08-30 11:30:09 +02:00
parent 47714fa7e3
commit c139b30f6f
28 changed files with 2100 additions and 34 deletions
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}MQTT Ingester{% endblock %}</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<header>
<nav>
<a class="brand" href="/data">MQTT Ingester</a>
<a href="/data" {% if nav == 'data' %}class="active"{% endif %}>Data</a>
{% if identity and identity.is_admin %}
<a href="/users" {% if nav == 'users' %}class="active"{% endif %}>Users</a>
<a href="/groups" {% if nav == 'groups' %}class="active"{% endif %}>Groups</a>
{% endif %}
<span class="spacer"></span>
{% if identity %}
<span class="user">{{ identity.username }}{% if identity.is_admin %} (admin){% endif %}</span>
<a href="/password">Password</a>
<form method="post" action="/logout" class="inline">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<button type="submit" class="link">Log out</button>
</form>
{% endif %}
</nav>
</header>
<main>
{% if identity and identity.must_change_password %}
<div class="banner">Default password in use — please <a href="/password">change your password</a>.</div>
{% endif %}
{% block content %}{% endblock %}
</main>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Data — MQTT Ingester{% endblock %}
{% block content %}
<h1>Recent telemetry</h1>
<form method="get" action="/data" class="filter">
<input type="search" name="topic" value="{{ topic }}" placeholder="Filter by topic, e.g. 11001.*">
<button type="submit">Filter</button>
</form>
<p class="hint">Showing the 100 most recent measurements you have access to. Patterns follow the access rules: <code>*</code> matches any sequence.</p>
{% if rows %}
<table>
<thead><tr><th>Time</th><th>Topic</th><th>Payload</th></tr></thead>
<tbody>
{% for row in rows %}
<tr>
<td>{{ row.time }}</td>
<td>{{ row.topic }}</td>
<td><code>{{ row.payload | tojson }}</code></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No telemetry visible (nothing ingested yet, or your access rules match no topics).</p>
{% endif %}
{% endblock %}
+53
View File
@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}{{ group.name }} — MQTT Ingester{% endblock %}
{% block content %}
<h1>{{ group.name }}</h1>
<section>
<h2>Access rules</h2>
<p class="hint">Glob patterns matched against the full tag name. <code>*</code> matches any sequence (including <code>.</code>). Deny wins over allow. Group rules combine with users' personal rules.</p>
<table>
<thead><tr><th>Kind</th><th>Pattern</th><th></th></tr></thead>
<tbody>
{% for r in rules %}
<tr>
<td><span class="tag {{ r.kind }}">{{ r.kind }}</span></td>
<td><code>{{ r.pattern }}</code></td>
<td>
<form method="post" action="/groups/{{ group.id }}/rules/{{ r.id }}/delete">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<button type="submit" class="link danger">remove</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<form method="post" action="/groups/{{ group.id }}/rules" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<select name="kind">
<option value="allow">allow</option>
<option value="deny">deny</option>
</select>
<input type="text" name="pattern" placeholder="e.g. 11001.*" required>
<button type="submit">Add rule</button>
</form>
</section>
<section>
<h2>Members</h2>
<form method="post" action="/groups/{{ group.id }}/members" class="stack">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<fieldset>
{% for u in all_users %}
<label class="check">
<input type="checkbox" name="user_ids" value="{{ u.id }}"
{% if u.id in members | map(attribute='id') | list %}checked{% endif %}>
{{ u.username }}
</label>
{% endfor %}
</fieldset>
<button type="submit">Save members</button>
</form>
</section>
{% endblock %}
+35
View File
@@ -0,0 +1,35 @@
{% extends "base.html" %}
{% block title %}Groups — MQTT Ingester{% endblock %}
{% block content %}
<h1>Groups</h1>
<h2>Create group</h2>
<form method="post" action="/groups" class="stack">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<label>Name <input type="text" name="name" required></label>
<button type="submit">Create</button>
</form>
<h2>Existing groups</h2>
{% if groups %}
<table>
<thead><tr><th>Name</th><th>Members</th><th>Rules</th><th></th></tr></thead>
<tbody>
{% for g in groups %}
<tr>
<td><a href="/groups/{{ g.id }}">{{ g.name }}</a></td>
<td>{{ g.member_count }}</td>
<td>{{ g.rule_count }}</td>
<td>
<form method="post" action="/groups/{{ g.id }}/delete">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<button type="submit" class="link danger">delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No groups yet.</p>
{% endif %}
{% endblock %}
+24
View File
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Log in — MQTT Ingester</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="login-body">
<main class="login-card">
<h1>MQTT Ingester</h1>
{% if error %}<p class="error">{{ error }}</p>{% endif %}
<form method="post" action="/login">
<label>Username
<input type="text" name="username" required autofocus autocomplete="username">
</label>
<label>Password
<input type="password" name="password" required autocomplete="current-password">
</label>
<button type="submit">Log in</button>
</form>
</main>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
{% extends "base.html" %}
{% block title %}Password — MQTT Ingester{% endblock %}
{% block content %}
<h1>Change password</h1>
{% if error %}<p class="error">{{ error }}</p>{% endif %}
{% if identity.must_change_password %}
<p class="banner">You are using the default or admin-set password. Set a new one to continue — you will be redirected here until you do.</p>
{% endif %}
<form method="post" action="/password" class="stack">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<label>Current password
<input type="password" name="current_password" required>
</label>
<label>New password
<input type="password" name="new_password" required minlength="8" autocomplete="new-password">
</label>
<label>Confirm new password
<input type="password" name="confirm_password" required minlength="8" autocomplete="new-password">
</label>
<button type="submit">Change password</button>
</form>
{% endblock %}
+66
View File
@@ -0,0 +1,66 @@
{% extends "base.html" %}
{% block title %}{{ user.username }} — MQTT Ingester{% endblock %}
{% block content %}
<h1>{{ user.username }} <span class="tag">{{ user.role }}</span></h1>
{% if user.disabled_at %}<p class="banner">User is disabled.</p>{% endif %}
<section>
<h2>Groups</h2>
<form method="post" action="/users/{{ user.id }}/groups" class="stack">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<fieldset>
{% for g in all_groups %}
<label class="check">
<input type="checkbox" name="group_ids" value="{{ g.id }}"
{% if g.id in member_of | map(attribute='id') | list %}checked{% endif %}>
{{ g.name }}
</label>
{% else %}
<p class="hint">No groups exist yet — create one under Groups.</p>
{% endfor %}
</fieldset>
<button type="submit">Save groups</button>
</form>
</section>
<section>
<h2>Personal access rules</h2>
<p class="hint">Glob patterns matched against the full tag name. <code>*</code> matches any sequence (including <code>.</code>). Deny wins over allow. Personal rules combine with group rules; use a personal deny to override a group allow, and vice versa.</p>
<table>
<thead><tr><th>Kind</th><th>Pattern</th><th></th></tr></thead>
<tbody>
{% for r in rules %}
<tr>
<td><span class="tag {{ r.kind }}">{{ r.kind }}</span></td>
<td><code>{{ r.pattern }}</code></td>
<td>
<form method="post" action="/users/{{ user.id }}/rules/{{ r.id }}/delete">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<button type="submit" class="link danger">remove</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<form method="post" action="/users/{{ user.id }}/rules" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<select name="kind">
<option value="allow">allow</option>
<option value="deny">deny</option>
</select>
<input type="text" name="pattern" placeholder="e.g. 11001.*_Value" required>
<button type="submit">Add rule</button>
</form>
</section>
<section>
<h2>Set new password</h2>
<form method="post" action="/users/{{ user.id }}/password" class="stack">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<label>Password <input type="password" name="password" required minlength="8"></label>
<button type="submit">Set password</button>
</form>
<p class="hint">The user must change it at next login.</p>
</section>
{% endblock %}
+53
View File
@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}Users — MQTT Ingester{% endblock %}
{% block content %}
<h1>Users</h1>
<h2>Create user</h2>
<form method="post" action="/users" class="stack">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<label>Username <input type="text" name="username" required></label>
<label>Password <input type="password" name="password" required minlength="8"></label>
<label>Role
<select name="role">
<option value="user">user</option>
<option value="admin">admin</option>
</select>
</label>
<fieldset>
<legend>Groups</legend>
{% for g in groups %}
<label class="check"><input type="checkbox" name="group_ids" value="{{ g.id }}"> {{ g.name }}</label>
{% endfor %}
</fieldset>
<button type="submit">Create</button>
</form>
<h2>Existing users</h2>
<table>
<thead><tr><th>Username</th><th>Role</th><th>Groups</th><th>Status</th><th></th></tr></thead>
<tbody>
{% for u in users %}
<tr>
<td><a href="/users/{{ u.id }}">{{ u.username }}</a></td>
<td>{{ u.role }}</td>
<td>{{ u.groups | join(', ') if u.groups else '—' }}</td>
<td>
{% if u.disabled_at %}<span class="tag disabled">disabled</span>
{% elif u.must_change_password %}<span class="tag warn">must change pw</span>
{% else %}<span class="tag ok">active</span>{% endif %}
</td>
<td class="actions">
<form method="post" action="/users/{{ u.id }}/{% if u.disabled_at %}enable{% else %}disable{% endif %}">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<button type="submit" class="link">{{ 'enable' if u.disabled_at else 'disable' }}</button>
</form>
<form method="post" action="/users/{{ u.id }}/delete">
<input type="hidden" name="csrf_token" value="{{ identity.csrf_token }}">
<button type="submit" class="link danger">delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}