feat: restyle admin UI and add per-user sync run history
Adds a self-hosted stylesheet (no CDN dependencies) with a card-based dashboard and color-coded status badges, and shows the last 10 sync runs per user on the detail page.
This commit is contained in:
@@ -2,9 +2,20 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}MyWhoosh Garmin Sync{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
<header class="topbar">
|
||||
<span class="brand">MyWhoosh → Garmin Sync</span>
|
||||
<nav>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/system">System</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,31 +4,39 @@
|
||||
|
||||
{% block content %}
|
||||
<h1>Dashboard</h1>
|
||||
<p><a href="/users/new">Add user</a> | <a href="/system">System</a></p>
|
||||
|
||||
<form method="post" action="/sync-all">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit">Sync all now</button>
|
||||
</form>
|
||||
<div class="page-actions">
|
||||
<a class="btn secondary" href="/users/new">Add user</a>
|
||||
<form method="post" action="/sync-all" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit">Sync all now</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<ul class="user-list">
|
||||
{% for row in rows %}
|
||||
<li>
|
||||
<a href="/users/{{ row.id }}">{{ row.name }}</a>
|
||||
— {{ "enabled" if row.enabled else "disabled" }}
|
||||
— {{ row.health_state }}
|
||||
— last sync: {{ row.last_sync_at or "-" }}
|
||||
— last activity: {{ row.last_activity_name or "-" }} ({{ row.last_activity_status or "-" }})
|
||||
{% if row.action_reason == "garmin_mfa_required" %}
|
||||
<span class="action-required">Garmin MFA required — <a href="/users/{{ row.id }}">resolve</a></span>
|
||||
{% endif %}
|
||||
<form method="post" action="/users/{{ row.id }}/sync" style="display:inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit">Sync now</button>
|
||||
</form>
|
||||
<li class="card user-card">
|
||||
<div class="user-main">
|
||||
<a class="user-name" href="/users/{{ row.id }}">{{ row.name }}</a>
|
||||
<div class="user-meta">
|
||||
<span class="badge badge-{{ row.health_state }}">{{ row.health_state.replace("_", " ") }}</span>
|
||||
<span>{{ "enabled" if row.enabled else "disabled" }}</span>
|
||||
<span>last sync: {{ row.last_sync_at or "-" }}</span>
|
||||
<span>last activity: {{ row.last_activity_name or "-" }} ({{ row.last_activity_status or "-" }})</span>
|
||||
</div>
|
||||
{% if row.action_reason == "garmin_mfa_required" %}
|
||||
<span class="action-required">Garmin MFA required — <a href="/users/{{ row.id }}">resolve</a></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="user-actions">
|
||||
<form method="post" action="/users/{{ row.id }}/sync" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="secondary">Sync now</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>No users yet.</li>
|
||||
<li class="card empty-state">No users yet.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<form method="post" action="/users/{{ user.id }}/garmin-mfa">
|
||||
<form method="post" action="/users/{{ user.id }}/garmin-mfa" class="stacked-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label>Garmin MFA code: <input type="text" name="code" maxlength="20" required></label>
|
||||
<label for="mfa_code">Garmin MFA code</label>
|
||||
<input type="text" id="mfa_code" name="code" maxlength="20" required>
|
||||
<button type="submit">Submit code</button>
|
||||
</form>
|
||||
|
||||
@@ -1,17 +1,45 @@
|
||||
<ul>
|
||||
{% for outcome in outcomes %}
|
||||
<li>
|
||||
User {{ outcome.user_id if outcome.user_id is not none else "unknown" }}:
|
||||
status={{ outcome.status }}
|
||||
discovered={{ outcome.discovered }}
|
||||
imported={{ outcome.imported }}
|
||||
skipped={{ outcome.skipped }}
|
||||
failed={{ outcome.failed }}
|
||||
{% if outcome.message %}
|
||||
— {{ outcome.message }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% else %}
|
||||
<li>No outcomes.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Sync result - MyWhoosh Garmin Sync{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Sync result</h1>
|
||||
<div class="page-actions">
|
||||
<a class="btn secondary" href="/">Back to dashboard</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Status</th>
|
||||
<th>Discovered</th>
|
||||
<th>Imported</th>
|
||||
<th>Skipped</th>
|
||||
<th>Failed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for outcome in outcomes %}
|
||||
<tr>
|
||||
<td>{{ outcome.user_id if outcome.user_id is not none else "unknown" }}</td>
|
||||
<td><span class="badge badge-{{ outcome.status }}">{{ outcome.status }}</span></td>
|
||||
<td>{{ outcome.discovered }}</td>
|
||||
<td>{{ outcome.imported }}</td>
|
||||
<td>{{ outcome.skipped }}</td>
|
||||
<td>{{ outcome.failed }}</td>
|
||||
</tr>
|
||||
{% if outcome.message %}
|
||||
<tr>
|
||||
<td colspan="6" class="summary-error">{{ outcome.message }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="empty-state">No outcomes.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
{% block content %}
|
||||
<h1>Admin Login</h1>
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form method="post" action="/login">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required autofocus>
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
<div class="card">
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form method="post" action="/login" class="stacked-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required autofocus>
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,29 +4,30 @@
|
||||
|
||||
{% block content %}
|
||||
<h1>System</h1>
|
||||
<p><a href="/">Back to dashboard</a></p>
|
||||
|
||||
<dl>
|
||||
<dt>Application version</dt>
|
||||
<dd>{{ app_version }}</dd>
|
||||
<div class="card">
|
||||
<dl class="info-grid">
|
||||
<dt>Application version</dt>
|
||||
<dd>{{ app_version }}</dd>
|
||||
|
||||
<dt>Sync interval (minutes)</dt>
|
||||
<dd>{{ sync_interval_minutes }}</dd>
|
||||
<dt>Sync interval (minutes)</dt>
|
||||
<dd>{{ sync_interval_minutes }}</dd>
|
||||
|
||||
<dt>Last scheduler tick</dt>
|
||||
<dd>{{ last_tick or "-" }}</dd>
|
||||
<dt>Last scheduler tick</dt>
|
||||
<dd>{{ last_tick or "-" }}</dd>
|
||||
|
||||
<dt>Next scheduler tick</dt>
|
||||
<dd>{{ next_tick or "-" }}</dd>
|
||||
<dt>Next scheduler tick</dt>
|
||||
<dd>{{ next_tick or "-" }}</dd>
|
||||
|
||||
<dt>User count</dt>
|
||||
<dd>{{ user_count }}</dd>
|
||||
<dt>User count</dt>
|
||||
<dd>{{ user_count }}</dd>
|
||||
|
||||
<dt>Activity count</dt>
|
||||
<dd>{{ activity_count }}</dd>
|
||||
</dl>
|
||||
<dt>Activity count</dt>
|
||||
<dd>{{ activity_count }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/sync-all">
|
||||
<form method="post" action="/sync-all" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit">Sync all now</button>
|
||||
</form>
|
||||
|
||||
@@ -4,50 +4,98 @@
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ user.name }}</h1>
|
||||
<p><a href="/users/{{ user.id }}/edit">Edit</a> | <a href="/">Back to dashboard</a></p>
|
||||
<div class="page-actions">
|
||||
<a class="btn secondary" href="/users/{{ user.id }}/edit">Edit</a>
|
||||
<a class="btn secondary" href="/">Back to dashboard</a>
|
||||
</div>
|
||||
|
||||
<dl>
|
||||
<dt>Enabled</dt>
|
||||
<dd>{{ "Yes" if user.enabled else "No" }}</dd>
|
||||
<div class="card">
|
||||
<dl class="info-grid">
|
||||
<dt>Status</dt>
|
||||
<dd><span class="badge badge-{{ user.health_state.value }}">{{ user.health_state.value.replace("_", " ") }}</span></dd>
|
||||
|
||||
<dt>Health state</dt>
|
||||
<dd>{{ user.health_state.value }}</dd>
|
||||
<dt>Enabled</dt>
|
||||
<dd>{{ "Yes" if user.enabled else "No" }}</dd>
|
||||
|
||||
<dt>MyWhoosh state</dt>
|
||||
<dd>{{ user.mywhoosh_state }}</dd>
|
||||
<dt>MyWhoosh state</dt>
|
||||
<dd>{{ user.mywhoosh_state }}</dd>
|
||||
|
||||
<dt>Garmin state</dt>
|
||||
<dd>{{ user.garmin_state }}</dd>
|
||||
<dt>Garmin state</dt>
|
||||
<dd>{{ user.garmin_state }}</dd>
|
||||
|
||||
<dt>Action reason</dt>
|
||||
<dd>{{ user.action_reason or "-" }}</dd>
|
||||
<dt>Action reason</dt>
|
||||
<dd>{{ user.action_reason or "-" }}</dd>
|
||||
|
||||
<dt>Created at</dt>
|
||||
<dd>{{ user.created_at }}</dd>
|
||||
<dt>Created at</dt>
|
||||
<dd>{{ user.created_at }}</dd>
|
||||
|
||||
<dt>Updated at</dt>
|
||||
<dd>{{ user.updated_at }}</dd>
|
||||
</dl>
|
||||
<dt>Updated at</dt>
|
||||
<dd>{{ user.updated_at }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{% if user.action_reason == "garmin_mfa_required" %}
|
||||
<h2>Garmin MFA required</h2>
|
||||
{% include "fragments/mfa_form.html" %}
|
||||
<div class="card">
|
||||
{% include "fragments/mfa_form.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2>Activities</h2>
|
||||
<ul>
|
||||
{% for activity in activities %}
|
||||
<li>
|
||||
{{ activity.activity_name }} — {{ activity.status.value }}
|
||||
{% if activity.status.value == "failed" and activity.retryable %}
|
||||
<form method="post" action="/activities/{{ activity.id }}/retry" style="display:inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit">Retry</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
<h2>Recent sync runs</h2>
|
||||
<div class="card">
|
||||
{% if recent_runs %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Started</th>
|
||||
<th>Finished</th>
|
||||
<th>Status</th>
|
||||
<th>Discovered</th>
|
||||
<th>Imported</th>
|
||||
<th>Skipped</th>
|
||||
<th>Failed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for run in recent_runs %}
|
||||
<tr>
|
||||
<td>{{ run.started_at }}</td>
|
||||
<td>{{ run.finished_at or "-" }}</td>
|
||||
<td><span class="badge badge-{{ run.status.value }}">{{ run.status.value }}</span></td>
|
||||
<td>{{ run.discovered_count }}</td>
|
||||
<td>{{ run.imported_count }}</td>
|
||||
<td>{{ run.skipped_count }}</td>
|
||||
<td>{{ run.failed_count }}</td>
|
||||
</tr>
|
||||
{% if run.summary_error %}
|
||||
<tr>
|
||||
<td colspan="7" class="summary-error">{{ run.summary_error }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<li>No pending activities.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p class="empty-state">No sync runs yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2>Activities</h2>
|
||||
<div class="card">
|
||||
<ul class="user-list">
|
||||
{% for activity in activities %}
|
||||
<li>
|
||||
{{ activity.activity_name }} — {{ activity.status.value }}
|
||||
{% if activity.status.value == "failed" and activity.retryable %}
|
||||
<form method="post" action="/activities/{{ activity.id }}/retry" class="inline-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="secondary">Retry</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="empty-state">No pending activities.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,37 +4,39 @@
|
||||
|
||||
{% block content %}
|
||||
<h1>{% if user %}Edit User{% else %}New User{% endif %}</h1>
|
||||
<form method="post" action="{{ form_action }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="card">
|
||||
<form method="post" action="{{ form_action }}" class="stacked-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" value="{{ user.name if user else '' }}" required>
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" value="{{ user.name if user else '' }}" required>
|
||||
|
||||
<label for="mywhoosh_email">MyWhoosh Email</label>
|
||||
<input type="email" id="mywhoosh_email" name="mywhoosh_email" value="{{ mywhoosh_email }}" required>
|
||||
<label for="mywhoosh_email">MyWhoosh Email</label>
|
||||
<input type="email" id="mywhoosh_email" name="mywhoosh_email" value="{{ mywhoosh_email }}" required>
|
||||
|
||||
<label for="mywhoosh_password">MyWhoosh Password</label>
|
||||
<input type="password" id="mywhoosh_password" name="mywhoosh_password" autocomplete="new-password"
|
||||
{% if not user %}required{% endif %}>
|
||||
{% if user %}
|
||||
<p class="hint">Leave blank to keep the existing password.</p>
|
||||
{% endif %}
|
||||
<label for="mywhoosh_password">MyWhoosh Password</label>
|
||||
<input type="password" id="mywhoosh_password" name="mywhoosh_password" autocomplete="new-password"
|
||||
{% if not user %}required{% endif %}>
|
||||
{% if user %}
|
||||
<p class="hint">Leave blank to keep the existing password.</p>
|
||||
{% endif %}
|
||||
|
||||
<label for="garmin_email">Garmin Email</label>
|
||||
<input type="email" id="garmin_email" name="garmin_email" value="{{ garmin_email }}" required>
|
||||
<label for="garmin_email">Garmin Email</label>
|
||||
<input type="email" id="garmin_email" name="garmin_email" value="{{ garmin_email }}" required>
|
||||
|
||||
<label for="garmin_password">Garmin Password</label>
|
||||
<input type="password" id="garmin_password" name="garmin_password" autocomplete="new-password"
|
||||
{% if not user %}required{% endif %}>
|
||||
{% if user %}
|
||||
<p class="hint">Leave blank to keep the existing password.</p>
|
||||
{% endif %}
|
||||
<label for="garmin_password">Garmin Password</label>
|
||||
<input type="password" id="garmin_password" name="garmin_password" autocomplete="new-password"
|
||||
{% if not user %}required{% endif %}>
|
||||
{% if user %}
|
||||
<p class="hint">Leave blank to keep the existing password.</p>
|
||||
{% endif %}
|
||||
|
||||
<label for="enabled">
|
||||
<input type="checkbox" id="enabled" name="enabled" {% if not user or user.enabled %}checked{% endif %}>
|
||||
Enabled
|
||||
</label>
|
||||
<label for="enabled">
|
||||
<input type="checkbox" id="enabled" name="enabled" {% if not user or user.enabled %}checked{% endif %}>
|
||||
Enabled
|
||||
</label>
|
||||
|
||||
<button type="submit">{% if user %}Save{% else %}Create{% endif %}</button>
|
||||
</form>
|
||||
<button type="submit">{% if user %}Save{% else %}Create{% endif %}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user