First product
This commit is contained in:
parent
778da9e2fc
commit
4433d7d2c4
157 changed files with 23975 additions and 0 deletions
23
app/templates/account_password.html
Normal file
23
app/templates/account_password.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero" data-test="account-password-page">
|
||||
<p class="eyebrow">Account</p>
|
||||
<h1>Change Password</h1>
|
||||
<p class="lede">Use a strong password. This updates only your own account.</p>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="account-password-form-card">
|
||||
<h2>Password Update</h2>
|
||||
<form method="post" action="/account/password" class="stack-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label for="current_password">Current Password</label>
|
||||
<input id="current_password" name="current_password" type="password" autocomplete="current-password" required>
|
||||
<label for="new_password">New Password</label>
|
||||
<input id="new_password" name="new_password" type="password" autocomplete="new-password" minlength="8" required>
|
||||
<label for="confirm_password">Confirm New Password</label>
|
||||
<input id="confirm_password" name="confirm_password" type="password" autocomplete="new-password" minlength="8" required>
|
||||
<button type="submit" data-loading-text="Updating...">Update Password</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
66
app/templates/base.html
Normal file
66
app/templates/base.html
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="{{ theme_name | default('terracotta') }}" data-default-theme="{{ theme_name | default('terracotta') }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ page_title }} | scholarr</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='theme.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='app.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="loading-indicator" data-loading-indicator aria-hidden="true"></div>
|
||||
<div class="app-backdrop" aria-hidden="true"></div>
|
||||
<header class="top-app-bar">
|
||||
<div class="brand-wrap">
|
||||
<a class="brand" href="/">scholarr</a>
|
||||
{% if session_user %}
|
||||
<p class="session-user" data-test="session-user">{{ session_user.email }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<nav class="site-nav" aria-label="Primary">
|
||||
{% if session_user %}
|
||||
<a class="nav-link {% if active_nav == 'home' %}is-active{% endif %}" href="/">Dashboard</a>
|
||||
<a class="nav-link {% if active_nav == 'scholars' %}is-active{% endif %}" href="/scholars">Scholars</a>
|
||||
<a class="nav-link {% if active_nav == 'publications' %}is-active{% endif %}" href="/publications?mode=new">Publications</a>
|
||||
<a class="nav-link {% if active_nav == 'runs' %}is-active{% endif %}" href="/runs">Runs</a>
|
||||
<a class="nav-link {% if active_nav == 'settings' %}is-active{% endif %}" href="/settings">Settings</a>
|
||||
<a class="nav-link {% if active_nav == 'account_password' %}is-active{% endif %}" href="/account/password">Password</a>
|
||||
{% if session_user.is_admin %}
|
||||
<a class="nav-link {% if active_nav == 'users' %}is-active{% endif %}" href="/users">Users</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<a class="nav-link {% if active_nav == 'login' %}is-active{% endif %}" href="/login">Login</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<div class="header-actions">
|
||||
<div class="theme-control-wrap">
|
||||
<label for="theme-control">Theme</label>
|
||||
<select id="theme-control" class="theme-control" data-theme-control aria-label="Color theme">
|
||||
{% for theme in themes %}
|
||||
<option value="{{ theme.slug }}" {% if theme.slug == theme_name %}selected{% endif %}>{{ theme.label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if session_user %}
|
||||
<form method="post" action="/logout" class="logout-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="button button-text" data-loading-text="Logging out...">Log out</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
<main class="page-shell">
|
||||
{% if notice %}
|
||||
<p class="flash flash-notice" data-test="flash-notice">{{ notice }}</p>
|
||||
{% endif %}
|
||||
{% if page_error %}
|
||||
<p class="flash flash-error" data-test="flash-error">{{ page_error }}</p>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<script defer src="{{ url_for('static', path='app.js') }}"></script>
|
||||
<script defer src="{{ url_for('static', path='theme.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
app/templates/dashboard/_run_controls.html
Normal file
23
app/templates/dashboard/_run_controls.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<section class="sandbox" data-test="dashboard-run-controls">
|
||||
<div class="section-heading">
|
||||
<h2>Run Tracking</h2>
|
||||
<div class="inline-actions">
|
||||
<a class="link-button" href="/publications?mode=new">New Publications</a>
|
||||
<a class="link-button" href="/publications?mode=all">All Publications</a>
|
||||
<a class="link-button" href="/runs?failed_only=1">Run Diagnostics</a>
|
||||
<a class="link-button" href="/settings">Automation Settings</a>
|
||||
<a class="link-button" href="/scholars">Manage Scholars</a>
|
||||
</div>
|
||||
</div>
|
||||
<p class="lede">Manual runs use your request delay of {{ dashboard.run_controls.request_delay_seconds }} second(s).</p>
|
||||
<p class="helper-text">
|
||||
Queue state:
|
||||
{{ dashboard.run_controls.queue_queued_count }} queued,
|
||||
{{ dashboard.run_controls.queue_retrying_count }} retrying,
|
||||
{{ dashboard.run_controls.queue_dropped_count }} dropped.
|
||||
</p>
|
||||
<form method="post" action="{{ dashboard.run_controls.run_manual_action }}" class="inline-actions">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" data-loading-text="Running...">Run Now</button>
|
||||
</form>
|
||||
</section>
|
||||
38
app/templates/dashboard/_run_history.html
Normal file
38
app/templates/dashboard/_run_history.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<section class="sandbox" data-test="dashboard-run-history">
|
||||
<div class="section-heading">
|
||||
<h2>Run History</h2>
|
||||
<div class="inline-actions">
|
||||
<a class="link-button" href="/runs">View All Runs</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if dashboard.run_history %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Started</th>
|
||||
<th>Status</th>
|
||||
<th>Scholars</th>
|
||||
<th>New Publications</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for run in dashboard.run_history %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if run.detail_url %}
|
||||
<a href="{{ run.detail_url }}">{{ run.started_at_display }}</a>
|
||||
{% else %}
|
||||
{{ run.started_at_display }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="badge {{ run.status_badge }}">{{ run.status_label }}</span></td>
|
||||
<td>{{ run.scholar_count }}</td>
|
||||
<td>{{ run.new_publication_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No runs yet.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
47
app/templates/dashboard/_unread_publications.html
Normal file
47
app/templates/dashboard/_unread_publications.html
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<section class="sandbox" data-test="dashboard-unread-publications">
|
||||
<div class="section-heading">
|
||||
<h2>New Since Last Run</h2>
|
||||
<div class="inline-actions">
|
||||
<a class="link-button" href="/publications?mode=all">View All</a>
|
||||
<form method="post" action="{{ dashboard.run_controls.mark_all_read_action }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="return_to" value="/">
|
||||
<button type="submit" data-loading-text="Marking...">Mark All Read</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if dashboard.unread_publications %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Scholar</th>
|
||||
<th>Year</th>
|
||||
<th>Citations</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in dashboard.unread_publications %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if item.pub_url %}
|
||||
<a href="{{ item.pub_url }}" target="_blank" rel="noreferrer">{{ item.title }}</a>
|
||||
{% else %}
|
||||
{{ item.title }}
|
||||
{% endif %}
|
||||
{% if item.venue_text %}
|
||||
<p class="helper-text">{{ item.venue_text }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.scholar_label }}</td>
|
||||
<td>{{ item.year_display }}</td>
|
||||
<td>{{ item.citation_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No new publications in the latest run.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
27
app/templates/index.html
Normal file
27
app/templates/index.html
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero" data-test="home-hero">
|
||||
<p class="eyebrow">Dashboard</p>
|
||||
<h1>Keep up with your tracked scholars</h1>
|
||||
<p class="lede">Run ingestion, review fresh publications, and inspect recent outcomes from one screen.</p>
|
||||
<div class="stat-strip">
|
||||
<article class="stat-item">
|
||||
<p class="stat-label">New Since Last Run</p>
|
||||
<p class="stat-value">{{ dashboard.unread_publications | length }}</p>
|
||||
</article>
|
||||
<article class="stat-item">
|
||||
<p class="stat-label">Recent Runs</p>
|
||||
<p class="stat-value">{{ dashboard.run_history | length }}</p>
|
||||
</article>
|
||||
<article class="stat-item">
|
||||
<p class="stat-label">Automation Delay</p>
|
||||
<p class="stat-value">{{ dashboard.run_controls.request_delay_seconds }}s</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% for section_template in dashboard.section_templates %}
|
||||
{% include section_template %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
23
app/templates/login.html
Normal file
23
app/templates/login.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero auth-card" data-test="login-card">
|
||||
<p class="eyebrow">Secure Access</p>
|
||||
<h1>Sign in</h1>
|
||||
<p class="lede">Use an admin-created account to access your tracked scholars and settings.</p>
|
||||
{% if error_message %}
|
||||
<p class="form-error" role="alert">{{ error_message }}</p>
|
||||
{% endif %}
|
||||
{% if retry_after_seconds %}
|
||||
<p class="form-note">Retry after {{ retry_after_seconds }} seconds.</p>
|
||||
{% endif %}
|
||||
<form method="post" action="/login" class="auth-form" data-test="login-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label for="email">Email</label>
|
||||
<input id="email" name="email" type="email" autocomplete="username" required autofocus>
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
||||
<button type="submit" data-loading-text="Signing in...">Sign in</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
104
app/templates/publications.html
Normal file
104
app/templates/publications.html
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero" data-test="publications-page">
|
||||
<p class="eyebrow">Publications</p>
|
||||
<h1>Publications</h1>
|
||||
<p class="lede">
|
||||
Browse publications discovered in the latest run or your complete tracked library.
|
||||
{% if selected_scholar %}
|
||||
Current scholar filter: <strong>{{ selected_scholar.display_name or selected_scholar.scholar_id }}</strong>.
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="inline-actions">
|
||||
<a class="link-button {% if mode == 'new' %}is-active{% endif %}" href="{{ mode_new_url }}">New Since Last Run ({{ new_count }})</a>
|
||||
<a class="link-button {% if mode == 'all' %}is-active{% endif %}" href="{{ mode_all_url }}">All ({{ total_count }})</a>
|
||||
<a class="link-button" href="/scholars">Manage Scholars</a>
|
||||
<a class="link-button" href="/runs?failed_only=1">Run Diagnostics</a>
|
||||
{% if mode == 'new' and new_count > 0 %}
|
||||
<form method="post" action="/publications/mark-all-read">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="return_to" value="{{ current_publications_url }}">
|
||||
<button type="submit" data-loading-text="Marking...">Mark All Read</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="publications-table">
|
||||
<div class="section-heading">
|
||||
<h2>Filter</h2>
|
||||
</div>
|
||||
<form method="get" action="/publications" class="inline-actions">
|
||||
<label for="mode">Mode</label>
|
||||
<select id="mode" name="mode">
|
||||
<option value="new" {% if mode == 'new' %}selected{% endif %}>New Since Last Run</option>
|
||||
<option value="all" {% if mode == 'all' %}selected{% endif %}>All Publications</option>
|
||||
</select>
|
||||
|
||||
<label for="scholar_profile_id">Scholar</label>
|
||||
<select id="scholar_profile_id" name="scholar_profile_id">
|
||||
<option value="">All Scholars</option>
|
||||
{% for scholar in scholars %}
|
||||
<option value="{{ scholar.id }}" {% if selected_scholar_id == scholar.id %}selected{% endif %}>
|
||||
{{ scholar.display_name or scholar.scholar_id }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" data-loading-text="Filtering...">Apply</button>
|
||||
</form>
|
||||
|
||||
<div class="section-heading">
|
||||
<h2>Results</h2>
|
||||
</div>
|
||||
{% if publications %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Scholar</th>
|
||||
<th>Year</th>
|
||||
<th>Citations</th>
|
||||
<th>New This Run</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in publications %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if item.pub_url %}
|
||||
<a href="{{ item.pub_url }}" target="_blank" rel="noreferrer">{{ item.title }}</a>
|
||||
{% else %}
|
||||
{{ item.title }}
|
||||
{% endif %}
|
||||
{% if item.venue_text %}
|
||||
<p class="helper-text">{{ item.venue_text }}</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.scholar_label }}</td>
|
||||
<td>{{ item.year if item.year is not none else "-" }}</td>
|
||||
<td>{{ item.citation_count }}</td>
|
||||
<td>
|
||||
{% if item.is_new_in_latest_run %}
|
||||
<span class="badge warn">new</span>
|
||||
{% else %}
|
||||
<span class="badge">older</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.is_read %}
|
||||
<span class="badge ok">read</span>
|
||||
{% else %}
|
||||
<span class="badge warn">new</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No publications match this mode yet.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
98
app/templates/run_detail.html
Normal file
98
app/templates/run_detail.html
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero" data-test="run-detail-page">
|
||||
<p class="eyebrow">Diagnostics</p>
|
||||
<h1>Run #{{ run.id }}</h1>
|
||||
<p class="lede">Detailed state and failure context for this execution.</p>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="run-detail-summary">
|
||||
<h2>Summary</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><th>Started</th><td>{{ run.started_at }}</td></tr>
|
||||
<tr><th>Finished</th><td>{{ run.finished_at }}</td></tr>
|
||||
<tr><th>Status</th><td><span class="badge {{ run.status_badge }}">{{ run.status }}</span></td></tr>
|
||||
<tr><th>Trigger</th><td>{{ run.trigger_type }}</td></tr>
|
||||
<tr><th>Scholars</th><td>{{ run.scholar_count }}</td></tr>
|
||||
<tr><th>New Publications</th><td>{{ run.new_publication_count }}</td></tr>
|
||||
<tr><th>Succeeded</th><td>{{ run_summary.succeeded_count or 0 }}</td></tr>
|
||||
<tr><th>Failed</th><td>{{ run_summary.failed_count or 0 }}</td></tr>
|
||||
<tr><th>Partial</th><td>{{ run_summary.partial_count or 0 }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% if run_summary.failed_state_counts or run_summary.failed_reason_counts %}
|
||||
<details>
|
||||
<summary>Failure Breakdown</summary>
|
||||
<pre>{{ {"failed_state_counts": run_summary.failed_state_counts, "failed_reason_counts": run_summary.failed_reason_counts} | tojson(indent=2) }}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="run-detail-results">
|
||||
<h2>Scholar Results</h2>
|
||||
{% if scholar_results %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Scholar</th>
|
||||
<th>Outcome</th>
|
||||
<th>State</th>
|
||||
<th>Reason</th>
|
||||
<th>Publications</th>
|
||||
<th>Pages</th>
|
||||
<th>Attempts</th>
|
||||
<th>Continuation</th>
|
||||
<th>Warnings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in scholar_results %}
|
||||
<tr>
|
||||
<td><code>{{ item.scholar_id }}</code></td>
|
||||
<td>{{ item.outcome or "-" }}</td>
|
||||
<td>{{ item.state }}</td>
|
||||
<td>{{ item.state_reason or "-" }}</td>
|
||||
<td>{{ item.publication_count }}</td>
|
||||
<td>{{ item.pages_fetched or 0 }} / {{ item.pages_attempted or 0 }}</td>
|
||||
<td>{{ item.attempt_count or 1 }}</td>
|
||||
<td>
|
||||
{% if item.continuation_enqueued %}
|
||||
<span class="badge warn">queued</span>
|
||||
<p class="helper-text">
|
||||
reason: {{ item.continuation_reason or "-" }},
|
||||
cstart: {{ item.continuation_cstart or "-" }}
|
||||
</p>
|
||||
{% elif item.continuation_cleared %}
|
||||
<span class="badge ok">cleared</span>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.warnings %}
|
||||
{{ item.warnings | join(", ") }}
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if item.debug %}
|
||||
<tr>
|
||||
<td colspan="9">
|
||||
<details>
|
||||
<summary>Debug Context</summary>
|
||||
<pre>{{ item.debug | tojson(indent=2) }}</pre>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No scholar result details were captured.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
129
app/templates/runs.html
Normal file
129
app/templates/runs.html
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero" data-test="runs-page">
|
||||
<p class="eyebrow">Runs</p>
|
||||
<h1>Run History</h1>
|
||||
<p class="lede">Review execution outcomes and drill into run diagnostics when needed.</p>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="runs-filter-card">
|
||||
<h2>Filters</h2>
|
||||
<form method="get" action="/runs" class="inline-actions">
|
||||
<label class="checkbox-row" for="failed_only">
|
||||
<input id="failed_only" type="checkbox" name="failed_only" value="1" {% if failed_only %}checked{% endif %}>
|
||||
<span>Failed / partial only</span>
|
||||
</label>
|
||||
<button type="submit" data-loading-text="Filtering...">Apply</button>
|
||||
{% if failed_only %}
|
||||
<a class="link-button" href="/runs">Clear</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="runs-table-card">
|
||||
<h2>Recent Runs</h2>
|
||||
{% if runs %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Started</th>
|
||||
<th>Status</th>
|
||||
<th>Trigger</th>
|
||||
<th>Scholars</th>
|
||||
<th>New Publications</th>
|
||||
<th>Failures</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for run in runs %}
|
||||
<tr>
|
||||
<td><a href="/runs/{{ run.id }}">#{{ run.id }}</a></td>
|
||||
<td>{{ run.started_at }}</td>
|
||||
<td><span class="badge {{ run.status_badge }}">{{ run.status }}</span></td>
|
||||
<td>{{ run.trigger_type }}</td>
|
||||
<td>{{ run.scholar_count }}</td>
|
||||
<td>{{ run.new_publication_count }}</td>
|
||||
<td>{{ run.failed_count }} failed / {{ run.partial_count }} partial</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No runs match the current filter.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="runs-queue-card">
|
||||
<div class="section-heading">
|
||||
<h2>Continuation Queue</h2>
|
||||
<a class="link-button" href="/runs?failed_only=1">Focus Failed Runs</a>
|
||||
</div>
|
||||
{% if queue_items %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Scholar</th>
|
||||
<th>Status</th>
|
||||
<th>Reason</th>
|
||||
<th>Attempts</th>
|
||||
<th>Next Attempt</th>
|
||||
<th>Last Error</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in queue_items %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/publications?mode=new&scholar_profile_id={{ item.scholar_profile_id }}">{{ item.scholar_label }}</a>
|
||||
<p class="helper-text">resume cstart: {{ item.resume_cstart }}</p>
|
||||
</td>
|
||||
<td><span class="badge {{ item.status_badge }}">{{ item.status }}</span></td>
|
||||
<td>
|
||||
{% if item.status == "dropped" and item.dropped_reason %}
|
||||
<code>{{ item.dropped_reason }}</code>
|
||||
{% else %}
|
||||
<code>{{ item.reason }}</code>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item.attempt_count }}</td>
|
||||
<td>{{ item.next_attempt_at }}</td>
|
||||
<td>
|
||||
{% if item.last_error %}
|
||||
<details>
|
||||
<summary>show</summary>
|
||||
<pre>{{ item.last_error }}</pre>
|
||||
</details>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="inline-actions">
|
||||
<form method="post" action="/runs/queue/{{ item.id }}/retry">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" data-loading-text="Retrying...">Retry Now</button>
|
||||
</form>
|
||||
{% if item.status != "dropped" %}
|
||||
<form method="post" action="/runs/queue/{{ item.id }}/drop">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="danger-button" data-loading-text="Dropping...">Drop</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="post" action="/runs/queue/{{ item.id }}/clear">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="button-text" data-loading-text="Clearing...">Clear</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No queued continuation items.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
99
app/templates/scholars.html
Normal file
99
app/templates/scholars.html
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero" data-test="scholars-page">
|
||||
<p class="eyebrow">Scholars</p>
|
||||
<h1>Your Scholars</h1>
|
||||
<p class="lede">Add, disable, or remove profiles scoped to your account.</p>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="scholars-create-card">
|
||||
<h2>Add Scholar</h2>
|
||||
<form method="post" action="/scholars" class="stack-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label for="scholar_id">Scholar ID</label>
|
||||
<input
|
||||
id="scholar_id"
|
||||
name="scholar_id"
|
||||
type="text"
|
||||
value="{{ form_scholar_id | default('') }}"
|
||||
placeholder="abcDEF123456"
|
||||
pattern="[a-zA-Z0-9_-]{12}"
|
||||
required
|
||||
>
|
||||
<label for="display_name">Display Name (Optional)</label>
|
||||
<input
|
||||
id="display_name"
|
||||
name="display_name"
|
||||
type="text"
|
||||
value="{{ form_display_name | default('') }}"
|
||||
placeholder="Ada Lovelace"
|
||||
>
|
||||
<button type="submit" data-loading-text="Adding...">Add Scholar</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="scholars-list-card">
|
||||
<h2>Tracked Scholars</h2>
|
||||
{% if scholars %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Scholar ID</th>
|
||||
<th>Status</th>
|
||||
<th>Last Run</th>
|
||||
<th>Publications</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for scholar in scholars %}
|
||||
<tr>
|
||||
<td>{{ scholar.display_name }}</td>
|
||||
<td><code>{{ scholar.scholar_id }}</code></td>
|
||||
<td>
|
||||
{% if scholar.is_enabled %}
|
||||
<span class="badge ok">enabled</span>
|
||||
{% else %}
|
||||
<span class="badge warn">disabled</span>
|
||||
{% endif %}
|
||||
{% if scholar.baseline_completed %}
|
||||
<p class="helper-text">baseline complete</p>
|
||||
{% else %}
|
||||
<p class="helper-text">awaiting first run</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge {% if scholar.last_run_status == 'success' %}ok{% elif scholar.last_run_status in ['failed', 'partial_failure'] %}danger{% else %}warn{% endif %}">
|
||||
{{ scholar.last_run_status }}
|
||||
</span>
|
||||
<p class="helper-text">{{ scholar.last_run_dt }}</p>
|
||||
</td>
|
||||
<td>
|
||||
<div class="inline-actions">
|
||||
<a class="link-button" href="/publications?mode=new&scholar_profile_id={{ scholar.id }}">New</a>
|
||||
<a class="link-button" href="/publications?mode=all&scholar_profile_id={{ scholar.id }}">All</a>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="inline-actions">
|
||||
<form method="post" action="/scholars/{{ scholar.id }}/toggle">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" data-loading-text="Saving...">{% if scholar.is_enabled %}Disable{% else %}Enable{% endif %}</button>
|
||||
</form>
|
||||
<form method="post" action="/scholars/{{ scholar.id }}/delete">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="danger-button" data-loading-text="Deleting...">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No scholars tracked yet.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
44
app/templates/settings.html
Normal file
44
app/templates/settings.html
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero" data-test="settings-page">
|
||||
<p class="eyebrow">Settings</p>
|
||||
<h1>Your Settings</h1>
|
||||
<p class="lede">Control automation cadence and request pacing for your own account.</p>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="settings-form-card">
|
||||
<h2>Automation Settings</h2>
|
||||
<form method="post" action="/settings" class="stack-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label class="checkbox-row" for="auto_run_enabled">
|
||||
<input id="auto_run_enabled" name="auto_run_enabled" type="checkbox" {% if user_settings.auto_run_enabled %}checked{% endif %}>
|
||||
<span>Enable automatic runs</span>
|
||||
</label>
|
||||
|
||||
<label for="run_interval_minutes">Run Interval (minutes)</label>
|
||||
<input
|
||||
id="run_interval_minutes"
|
||||
name="run_interval_minutes"
|
||||
type="number"
|
||||
min="15"
|
||||
step="1"
|
||||
value="{{ user_settings.run_interval_minutes }}"
|
||||
required
|
||||
>
|
||||
|
||||
<label for="request_delay_seconds">Request Delay (seconds)</label>
|
||||
<input
|
||||
id="request_delay_seconds"
|
||||
name="request_delay_seconds"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
value="{{ user_settings.request_delay_seconds }}"
|
||||
required
|
||||
>
|
||||
|
||||
<button type="submit" data-loading-text="Saving...">Save Settings</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
75
app/templates/users.html
Normal file
75
app/templates/users.html
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero" data-test="users-page">
|
||||
<p class="eyebrow">Admin</p>
|
||||
<h1>User Management</h1>
|
||||
<p class="lede">Admin-only controls for account creation, activation, and password resets.</p>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="users-create-card">
|
||||
<h2>Create User</h2>
|
||||
<form method="post" action="/users" class="stack-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<label for="email">Email</label>
|
||||
<input id="email" name="email" type="email" value="{{ form_email | default('') }}" required>
|
||||
<label for="password">Temporary Password</label>
|
||||
<input id="password" name="password" type="password" required>
|
||||
<label class="checkbox-row" for="is_admin">
|
||||
<input id="is_admin" name="is_admin" type="checkbox" {% if form_is_admin %}checked{% endif %}>
|
||||
<span>Admin account</span>
|
||||
</label>
|
||||
<button type="submit" data-loading-text="Creating...">Create User</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="sandbox" data-test="users-list-card">
|
||||
<h2>Users</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
{% if user.is_admin %}
|
||||
<span class="badge ok">admin</span>
|
||||
{% else %}
|
||||
<span class="badge">user</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
<span class="badge ok">active</span>
|
||||
{% else %}
|
||||
<span class="badge warn">inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="inline-actions">
|
||||
<form method="post" action="/users/{{ user.id }}/toggle-active">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" data-loading-text="Saving..." {% if user.id == current_user_id and user.is_active %}disabled{% endif %}>
|
||||
{% if user.is_active %}Deactivate{% else %}Activate{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="/users/{{ user.id }}/reset-password" class="inline-reset-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input name="new_password" type="password" placeholder="New password" minlength="8" required>
|
||||
<button type="submit" data-loading-text="Resetting...">Reset Password</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue