exporting ECB
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
{% if page.can_manage_validations %}<a href="/admin/validation/new">Add validation</a>{% endif %}
|
||||
{% if page.can_manage_validations %}<a href="/admin/validation/sets/new">Add rule</a>{% endif %}
|
||||
{% if page.can_export %}<a href="/admin/export">Export</a>{% endif %}
|
||||
{% if page.can_ecb %}<a href="/admin/ecb">Exchange rates</a>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
<div id="admin-workspace">{% include "pages/admin/admin/workspace.html" %}</div>
|
||||
|
||||
80
web/templates/pages/admin/ecb/ecb.html
Normal file
80
web/templates/pages/admin/ecb/ecb.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{# GET /admin/ecb — crate::pages::admin::ecb::ui::EcbPage #}
|
||||
{% extends "ui/base.html" %}
|
||||
|
||||
{% block title %}Exchange rates{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<section class="heading">
|
||||
<div>
|
||||
<p class="eyebrow">Exchange rates</p>
|
||||
<h1>ECB reference rates</h1>
|
||||
<p>
|
||||
The importer fetches the ECB's published rates once a day and records every
|
||||
attempt. Conversions read only what it has verified, so coverage is what decides
|
||||
whether a foreign-currency amount can be posted at all.
|
||||
</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a href="/admin">← Admin panel</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include "pages/admin/ecb/status.html" %}
|
||||
|
||||
<section class="panel">
|
||||
<h2>Import attempts <span class="count">{{ page.batches.len() }}</span></h2>
|
||||
{% if page.batches.is_empty() %}
|
||||
<p class="hint">
|
||||
The importer has not recorded an attempt. It runs on startup and then daily, so
|
||||
an empty log means the server has not completed a run since this table was
|
||||
created.
|
||||
</p>
|
||||
{% else %}
|
||||
<div class="table-scroll">
|
||||
<table class="builder-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Started</th><th>Finished</th><th>Window</th><th>Result</th>
|
||||
<th>Observations</th><th>Verified through</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for batch in page.batches %}
|
||||
<tr>
|
||||
<td>{{ batch.started_at }}</td>
|
||||
<td>
|
||||
{%- if let Some(finished) = batch.completed_at %}{{ finished }}
|
||||
{%- else %}<span class="hint">still running</span>{% endif -%}
|
||||
</td>
|
||||
<td>
|
||||
{{ batch.window() }}
|
||||
<div class="hint"><code>{{ batch.endpoint }}</code></div>
|
||||
</td>
|
||||
<td>
|
||||
{%- if batch.succeeded() %}<span class="tag tag-ok">succeeded</span>
|
||||
{%- else if batch.running() %}<span class="tag">running</span>
|
||||
{%- else %}<span class="tag tag-bad">failed</span>{% endif -%}
|
||||
{%- if let Some(message) = batch.error_message %}
|
||||
<div class="hint">{{ message }}</div>
|
||||
{% endif -%}
|
||||
</td>
|
||||
<td>{{ batch.observations() }}</td>
|
||||
<td>
|
||||
{%- if let Some(date) = batch.verified_through_date %}{{ date }}
|
||||
{%- else %}<span class="hint">—</span>{% endif -%}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="hint">
|
||||
Attempts are an audit record: they cannot be edited or deleted, and a batch that
|
||||
re-fetched a day an earlier one already owned inserts nothing, which is why the
|
||||
new count can be lower than the total.
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
53
web/templates/pages/admin/ecb/status.html
Normal file
53
web/templates/pages/admin/ecb/status.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{#
|
||||
The health card — crate::pages::admin::ecb::ui::StatusFragment, and what
|
||||
ecb.html embeds on first load.
|
||||
|
||||
It carries its own poll trigger rather than the page carrying it, so the
|
||||
trigger travels with each swap: while a batch is running the card asks for
|
||||
itself every few seconds, and the card that comes back after the batch
|
||||
finishes has no trigger, which is what stops the polling.
|
||||
#}
|
||||
<section class="panel status-card {{ page.health_class() }}"
|
||||
{% if page.should_poll() %}hx-get="/admin/ecb/status" hx-trigger="every 3s" hx-target="#ecb-status" hx-swap="outerHTML"{% endif %}
|
||||
id="ecb-status">
|
||||
<h2>
|
||||
{{ page.headline() }}
|
||||
{% if page.import_running %}<span class="tag">import running</span>{% endif %}
|
||||
</h2>
|
||||
<p class="hint">{{ page.explanation() }}</p>
|
||||
|
||||
<dl class="status-grid">
|
||||
<div>
|
||||
<dt>Verified through</dt>
|
||||
<dd>{% if let Some(date) = page.verified_through_date %}{{ date }}{% else %}<span class="hint">never</span>{% endif %}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Should reach</dt>
|
||||
<dd>{{ page.latest_verifiable_date }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Next import</dt>
|
||||
<dd>{{ page.next_import_at }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Currencies on that day</dt>
|
||||
<dd>
|
||||
{%- if page.covered_currencies.is_empty() %}<span class="hint">none</span>
|
||||
{%- else %}{{ page.covered_currencies.len() }}{% endif -%}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
{% if !page.covered_currencies.is_empty() %}
|
||||
<div class="currency-list">
|
||||
{% for currency in page.covered_currencies %}<span class="tag">{{ currency }}</span>{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if let Some(failure) = page.last_failure() %}
|
||||
<p class="hint failure-note">
|
||||
Most recent failure — batch {{ failure.batch_id }}, started {{ failure.started_at }}:
|
||||
{% if let Some(message) = failure.error_message %}{{ message }}{% else %}no reason was recorded.{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
@@ -25,6 +25,7 @@
|
||||
{% if nav.can_admin %}<li><a href="/admin" class="{% if nav.active == "admin" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark dark:hover:text-primary-dark{% endif %} underline-offset-2 hover:text-primary focus:outline-hidden focus:underline" {% if nav.active == "admin" %}aria-current="page"{% endif %}>Admin</a></li>{% endif %}
|
||||
{% if nav.can_permissions %}<li><a href="/permissions" class="{% if nav.active == "permissions" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark dark:hover:text-primary-dark{% endif %} underline-offset-2 hover:text-primary focus:outline-hidden focus:underline" {% if nav.active == "permissions" %}aria-current="page"{% endif %}>Permissions</a></li>{% endif %}
|
||||
<li><a href="/" class="{% if nav.active == "analytics" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark dark:hover:text-primary-dark{% endif %} underline-offset-2 hover:text-primary focus:outline-hidden focus:underline" {% if nav.active == "analytics" %}aria-current="page"{% endif %}>Analytics</a></li>
|
||||
{% if nav.can_ecb %}<li><a href="/admin/ecb" class="{% if nav.active == "ecb" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark dark:hover:text-primary-dark{% endif %} underline-offset-2 hover:text-primary focus:outline-hidden focus:underline" {% if nav.active == "ecb" %}aria-current="page"{% endif %}>Rates</a></li>{% endif %}
|
||||
{% if nav.can_import %}<li><a href="/admin/import" class="font-medium text-on-surface underline-offset-2 hover:text-primary focus:outline-hidden focus:underline dark:text-on-surface-dark dark:hover:text-primary-dark">Import</a></li>{% endif %}
|
||||
{% if nav.can_export %}<li><a href="/admin/export" class="font-medium text-on-surface underline-offset-2 hover:text-primary focus:outline-hidden focus:underline dark:text-on-surface-dark dark:hover:text-primary-dark">Export</a></li>{% endif %}
|
||||
{% if nav.authenticated %}
|
||||
@@ -49,6 +50,7 @@
|
||||
{% if nav.can_admin %}<li class="py-4"><a href="/admin" class="w-full text-lg {% if nav.active == "admin" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark{% endif %} focus:underline" {% if nav.active == "admin" %}aria-current="page"{% endif %}>Admin</a></li>{% endif %}
|
||||
{% if nav.can_permissions %}<li class="py-4"><a href="/permissions" class="w-full text-lg {% if nav.active == "permissions" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark{% endif %} focus:underline" {% if nav.active == "permissions" %}aria-current="page"{% endif %}>Permissions</a></li>{% endif %}
|
||||
<li class="py-4"><a href="/" class="w-full text-lg {% if nav.active == "analytics" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark{% endif %} focus:underline" {% if nav.active == "analytics" %}aria-current="page"{% endif %}>Analytics</a></li>
|
||||
{% if nav.can_ecb %}<li class="py-4"><a href="/admin/ecb" class="w-full text-lg {% if nav.active == "ecb" %}font-bold text-primary dark:text-primary-dark{% else %}font-medium text-on-surface dark:text-on-surface-dark{% endif %} focus:underline" {% if nav.active == "ecb" %}aria-current="page"{% endif %}>Rates</a></li>{% endif %}
|
||||
{% if nav.can_import %}<li class="py-4"><a href="/admin/import" class="w-full text-lg font-medium text-on-surface focus:underline dark:text-on-surface-dark">Import</a></li>{% endif %}
|
||||
{% if nav.can_export %}<li class="py-4"><a href="/admin/export" class="w-full text-lg font-medium text-on-surface focus:underline dark:text-on-surface-dark">Export</a></li>{% endif %}
|
||||
{% if nav.authenticated %}
|
||||
|
||||
Reference in New Issue
Block a user