INITIAL COMMIT
This commit is contained in:
31
templates/base.html
Normal file
31
templates/base.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ title or "Garmin Coach Clone" }}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/">
|
||||
<span class="brand-mark"></span>
|
||||
<span>Coach Clone</span>
|
||||
</a>
|
||||
<nav>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/garmin">Garmin</a>
|
||||
<a href="/search">Search</a>
|
||||
<a href="/schedule">Schedule</a>
|
||||
<a href="/traces">Traces</a>
|
||||
<a href="/logs">Logs</a>
|
||||
</nav>
|
||||
<form method="post" action="/logout">
|
||||
<button class="icon-button" type="submit" title="Log out">Log out</button>
|
||||
</form>
|
||||
</header>
|
||||
<main class="shell">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
85
templates/dashboard.html
Normal file
85
templates/dashboard.html
Normal file
@ -0,0 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<p class="eyebrow">Edge 1030 structured workout bridge</p>
|
||||
<h1>Dashboard</h1>
|
||||
</div>
|
||||
<form method="post" action="/sync/run" class="inline-actions">
|
||||
<button type="submit">Sync now</button>
|
||||
<label class="checkbox"><input type="checkbox" name="dry_run"> Dry run</label>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="metrics">
|
||||
<article>
|
||||
<span>Garmin</span>
|
||||
<strong>{{ "Configured" if garmin_configured else "Needs setup" }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Last run</span>
|
||||
<strong>{{ status.last_run.status if status.last_run else "Never" }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Next check</span>
|
||||
<strong>{{ next_run.strftime("%Y-%m-%d %H:%M") if next_run else "Disabled" }}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<span>Window</span>
|
||||
<strong>{{ schedule.active_window }}</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header>
|
||||
<h2>Manual Checks</h2>
|
||||
</header>
|
||||
<div class="button-row">
|
||||
<form method="post" action="/sync/run">
|
||||
<input type="hidden" name="date_value" value="{{ today }}">
|
||||
<button type="submit">Check today</button>
|
||||
</form>
|
||||
<form method="post" action="/sync/run">
|
||||
<input type="hidden" name="date_value" value="{{ tomorrow }}">
|
||||
<button type="submit">Check tomorrow</button>
|
||||
</form>
|
||||
<form method="post" action="/sync/run">
|
||||
<input type="hidden" name="date_value" value="{{ today }}">
|
||||
<input type="hidden" name="dry_run" value="on">
|
||||
<button type="submit">Dry-run today</button>
|
||||
</form>
|
||||
<a class="button secondary" href="/search">Search workouts</a>
|
||||
<a class="button secondary" href="/schedule">Edit schedule</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header>
|
||||
<h2>Clone State</h2>
|
||||
</header>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Source</th>
|
||||
<th>Clone</th>
|
||||
<th>Status</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for clone in status.clones %}
|
||||
<tr>
|
||||
<td>{{ clone.scheduled_date }}</td>
|
||||
<td>{{ clone.source_name or "-" }}</td>
|
||||
<td>{{ clone.clone_workout_name or "-" }}</td>
|
||||
<td><span class="badge">{{ clone.status }}</span></td>
|
||||
<td>{{ clone.updated_at }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="empty">No generated clones recorded yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
32
templates/garmin.html
Normal file
32
templates/garmin.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<p class="eyebrow">Garmin Connect</p>
|
||||
<h1>Authentication</h1>
|
||||
</div>
|
||||
<span class="state {{ 'ok' if configured else 'warn' }}">{{ "Configured" if configured else "Not configured" }}</span>
|
||||
</section>
|
||||
|
||||
{% if error %}<p class="alert">{{ error }}</p>{% endif %}
|
||||
{% if message %}<p class="notice">{{ message }}</p>{% endif %}
|
||||
|
||||
<section class="panel form-panel">
|
||||
<header><h2>Garmin Login</h2></header>
|
||||
<form method="post" action="/garmin/setup" class="grid-form">
|
||||
<label>Email <input name="email" type="email" autocomplete="email" required></label>
|
||||
<label>Password <input name="password" type="password" autocomplete="current-password" required></label>
|
||||
<button type="submit">Save and log in</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if mfa_pending %}
|
||||
<section class="panel form-panel">
|
||||
<header><h2>MFA Challenge</h2></header>
|
||||
<form method="post" action="/garmin/mfa" class="grid-form">
|
||||
<label>Code <input name="code" inputmode="numeric" autocomplete="one-time-code" required></label>
|
||||
<button type="submit">Submit MFA</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
22
templates/login.html
Normal file
22
templates/login.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Log In</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
<main class="auth-panel">
|
||||
<div class="brand auth-brand"><span class="brand-mark"></span><span>Coach Clone</span></div>
|
||||
<h1>Sign In</h1>
|
||||
<p>Local single-user access for Garmin Coach cloning.</p>
|
||||
{% if error %}<p class="alert">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="/login" class="stack">
|
||||
<label>Username <input name="username" autocomplete="username" required></label>
|
||||
<label>Password <input name="password" type="password" autocomplete="current-password" required></label>
|
||||
<button type="submit">Log in</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
36
templates/logs.html
Normal file
36
templates/logs.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<p class="eyebrow">Runtime trail</p>
|
||||
<h1>Logs</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Level</th>
|
||||
<th>Event</th>
|
||||
<th>Date</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in events %}
|
||||
<tr>
|
||||
<td>{{ event.created_at }}</td>
|
||||
<td><span class="badge {{ event.level }}">{{ event.level }}</span></td>
|
||||
<td>{{ event.event_type }}</td>
|
||||
<td>{{ event.scheduled_date or "-" }}</td>
|
||||
<td>{{ event.message }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="empty">No log events yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
47
templates/schedule.html
Normal file
47
templates/schedule.html
Normal file
@ -0,0 +1,47 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<p class="eyebrow">Change detection</p>
|
||||
<h1>Schedule</h1>
|
||||
</div>
|
||||
<form method="post" action="/schedule/restore">
|
||||
<button type="submit" class="secondary">Restore defaults</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% if error %}<p class="alert">{{ error }}</p>{% endif %}
|
||||
{% if message %}<p class="notice">{{ message }}</p>{% endif %}
|
||||
|
||||
<section class="panel form-panel">
|
||||
<header>
|
||||
<h2>Sync Cadence</h2>
|
||||
</header>
|
||||
<form method="post" action="/schedule" class="stack">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" name="enabled" {% if schedule.enabled %}checked{% endif %}>
|
||||
Sync enabled
|
||||
</label>
|
||||
<div class="grid-form">
|
||||
<label>Interval minutes
|
||||
<input name="interval_minutes" type="number" min="5" max="1440" value="{{ schedule.interval_minutes }}" required>
|
||||
</label>
|
||||
<label>Active window
|
||||
<input name="active_window" pattern="^([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]$" value="{{ schedule.active_window }}" required>
|
||||
</label>
|
||||
<label>Days ahead
|
||||
<input name="days_ahead" type="number" min="0" max="14" value="{{ schedule.days_ahead }}" required>
|
||||
</label>
|
||||
</div>
|
||||
<label>Fixed check times
|
||||
<textarea name="fixed_times" rows="3" required>{{ schedule.fixed_times | join(",") }}</textarea>
|
||||
</label>
|
||||
<button type="submit">Save schedule</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header><h2>Default Behavior</h2></header>
|
||||
<p class="muted">All scheduled runs are change-detection runs. They create a missing clone, skip a current clone, and replace only when the Coach source hash changes.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
74
templates/search.html
Normal file
74
templates/search.html
Normal file
@ -0,0 +1,74 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<p class="eyebrow">Garmin Connect</p>
|
||||
<h1>Search</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if error %}<p class="alert">{{ error }}</p>{% endif %}
|
||||
|
||||
<section class="panel form-panel">
|
||||
<header><h2>Filters</h2></header>
|
||||
<form method="get" action="/search" class="grid-form">
|
||||
<label>Start
|
||||
<input name="start" type="date" value="{{ filters.start }}" required>
|
||||
</label>
|
||||
<label>End
|
||||
<input name="end" type="date" value="{{ filters.end }}" required>
|
||||
</label>
|
||||
<label>Sport
|
||||
<input name="sport" value="{{ filters.sport }}" placeholder="cycling">
|
||||
</label>
|
||||
<label>Text
|
||||
<input name="q" value="{{ filters.q }}" placeholder="Sprint, GCClone, plan">
|
||||
</label>
|
||||
<label>Source
|
||||
<select name="source">
|
||||
{% for value, label in [
|
||||
("all", "All"),
|
||||
("calendar", "Calendar"),
|
||||
("workouts", "Normal workouts"),
|
||||
("plans", "Plans"),
|
||||
("coach", "Coach"),
|
||||
("cloned", "Cloned")
|
||||
] %}
|
||||
<option value="{{ value }}" {% if filters.source == value %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header><h2>Results</h2></header>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Date</th>
|
||||
<th>Sport</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td><span class="badge">{{ item.source }}</span></td>
|
||||
<td>{{ item.date or "-" }}</td>
|
||||
<td>{{ item.sport or "-" }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.status or "-" }}</td>
|
||||
<td><pre class="inline-pre">{{ item.summary or "" }}</pre></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="empty">No matching Garmin items found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
23
templates/setup.html
Normal file
23
templates/setup.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Set Up Coach Clone</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
<main class="auth-panel">
|
||||
<div class="brand auth-brand"><span class="brand-mark"></span><span>Coach Clone</span></div>
|
||||
<h1>First Launch</h1>
|
||||
<p>Create the local app login. Garmin credentials are configured after this.</p>
|
||||
{% if error %}<p class="alert">{{ error }}</p>{% endif %}
|
||||
<form method="post" action="/setup" class="stack">
|
||||
<label>Username <input name="username" autocomplete="username" required></label>
|
||||
<label>Password <input name="password" type="password" autocomplete="new-password" required></label>
|
||||
<label>Confirm <input name="confirm_password" type="password" autocomplete="new-password" required></label>
|
||||
<button type="submit">Create app login</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
32
templates/trace.html
Normal file
32
templates/trace.html
Normal file
@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<p class="eyebrow">Trace #{{ trace.id }}</p>
|
||||
<h1>{{ trace.action }} / {{ trace.status }}</h1>
|
||||
</div>
|
||||
<a class="button secondary" href="/traces">Back to traces</a>
|
||||
</section>
|
||||
|
||||
<section class="metrics">
|
||||
<article><span>Date</span><strong>{{ trace.scheduled_date }}</strong></article>
|
||||
<article><span>Source</span><strong>{{ trace.source_name or "-" }}</strong></article>
|
||||
<article><span>Clone</span><strong>{{ trace.clone_workout_name or "-" }}</strong></article>
|
||||
<article><span>Hash</span><strong class="hash">{{ trace.source_hash or "-" }}</strong></article>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header><h2>Message</h2></header>
|
||||
<p>{{ trace.message }}</p>
|
||||
</section>
|
||||
|
||||
{% for key, value in json.items() %}
|
||||
<section class="panel">
|
||||
<header>
|
||||
<h2>{{ key.replace("_json", "").title() }} JSON</h2>
|
||||
<a class="button secondary" href="/traces/{{ trace.id }}/{{ key.replace('_json', '') }}.json">Download</a>
|
||||
</header>
|
||||
<pre>{{ value }}</pre>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
38
templates/traces.html
Normal file
38
templates/traces.html
Normal file
@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<p class="eyebrow">Workout tracing</p>
|
||||
<h1>Traces</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Date</th>
|
||||
<th>Action</th>
|
||||
<th>Status</th>
|
||||
<th>Source</th>
|
||||
<th>Clone</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for trace in traces %}
|
||||
<tr>
|
||||
<td><a href="/traces/{{ trace.id }}">{{ trace.created_at }}</a></td>
|
||||
<td>{{ trace.scheduled_date }}</td>
|
||||
<td>{{ trace.action }}</td>
|
||||
<td><span class="badge">{{ trace.status }}</span></td>
|
||||
<td>{{ trace.source_name or "-" }}</td>
|
||||
<td>{{ trace.clone_workout_name or "-" }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="empty">No workout traces yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user