Merge branch 'step6' into web-dashboard
# Conflicts: # src/web/static/app.css
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
<figure class="spark-figure"><figcaption>{{ spark.title }}{% if !spark.caption.is_empty() %} <span class="muted">· {{ spark.caption }}</span>{% endif %}</figcaption>
|
||||
{% if spark.empty %}<p class="muted">No data yet.</p>{% else %}<svg class="spark" viewBox="0 0 {{ spark.width }} {{ spark.height }}" width="{{ spark.width }}" height="{{ spark.height }}" role="img" aria-label="{{ spark.title }}">
|
||||
{% for bar in spark.bars %}<rect class="s{{ bar.series }}" x="{{ bar.x }}" y="{{ bar.y }}" width="{{ bar.w }}" height="{{ bar.h }}"><title>{{ bar.label }}</title></rect>
|
||||
{% endfor %}{% if !spark.points.is_empty() %}<polyline class="line" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" points="{{ spark.points }}"/>{% endif %}
|
||||
</svg>
|
||||
<div class="spark-axis muted"><span>{{ spark.first_label }}</span><span>{{ spark.last_label }}</span></div>
|
||||
{% if !spark.legend.is_empty() %}<ul class="spark-legend">{% for item in spark.legend %}<li><svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><rect class="s{{ item.series }}" width="10" height="10"/></svg> {{ item.name }}</li>{% endfor %}</ul>{% endif %}
|
||||
{% endif %}</figure>
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "layout.html" %}{% block content %}<section class="dashboard"{% if refresh %} data-refresh="5"{% endif %}>
|
||||
<h1><a href="/dashboard/jobs">Jobs</a> › Job {{ job.id }} · {{ job.name }} <span class="badge {{ job.status }}">{{ job.status }}</span></h1>
|
||||
{% if !description.is_empty() %}<p class="muted">{{ description }}</p>{% endif %}
|
||||
<div class="cards">
|
||||
<section class="card"><h2>Job</h2>
|
||||
<dl class="kv">
|
||||
<dt>Unit</dt><dd><code>{{ unit }}</code></dd>
|
||||
<dt>Requested</dt><dd>{{ job.requested }} · {{ job.requested_by }}</dd>
|
||||
<dt>Started</dt><dd>{{ job.started }}</dd>
|
||||
<dt>Finished</dt><dd>{{ job.finished }} · {{ job.duration }}</dd>
|
||||
<dt>Status</dt><dd><span class="badge {{ job.status }}">{{ job.status }}</span></dd>
|
||||
{% if let Some(message) = job.message %}<dt>Message</dt><dd class="message">{{ message }}</dd>{% endif %}
|
||||
{% if let Some(run_id) = job.run_id %}<dt>Run</dt><dd><a href="/dashboard/runs/{{ run_id }}">Run {{ run_id }}</a></dd>{% endif %}
|
||||
</dl>
|
||||
{% if refresh %}<p class="muted">This page reloads every 5 seconds while the job is requested or running.</p>{% endif %}
|
||||
</section>
|
||||
<section class="card"><h2>Unit</h2>
|
||||
{% if let Some(status) = status %}<dl class="kv">
|
||||
<dt>Active</dt><dd>{{ status.active_state }}{% if !status.sub_state.is_empty() %} ({{ status.sub_state }}){% endif %}</dd>
|
||||
<dt>Result</dt><dd>{{ status.result }}{% if let Some(code) = status.exit_status %} · exit status {{ code }}{% endif %}</dd>
|
||||
{% if let Some(started) = status.started %}<dt>Main started</dt><dd>{{ started }}</dd>{% endif %}
|
||||
{% if let Some(exited) = status.exited %}<dt>Main exited</dt><dd>{{ exited }}</dd>{% endif %}
|
||||
</dl>{% endif %}
|
||||
{% if let Some(error) = status_error %}<p class="error">Unit status unavailable: {{ error }}</p>{% endif %}
|
||||
<p class="muted">From <code>systemctl show {{ unit }}</code>.</p>
|
||||
</section>
|
||||
</div>
|
||||
<h2>Log</h2>
|
||||
{% if let Some(error) = log_error %}<p class="error">Journal unavailable: {{ error }}</p>{% endif %}
|
||||
<p class="muted">Last {{ log_lines }} lines of <code>journalctl -u {{ unit }}</code>.</p>
|
||||
<pre class="preview journal">{{ log }}</pre>
|
||||
</section>{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "layout.html" %}{% block content %}<section class="dashboard">
|
||||
<h1>Jobs</h1>
|
||||
{% if jobs_enabled %}<p class="muted">Each job starts <code>daily-epub-job@<name>.service</code> through systemd; the unit runs <code>daily-epub job run <name></code> and records itself here. Jobs that take the run lock wait for nothing: a second <code>generate</code> while one is running fails immediately.</p>{% else %}<p class="notice">Jobs are disabled on this server (<code>server.jobs_enabled = false</code>); run the commands by hand instead.</p>{% endif %}
|
||||
<div class="cards job-cards">{% for card in cards %}<section class="card">
|
||||
<h2>{{ card.name }}</h2>
|
||||
<p>{{ card.description }}</p>
|
||||
{% if let Some(lock) = card.lock %}<p class="muted">Takes the <code>{{ lock }}</code> lock.</p>{% endif %}
|
||||
<form method="post" action="/dashboard/jobs/{{ card.name }}" class="inline"{% if card.dangerous %} data-confirm="Start {{ card.name }}? This republishes the issue of that date."{% endif %}>
|
||||
{% if card.dated %}<label>Date <input type="date" name="date" value="{{ today }}"></label>{% endif %}
|
||||
<button type="submit"{% if !jobs_enabled %} disabled{% endif %}>Start</button>
|
||||
</form>
|
||||
</section>{% endfor %}</div>
|
||||
<h2>History</h2>
|
||||
{% if jobs.is_empty() %}<p class="muted">No jobs recorded yet.</p>{% else %}<div class="scroll-x"><table data-filter>
|
||||
<thead><tr><th>job</th><th>name</th><th>requested by</th><th>requested</th><th>started</th><th>finished</th><th class="num">duration</th><th>status</th><th>message</th><th>run</th></tr></thead>
|
||||
<tbody>{% for job in jobs %}<tr>
|
||||
<td><a href="/dashboard/jobs/{{ job.id }}">{{ job.id }}</a></td>
|
||||
<td>{{ job.name }}</td>
|
||||
<td>{{ job.requested_by }}</td>
|
||||
<td>{{ job.requested }}</td>
|
||||
<td>{{ job.started }}</td>
|
||||
<td>{{ job.finished }}</td>
|
||||
<td class="num">{{ job.duration }}</td>
|
||||
<td><span class="badge {{ job.status }}">{{ job.status }}</span></td>
|
||||
<td class="message">{% if let Some(message) = job.message %}{{ message }}{% endif %}</td>
|
||||
<td>{% if let Some(run_id) = job.run_id %}<a href="/dashboard/runs/{{ run_id }}">{{ run_id }}</a>{% endif %}</td>
|
||||
</tr>{% endfor %}</tbody></table></div>{% endif %}
|
||||
</section>{% endblock %}
|
||||
@@ -32,7 +32,8 @@
|
||||
<p><a href="/dashboard/settings">Settings</a></p>
|
||||
</section>{% endif %}
|
||||
</div>
|
||||
<!-- step 6: sparklines (cost per run, selected per run, generation seconds; last 30 non-dry runs) -->
|
||||
<h2>Last 30 runs</h2>
|
||||
<div class="sparklines">{% for item in sparklines %}{% let spark = item %}{% include "dashboard/_sparkline.html" %}{% endfor %}</div>
|
||||
<h2>Unrated picks</h2>
|
||||
{% if unrated.is_empty() %}<p class="muted">Every pick from the last three issues has a verdict.</p>{% else %}<p class="muted">Picks from the last three issues without a verdict yet.</p>
|
||||
<ul class="picks">{% for pick in unrated %}<li><a href="{{ pick.issue_href }}">{{ pick.title }}</a> <span class="muted">· {{ pick.feed }} · {{ pick.issue_date }}</span>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{% extends "layout.html" %}{% block content %}<section class="dashboard">
|
||||
<h1>Stats</h1>
|
||||
<p class="muted">Last {{ days }} days ({{ since_date }} → {{ today }}) · window: {% for window in windows %}{% if *window == days %}<strong>{{ window }}</strong>{% else %}<a href="/dashboard/stats?days={{ window }}">{{ window }}</a>{% endif %}{% if !loop.last %} · {% endif %}{% endfor %} days · the same figures as <code>daily-epub stats --days {{ days }}</code>.</p>
|
||||
<div class="sparklines">{% for item in sparklines %}{% let spark = item %}{% include "dashboard/_sparkline.html" %}{% endfor %}</div>
|
||||
<div class="cards">
|
||||
<section class="card"><h2>Issues and ratings</h2>
|
||||
<dl class="kv">{% for line in summary %}<dt>{{ line.key }}</dt><dd>{{ line.value }}</dd>{% endfor %}</dl>
|
||||
{% if ratings.is_empty() %}<p class="muted">No explicit ratings in the window.</p>{% else %}<p>{% for line in ratings %}<span class="badge {{ line.key }}">{{ line.key }}</span> {{ line.value }}{% if !loop.last %} · {% endif %}{% endfor %}</p>{% endif %}
|
||||
</section>
|
||||
<section class="card"><h2>Retriever yield</h2>
|
||||
{% if retrievers.is_empty() %}<p class="muted">No rated picks by admitting retriever in the window.</p>{% else %}<div class="scroll-x"><table>
|
||||
<thead><tr><th>admitted by</th><th class="num">rated</th><th class="num">up</th><th class="num">down</th><th class="num">ratio</th></tr></thead>
|
||||
<tbody>{% for line in retrievers %}<tr><td>{{ line.retriever }}</td><td class="num">{{ line.rated }}</td><td class="num">{{ line.up }}</td><td class="num">{{ line.down }}</td><td class="num">{{ line.ratio }}</td></tr>{% endfor %}</tbody></table></div>{% endif %}
|
||||
<h3>Exploration</h3>
|
||||
<dl class="kv">{% for line in exploration %}<dt>{{ line.key }}</dt><dd>{{ line.value }}</dd>{% endfor %}</dl>
|
||||
</section>
|
||||
<section class="card"><h2>Cost per day</h2>
|
||||
<dl class="kv">{% for line in cost_per_day %}<dt>{{ line.key }}</dt><dd>{{ line.value }}</dd>{% endfor %}</dl>
|
||||
<p class="muted">Window spend divided by {{ days }} days, per provider.</p>
|
||||
</section>
|
||||
</div>
|
||||
<h2>Spend by day</h2>
|
||||
{% if cost_rows.is_empty() %}<p class="muted">No provider costs recorded in the window.</p>{% else %}<div class="scroll-x"><table data-filter>
|
||||
<thead><tr><th>day (UTC)</th>{% for provider in providers %}<th class="num">{{ provider }}</th>{% endfor %}<th class="num">total</th></tr></thead>
|
||||
<tbody>{% for row in cost_rows %}<tr><td>{{ row.date }}</td>{% for cell in row.cells %}<td class="num">{{ cell }}</td>{% endfor %}<td class="num">{{ row.total }}</td></tr>{% endfor %}</tbody></table></div>{% endif %}
|
||||
<h2>Runs in the window</h2>
|
||||
{% if runs.is_empty() %}<p class="muted">No finished runs in the window.</p>{% else %}<div class="scroll-x"><table data-filter>
|
||||
<thead><tr><th>run</th><th>date</th><th>status</th><th class="num">cost</th><th class="num">selected</th><th class="num">duration</th></tr></thead>
|
||||
<tbody>{% for run in runs %}<tr><td><a href="/dashboard/runs/{{ run.run_id }}">{{ run.run_id }}</a></td><td><a href="/issues/{{ run.date }}">{{ run.date }}</a></td><td><span class="badge {{ run.status }}">{{ run.status }}</span></td><td class="num">{{ run.cost }}</td><td class="num">{{ run.selected }}</td><td class="num">{{ run.duration }}</td></tr>{% endfor %}</tbody></table></div>{% endif %}
|
||||
<details class="explain"><summary>As text (<code>daily-epub stats --days {{ days }}</code>)</summary><pre class="preview">{{ text }}</pre></details>
|
||||
</section>{% endblock %}
|
||||
Reference in New Issue
Block a user