INITIAL COMMIT

This commit is contained in:
2026-06-16 15:14:37 +02:00
commit 1477ec36fd
49 changed files with 6835 additions and 0 deletions

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