push
This commit is contained in:
40
templates/admin/audit.html
Normal file
40
templates/admin/audit.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Audit - Admin - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">Audit log</h1>
|
||||
<div class="text-muted">Last logs (limit 200).</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin.index') }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Action</th>
|
||||
<th>Type</th>
|
||||
<th>ID</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for l in logs %}
|
||||
<tr>
|
||||
<td class="text-muted small">{{ l.created_at }}</td>
|
||||
<td class="fw-semibold">{{ l.action }}</td>
|
||||
<td class="text-muted">{{ l.target_type or '-' }}</td>
|
||||
<td class="text-muted">{{ l.target_id or '-' }}</td>
|
||||
<td class="small">{{ l.details or '' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="text-muted">No data.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
42
templates/admin/index.html
Normal file
42
templates/admin/index.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Admin - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">Admin panel</h1>
|
||||
<div class="text-muted">Administrative tools and system overview.</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin.users') }}"><i class="fa-solid fa-users me-1"></i>Users</a>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin.audit_logs') }}"><i class="fa-solid fa-list-check me-1"></i>Audit</a>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin.smtp') }}"><i class="fa-solid fa-envelope me-1"></i>SMTP</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small">Users</div>
|
||||
<div class="display-6">{{ user_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small">Devices</div>
|
||||
<div class="display-6">{{ device_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="text-muted small">Dashboards</div>
|
||||
<div class="display-6">{{ dashboard_count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
44
templates/admin/smtp.html
Normal file
44
templates/admin/smtp.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}SMTP - Admin - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">SMTP</h1>
|
||||
<div class="text-muted">Test email sending configuration.</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin.index') }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-paper-plane me-2"></i>Send test</div>
|
||||
<form method="post" action="{{ url_for('admin.smtp_test') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Do</label>
|
||||
{{ form.to_email(class_="form-control", placeholder="email@example.com") }}
|
||||
<div class="form-text">Will send HTML email: <code>emails/smtp_test.html</code></div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-gear me-2"></i>Wymagane zmienne</div>
|
||||
<ul class="small mb-0">
|
||||
<li><code>SMTP_HOST</code>, <code>SMTP_PORT</code></li>
|
||||
<li><code>SMTP_FROM</code></li>
|
||||
<li>opcjonalnie: <code>SMTP_USER</code>, <code>SMTP_PASS</code></li>
|
||||
<li>opcjonalnie: <code>SMTP_USE_TLS</code> / <code>SMTP_USE_SSL</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
44
templates/admin/users.html
Normal file
44
templates/admin/users.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Users - Admin - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">Users</h1>
|
||||
<div class="text-muted">Lista kont w systemie.</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin.index') }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Email</th>
|
||||
<th>Rola</th>
|
||||
<th>Status</th>
|
||||
<th>Utworzono</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for u in users %}
|
||||
<tr>
|
||||
<td class="text-muted">{{ u.id }}</td>
|
||||
<td class="fw-semibold">{{ u.email }}</td>
|
||||
<td><span class="badge text-bg-secondary">{{ u.role.name }}</span></td>
|
||||
<td>
|
||||
{% if u.is_active_flag %}
|
||||
<span class="badge text-bg-success">active</span>
|
||||
{% else %}
|
||||
<span class="badge text-bg-danger">disabled</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted small">{{ u.created_at }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
40
templates/api/docs.html
Normal file
40
templates/api/docs.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}API - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">API</h1>
|
||||
<div class="text-muted">Endpoints overview (UI reference only).</div>
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.index') }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Path</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in routes %}
|
||||
<tr>
|
||||
<td><span class="badge text-bg-dark">{{ r.method }}</span></td>
|
||||
<td><code>{{ r.path }}</code></td>
|
||||
<td>{{ r.desc }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info mt-3 mb-0">
|
||||
Auth: most endpoints require a logged-in session (cookie). For external integrations use your own reverse-proxy / tokens (MVP has no dedicated API tokens).
|
||||
</div>
|
||||
{% endblock %}
|
||||
25
templates/auth/forgot.html
Normal file
25
templates/auth/forgot.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Password reset - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-6 col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h1 class="h4 mb-2"><i class="fa-solid fa-key me-2"></i>Password reset</h1>
|
||||
<div class="text-muted small mb-3">Enter your email. If the account exists, we'll send a reset link.</div>
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
{{ form.email(class_="form-control", placeholder="email@example.com") }}
|
||||
</div>
|
||||
<button class="btn btn-primary w-100" type="submit">Send link</button>
|
||||
</form>
|
||||
<div class="text-muted small mt-3">
|
||||
<a href="{{ url_for('auth.login') }}"><i class="fa-solid fa-arrow-left me-1"></i>Back to login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
29
templates/auth/login.html
Normal file
29
templates/auth/login.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Login - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-6 col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h1 class="h4 mb-3"><i class="fa-solid fa-right-to-bracket me-2"></i>Login</h1>
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
{{ form.email(class_="form-control", placeholder="email@example.com") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
{{ form.password(class_="form-control", placeholder="••••••••") }}
|
||||
</div>
|
||||
<button class="btn btn-primary w-100" type="submit">Login</button>
|
||||
</form>
|
||||
<div class="text-muted small mt-3">
|
||||
No account? <a href="{{ url_for('auth.register') }}">Register</a><br>
|
||||
Forgot password? <a href="{{ url_for('auth.forgot') }}">Reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
templates/auth/register.html
Normal file
28
templates/auth/register.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Register - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-7 col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h1 class="h4 mb-3"><i class="fa-solid fa-user-plus me-2"></i>Register</h1>
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
{{ form.email(class_="form-control", placeholder="email@example.com") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Password</label>
|
||||
{{ form.password(class_="form-control", placeholder="Min. 8 characters") }}
|
||||
</div>
|
||||
<button class="btn btn-primary w-100" type="submit">Create account</button>
|
||||
</form>
|
||||
<div class="text-muted small mt-3">
|
||||
Already have an account? <a href="{{ url_for('auth.login') }}">Login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
26
templates/auth/reset.html
Normal file
26
templates/auth/reset.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Set new password - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-6 col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h1 class="h4 mb-2"><i class="fa-solid fa-lock me-2"></i>Set new password</h1>
|
||||
<div class="text-muted small mb-3">This link is valid for a limited time.</div>
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">New password</label>
|
||||
{{ form.password(class_="form-control", placeholder="••••••••") }}
|
||||
<div class="form-text">Min. 8 characters.</div>
|
||||
</div>
|
||||
<button class="btn btn-primary w-100" type="submit">Change password</button>
|
||||
</form>
|
||||
<div class="text-muted small mt-3">
|
||||
<a href="{{ url_for('auth.login') }}"><i class="fa-solid fa-arrow-left me-1"></i>Back to login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
101
templates/base.html
Normal file
101
templates/base.html
Normal file
@@ -0,0 +1,101 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}MikroMon{% endblock %}</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
{% if current_user.is_authenticated %}
|
||||
<button class="btn btn-primary border-0 me-2 d-lg-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidePanel" aria-label="Menu">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<a class="navbar-brand fw-semibold" href="{{ url_for('dashboards.index') }}">
|
||||
<i class="fa-solid fa-chart-line me-2"></i>MikroMon
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navMain">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navMain">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('dashboards.index') }}"><i class="fa-solid fa-table-cells-large me-1"></i>Dashboards</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('devices.index') }}"><i class="fa-solid fa-microchip me-1"></i>Devices</a></li>
|
||||
{% if current_user.is_admin() %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin.index') }}"><i class="fa-solid fa-shield-halved me-1"></i>Admin</a></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul class="navbar-nav ms-auto">
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item"><span class="navbar-text small me-3"><i class="fa-regular fa-user me-1"></i>{{ current_user.email }}</span></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('auth.logout') }}"><i class="fa-solid fa-right-from-bracket me-1"></i>Logout</a></li>
|
||||
{% else %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('auth.login') }}"><i class="fa-solid fa-right-to-bracket me-1"></i>Login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="offcanvas offcanvas-start" tabindex="-1" id="sidePanel" aria-labelledby="sidePanelLabel">
|
||||
<div class="offcanvas-header">
|
||||
<h5 class="offcanvas-title" id="sidePanelLabel"><i class="fa-solid fa-gauge-high me-2"></i>MikroMon</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body">
|
||||
<div class="list-group list-group-flush">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('dashboards.index') }}"><i class="fa-solid fa-table-cells-large me-2"></i>Dashboards</a>
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('devices.index') }}"><i class="fa-solid fa-microchip me-2"></i>Devices</a>
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('api.docs') }}"><i class="fa-solid fa-code me-2"></i>API</a>
|
||||
{% if current_user.is_admin() %}
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.index') }}"><i class="fa-solid fa-shield-halved me-2"></i>Admin</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
<div class="small text-muted">
|
||||
Signed in as: <span class="fw-semibold">{{ current_user.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<main class="py-4">
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
{% set cls = 'info' %}
|
||||
{% if category in ['danger','warning','success','info'] %}{% set cls = category %}{% endif %}
|
||||
<div class="alert alert-{{ cls }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
29
templates/dashboards/index.html
Normal file
29
templates/dashboards/index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboards - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">Dashboards</h1>
|
||||
<div class="text-muted">Your monitoring dashboards.</div>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="{{ url_for('dashboards.new') }}"><i class="fa-solid fa-plus me-1"></i>New dashboard</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
{% for d in dashboards %}
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold">{{ d.name }}</div>
|
||||
<div class="text-muted small">{{ d.description or '' }}</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white border-0 pt-0 pb-3 px-3">
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('dashboards.view', dashboard_id=d.id) }}">Open <i class="fa-solid fa-arrow-right ms-1"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12"><div class="alert alert-info mb-0">No dashboards yet. Create the first one.</div></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
templates/dashboards/new.html
Normal file
28
templates/dashboards/new.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}New dashboard - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<h1 class="h4 mb-3"><i class="fa-solid fa-plus me-2"></i>New dashboard</h1>
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Name</label>
|
||||
{{ form.name(class_="form-control") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
{{ form.description(class_="form-control") }}
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-primary" type="submit">Create</button>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.index') }}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
69
templates/dashboards/public_view.html
Normal file
69
templates/dashboards/public_view.html
Normal file
@@ -0,0 +1,69 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ dashboard.name }} - Public - MikroMon{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<script>
|
||||
window.MIKROMON = {
|
||||
dashboardId: {{ dashboard.id }},
|
||||
publicToken: "{{ token }}"
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.navbar { display:none }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-3">
|
||||
<h1 class="h3 mb-0">{{ dashboard.name }}</h1>
|
||||
|
||||
{% if dashboard.description %}
|
||||
<div class="text-muted">{{ dashboard.description }}</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="text-muted small mt-2">
|
||||
<i class="fa-solid fa-link me-1"></i>
|
||||
Public view (read-only)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
{% for w in widgets %}
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card shadow-sm h-100">
|
||||
|
||||
<div class="card-header bg-white">
|
||||
<div class="fw-semibold">{{ w.title }}</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
{% if w.widget_type == 'table' %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0" data-table-widget="{{ w.id }}">
|
||||
<thead class="table-light"></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="chart-wrap">
|
||||
<canvas id="chart-{{ w.id }}"></canvas>
|
||||
</div>
|
||||
<div class="text-muted small mt-2" id="meta-{{ w.id }}">
|
||||
Waiting for data...
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info mb-0">
|
||||
No widgets available.
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
65
templates/dashboards/share.html
Normal file
65
templates/dashboards/share.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Sharing - {{ dashboard.name }} - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h1 class="h4 mb-0">Sharing</h1>
|
||||
<div class="text-muted">Dashboard: <span class="fw-semibold">{{ dashboard.name }}</span></div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.view', dashboard_id=dashboard.id) }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-user-plus me-2"></i>Share with user</div>
|
||||
<form method="post" action="{{ url_for('dashboards.share_post', dashboard_id=dashboard.id) }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-md-7">{{ form.email(class_="form-control", placeholder="email@example.com") }}</div>
|
||||
<div class="col-12 col-md-3">{{ form.permission(class_="form-select") }}</div>
|
||||
<div class="col-12 col-md-2"><button class="btn btn-primary w-100" type="submit">OK</button></div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr class="my-3">
|
||||
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-people-group me-2"></i>Current shares</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0">
|
||||
<thead class="table-light"><tr><th>Email</th><th>Permission</th></tr></thead>
|
||||
<tbody>
|
||||
{% for s in shares %}
|
||||
<tr><td>{{ s.user.email }}</td><td><span class="badge text-bg-secondary">{{ s.permission }}</span></td></tr>
|
||||
{% else %}
|
||||
<tr><td colspan="2" class="text-muted">None.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-link me-2"></i>Public link</div>
|
||||
{% if public %}
|
||||
<div class="alert alert-success small">
|
||||
<a href="{{ url_for('dashboards.public_view', token=public.token) }}" target="_blank">{{ url_for('dashboards.public_view', token=public.token, _external=true) }}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-secondary small">No active public link.</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{{ url_for('dashboards.share_public', dashboard_id=dashboard.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-outline-primary" type="submit"><i class="fa-solid fa-rotate me-1"></i>Create / refresh</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
48
templates/dashboards/view.html
Normal file
48
templates/dashboards/view.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ dashboard.name }} - MikroMon{% endblock %}
|
||||
{% block head %}
|
||||
<script>window.MIKROMON={dashboardId: {{ dashboard.id }}, publicToken: null};</script>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-start justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ dashboard.name }}</h1>
|
||||
{% if dashboard.description %}<div class="text-muted">{{ dashboard.description }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.share', dashboard_id=dashboard.id) }}"><i class="fa-solid fa-share-nodes me-1"></i>Share</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('dashboards.widget_new', dashboard_id=dashboard.id) }}"><i class="fa-solid fa-plus me-1"></i>Add widget</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
{% for w in widgets %}
|
||||
<div class="col-12 col-lg-{{ w.col_span or 6 }}">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-white d-flex align-items-center justify-content-between">
|
||||
<div class="fw-semibold">{{ w.title }}</div>
|
||||
<form method="post" action="{{ url_for('dashboards.widget_delete', dashboard_id=dashboard.id, widget_id=w.id) }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit" title="Delete"><i class="fa-solid fa-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if w.widget_type == 'table' %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle mb-0" data-table-widget="{{ w.id }}">
|
||||
<thead class="table-light"></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="chart-wrap" style="height: {{ w.height_px or 260 }}px;"><canvas id="chart-{{ w.id }}"></canvas></div>
|
||||
<div class="text-muted small mt-2" id="meta-{{ w.id }}">Waiting for data…</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12"><div class="alert alert-info mb-0">No widgets yet. Add the first one.</div></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
156
templates/dashboards/widget_new.html
Normal file
156
templates/dashboards/widget_new.html
Normal file
@@ -0,0 +1,156 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Add widget - {{ dashboard.name }} - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h1 class="h4 mb-0">Add widget</h1>
|
||||
<div class="text-muted">Dashboard: <span class="fw-semibold">{{ dashboard.name }}</span></div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.view', dashboard_id=dashboard.id) }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<form method="post" novalidate id="widgetForm">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Title</label>
|
||||
{{ form.title(class_="form-control") }}
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Preset</label>
|
||||
{{ form.preset_key(class_="form-select", id="presetSelect") }}
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Device</label>
|
||||
<select class="form-select" name="device_id" id="deviceSelect" required>
|
||||
<option value="">— select —</option>
|
||||
{% for d in devices %}
|
||||
<option value="{{ d.id }}">{{ d.name }} ({{ d.host }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-none" id="itemWrap">
|
||||
<label class="form-label" id="itemLabel">Item</label>
|
||||
<select class="form-select" id="itemSelect">
|
||||
<option value="">— select —</option>
|
||||
</select>
|
||||
<div class="form-text">E.g. interface / queue (depends on preset).</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="form-label">Refresh (seconds)</label>
|
||||
{{ form.refresh_seconds(class_="form-control", type="number", min="1") }}
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#advJson">
|
||||
<i class="fa-solid fa-code me-1"></i>Advanced (JSON)
|
||||
</button>
|
||||
<div class="collapse mt-2" id="advJson">
|
||||
{{ form.query_json(class_="form-control font-monospace", rows="6", placeholder='{"connector":"rest","endpoint":"/...","params":{}}') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<button class="btn btn-primary" type="submit"><i class="fa-solid fa-plus me-1"></i>Add</button>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('dashboards.view', dashboard_id=dashboard.id) }}">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-wand-magic-sparkles me-2"></i>Wizard mode</div>
|
||||
<div class="text-muted small">
|
||||
If the preset requires selecting an item (e.g. interface), the list will load automatically after choosing a device.
|
||||
If you fill the JSON manually, the JSON will be used.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function(){
|
||||
const presets = {{ presets|tojson }};
|
||||
const presetSelect = document.getElementById('presetSelect');
|
||||
const deviceSelect = document.getElementById('deviceSelect');
|
||||
const itemWrap = document.getElementById('itemWrap');
|
||||
const itemSelect = document.getElementById('itemSelect');
|
||||
const itemLabel = document.getElementById('itemLabel');
|
||||
|
||||
function presetNeedsItem(p){
|
||||
if(!p) return false;
|
||||
const params = p.params || {};
|
||||
return Object.prototype.hasOwnProperty.call(params, 'name');
|
||||
}
|
||||
|
||||
async function loadItems(){
|
||||
const presetKey = presetSelect.value;
|
||||
const deviceId = deviceSelect.value;
|
||||
const p = presets[presetKey];
|
||||
if(!deviceId || !presetNeedsItem(p)){
|
||||
itemWrap.classList.add('d-none');
|
||||
itemSelect.innerHTML = '<option value="">— select —</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
itemWrap.classList.remove('d-none');
|
||||
itemLabel.textContent = String(presetKey).includes('queue') ? 'Queue' : 'Interface / item';
|
||||
itemSelect.innerHTML = '<option value="">Loading…</option>';
|
||||
|
||||
try{
|
||||
const url = new URL("{{ url_for('api.preset_items') }}", window.location.origin);
|
||||
url.searchParams.set('device_id', deviceId);
|
||||
url.searchParams.set('preset_key', presetKey);
|
||||
const r = await fetch(url.toString(), {credentials:'same-origin'});
|
||||
const j = await r.json();
|
||||
const items = (j && j.data && j.data.items) ? j.data.items : [];
|
||||
itemSelect.innerHTML = '<option value="">— select —</option>' + items.map(x => `<option value="${x}">${x}</option>`).join('');
|
||||
}catch(e){
|
||||
itemSelect.innerHTML = '<option value="">Load error</option>';
|
||||
}
|
||||
}
|
||||
|
||||
presetSelect?.addEventListener('change', loadItems);
|
||||
deviceSelect?.addEventListener('change', loadItems);
|
||||
|
||||
document.getElementById('widgetForm')?.addEventListener('submit', ()=>{
|
||||
const adv = document.getElementById('{{ form.query_json.id }}');
|
||||
if(!adv) return;
|
||||
if(adv.value && adv.value.trim()) return;
|
||||
|
||||
const presetKey = presetSelect.value;
|
||||
const p = presets[presetKey];
|
||||
if(!p) return;
|
||||
|
||||
const q = {
|
||||
connector: p.connector || 'rest',
|
||||
endpoint: p.endpoint,
|
||||
params: Object.assign({}, (p.params||{})),
|
||||
extract: p.extract || {},
|
||||
preset_key: presetKey
|
||||
};
|
||||
if(presetNeedsItem(p) && itemSelect.value){
|
||||
q.params.name = itemSelect.value;
|
||||
}
|
||||
adv.value = JSON.stringify(q);
|
||||
});
|
||||
|
||||
loadItems();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
87
templates/devices/edit.html
Normal file
87
templates/devices/edit.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Edit device - {{ device.name }} - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">Edit device</h1>
|
||||
<div class="text-muted">{{ device.name }} ({{ device.host }})</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('devices.view', device_id=device.id) }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name</label>
|
||||
{{ form.name(class_="form-control") }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Host</label>
|
||||
{{ form.host(class_="form-control") }}
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">REST port</label>
|
||||
{{ form.rest_port(class_="form-control") }}
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">REST base path</label>
|
||||
{{ form.rest_base_path(class_="form-control") }}
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Username</label>
|
||||
{{ form.username(class_="form-control") }}
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Password</label>
|
||||
{{ form.password(class_="form-control", placeholder="Leave blank to keep unchanged") }}
|
||||
<div class="form-text">Leave blank to keep the current password.</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
{{ form.allow_insecure_tls(class_="form-check-input") }}
|
||||
<label class="form-check-label">Allow insecure TLS (self-signed)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
{{ form.ssh_enabled(class_="form-check-input", id="sshEnabled") }}
|
||||
<label class="form-check-label" for="sshEnabled">Enable SSH connector</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">SSH port</label>
|
||||
{{ form.ssh_port(class_="form-control") }}
|
||||
<div class="form-text">Used only when SSH is enabled.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
<button class="btn btn-primary" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-circle-info me-2"></i>Notes</div>
|
||||
<ul class="small mb-0">
|
||||
<li>Changing credentials updates the encrypted secret stored in the database.</li>
|
||||
<li>If REST fails, verify host/port/path and TLS setting.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
templates/devices/index.html
Normal file
32
templates/devices/index.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Devices - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">Devices</h1>
|
||||
<div class="text-muted">Routers / hosts to monitor.</div>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="{{ url_for('devices.new') }}"><i class="fa-solid fa-plus me-1"></i>Add device</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
{% for d in devices %}
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold">{{ d.name }}</div>
|
||||
<div class="text-muted small">{{ d.host }}</div>
|
||||
{% if d.last_error %}
|
||||
<div class="mt-2 alert alert-warning py-2 px-3 mb-0 small"><i class="fa-solid fa-triangle-exclamation me-1"></i>{{ d.last_error }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer bg-white border-0 pt-0 pb-3 px-3">
|
||||
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('devices.view', device_id=d.id) }}"><i class="fa-solid fa-eye me-1"></i>Details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-12"><div class="alert alert-info mb-0">No devices.</div></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
87
templates/devices/new.html
Normal file
87
templates/devices/new.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}New device - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">Add device</h1>
|
||||
<div class="text-muted">Configure REST/SSH access.</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('devices.index') }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="post" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">Name</label>
|
||||
{{ form.name(class_="form-control", placeholder="e.g. MikroTik RB4011") }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Host</label>
|
||||
{{ form.host(class_="form-control", placeholder="192.168.1.1 or router.example.com") }}
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">REST port</label>
|
||||
{{ form.rest_port(class_="form-control") }}
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">REST base path</label>
|
||||
{{ form.rest_base_path(class_="form-control", placeholder="/rest") }}
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Username</label>
|
||||
{{ form.username(class_="form-control") }}
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Password</label>
|
||||
{{ form.password(class_="form-control", placeholder="••••••••") }}
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
{{ form.allow_insecure_tls(class_="form-check-input") }}
|
||||
<label class="form-check-label">Allow insecure TLS (self-signed)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
{{ form.ssh_enabled(class_="form-check-input", id="sshEnabled") }}
|
||||
<label class="form-check-label" for="sshEnabled">Enable SSH connector</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">SSH port</label>
|
||||
{{ form.ssh_port(class_="form-control") }}
|
||||
<div class="form-text">Used only when SSH is enabled.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-3">
|
||||
<button class="btn btn-primary" type="submit"><i class="fa-solid fa-plus me-1"></i>Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-circle-info me-2"></i>Tips</div>
|
||||
<ul class="small mb-0">
|
||||
<li>REST uses the MikroTik API (<code>/rest</code>).</li>
|
||||
<li>If you use a self-signed cert, enable insecure TLS.</li>
|
||||
<li>SSH is optional (e.g. for commands/reads).</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
78
templates/devices/view.html
Normal file
78
templates/devices/view.html
Normal file
@@ -0,0 +1,78 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ device.name }} - Device - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex align-items-start justify-content-between mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">{{ device.name }}</h1>
|
||||
<div class="text-muted">{{ device.host }}</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('devices.index') }}"><i class="fa-solid fa-arrow-left me-1"></i>Back</a>
|
||||
<a class="btn btn-outline-primary" href="{{ url_for('devices.edit', device_id=device.id) }}"><i class="fa-solid fa-pen-to-square me-1"></i>Edit</a>
|
||||
<form method="post" action="{{ url_for('devices.delete', device_id=device.id) }}" onsubmit="return confirm('Delete this device?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button class="btn btn-outline-danger" type="submit"><i class="fa-solid fa-trash me-1"></i>Delete</button>
|
||||
</form>
|
||||
<button class="btn btn-primary" id="btnTest"><i class="fa-solid fa-plug-circle-check me-1"></i>Test REST</button>
|
||||
<button class="btn btn-outline-primary" id="btnDiscover"><i class="fa-solid fa-magnifying-glass me-1"></i>Discover</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-12 col-lg-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-server me-2"></i>Configuration</div>
|
||||
<dl class="row mb-0 small">
|
||||
<dt class="col-5 text-muted">REST</dt><dd class="col-7">{{ device.host }}:{{ device.rest_port }}{{ device.rest_base_path }}</dd>
|
||||
<dt class="col-5 text-muted">TLS</dt><dd class="col-7">{{ 'insecure' if device.allow_insecure_tls else 'strict' }}</dd>
|
||||
<dt class="col-5 text-muted">SSH</dt><dd class="col-7">{{ 'enabled' if device.ssh_enabled else 'disabled' }}{% if device.ssh_enabled %} ({{ device.ssh_port }}){% endif %}</dd>
|
||||
<dt class="col-5 text-muted">Last error</dt><dd class="col-7">{{ device.last_error or '-' }}</dd>
|
||||
<dt class="col-5 text-muted">Created</dt><dd class="col-7">{{ device.created_at }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-7">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="fw-semibold mb-2"><i class="fa-solid fa-terminal me-2"></i>Result</div>
|
||||
<pre class="bg-light border rounded p-3 mb-0" style="min-height: 240px" id="out">{}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
(function(){
|
||||
const out = document.getElementById('out');
|
||||
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
||||
function show(obj){ out.textContent = JSON.stringify(obj, null, 2); }
|
||||
async function postJson(url){
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type':'application/json', 'X-CSRFToken': csrf },
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
let data = null;
|
||||
try { data = await res.json(); } catch(e) { data = { ok:false, error:'Invalid JSON response' }; }
|
||||
if(!res.ok) return { ok:false, ...data };
|
||||
return data;
|
||||
}
|
||||
document.getElementById('btnTest')?.addEventListener('click', async ()=>{
|
||||
show({loading:true});
|
||||
const data = await postJson("{{ url_for('devices.test', device_id=device.id) }}");
|
||||
show(data);
|
||||
});
|
||||
document.getElementById('btnDiscover')?.addEventListener('click', async ()=>{
|
||||
show({loading:true});
|
||||
const res = await fetch("{{ url_for('devices.discover', device_id=device.id) }}");
|
||||
const data = await res.json();
|
||||
show(data);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
10
templates/emails/reset_password.html
Normal file
10
templates/emails/reset_password.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body style="font-family:Arial,Helvetica,sans-serif;line-height:1.4">
|
||||
<h2 style="margin:0 0 12px 0">MikroMon — password reset</h2>
|
||||
<p>We received a request to reset your password.</p>
|
||||
<p><a href="{{ reset_url }}" style="display:inline-block;padding:10px 14px;background:#0d6efd;color:#fff;text-decoration:none;border-radius:6px">Set a new password</a></p>
|
||||
<p style="color:#666;font-size:12px">This link expires in {{ ttl_minutes }} minutes. If this wasn't you, ignore this email.</p>
|
||||
</body>
|
||||
</html>
|
||||
8
templates/emails/smtp_test.html
Normal file
8
templates/emails/smtp_test.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"></head>
|
||||
<body style="font-family:Arial,Helvetica,sans-serif;line-height:1.4">
|
||||
<h2 style="margin:0 0 12px 0">MikroMon — SMTP test</h2>
|
||||
<p>If you can read this message, SMTP is working correctly.</p>
|
||||
</body>
|
||||
</html>
|
||||
29
templates/error.html
Normal file
29
templates/error.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ code }} - {{ title }} - MikroMon{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="display-6 text-muted mb-0">{{ code }}</div>
|
||||
<div>
|
||||
<h1 class="h4 mb-1">{{ title }}</h1>
|
||||
<div class="text-muted">{{ description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a class="btn btn-primary" href="{{ url_for('dashboards.index') }}"><i class="fa-solid fa-house me-1"></i>Dashboards</a>
|
||||
{% else %}
|
||||
<a class="btn btn-primary" href="{{ url_for('auth.login') }}"><i class="fa-solid fa-right-to-bracket me-1"></i>Logowanie</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('api.docs') }}"><i class="fa-solid fa-code me-1"></i>API</a>
|
||||
</div>
|
||||
<div class="text-muted small mt-3">Request ID: {{ request_id }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user