341 lines
13 KiB
Django/Jinja

{% extends "base.html.jinja" %}
{% block title %}Control Panel{% endblock title %}
{% block styles %}
{{ super() }}
<style>
.status-indicator {
width: 15px;
height: 15px;
border-radius: 50%;
display: inline-block;
margin-right: 5px;
}
.status-active {
background-color: #28a745;
}
.status-paused {
background-color: #ffc107;
}
.status-inactive {
background-color: #dc3545;
}
.stats-chart {
height: 400px;
}
.chart-wrapper {
position: relative;
height: 400px !important;
width: 100%;
overflow: hidden;
}
.chart-wrapper canvas {
height: 400px !important;
}
.scraper-chart-wrapper {
position: relative;
height: 150px !important;
width: 100%;
border-top: 1px solid #e0e0e0;
padding-top: 15px;
overflow: hidden;
}
.scraper-chart-wrapper canvas {
display: block;
width: 100%;
}
.search-results-container {
max-height: 300px;
overflow-y: auto;
}
/* Paper status badges */
.badge-new {
background-color: #17a2b8;
}
.badge-pending {
background-color: #ffc107;
}
.badge-done {
background-color: #28a745;
}
.badge-failed {
background-color: #dc3545;
}
.activity-controls {
width: auto;
display: inline-block;
}
</style>
{% endblock styles %}
{% block content %}
<div class="container mt-4">
<h1>Control Panel</h1>
<!-- Include standardized flash messages -->
{% include "partials/flash_messages.html.jinja" %}
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>Scraper Status</h5>
</div>
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div id="statusIndicator" class="status-indicator status-inactive"></div>
<span id="statusText">Inactive</span>
</div>
<div class="btn-group" role="group">
<button id="startButton" class="btn btn-success">Start</button>
<button id="pauseButton" class="btn btn-warning" disabled>Pause</button>
<button id="stopButton" class="btn btn-danger" disabled>Stop</button>
<button id="resetButton" class="btn btn-secondary" disabled>Reset</button>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5>Volume Configuration</h5>
</div>
<div class="card-body">
<form id="volumeForm">
<div class="form-group">
<label for="volumeInput">Papers per day:</label>
<input type="number" class="form-control" id="volumeInput"
value="{{ volume_config if volume_config else 100 }}" min="1" max="{{ max_volume }}">
<button type="submit" class="btn btn-primary mt-2">
<i class="fas fa-save"></i> Update Volume
</button>
</div>
<div class="form-text">Enter a value between 1 and {{ max_volume }}</div>
</form>
</div>
</div>
</div>
</div>
<!-- New row for single paper processing -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5>Process Single Paper</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<form id="searchPaperForm" class="mb-3">
<label for="paperSearchInput">Search paper:</label>
<div class="input-group">
<input type="text" id="paperSearchInput" class="form-control"
placeholder="By title, DOI, or ID...">
<button class="btn btn-outline-secondary" type="submit">Search</button>
</div>
</form>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="scraperSelect">Scraper Module:</label>
<select class="form-control" id="scraperSelect">
<option value="">Use default system scraper</option>
<!-- Available scrapers will be populated here -->
</select>
<div class="form-text">
Select which scraper to use for processing the paper
</div>
</div>
</div>
</div>
<div id="searchResults" class="mt-3 search-results-container d-none">
<table class="table table-hover table-striped">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>DOI</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="paperSearchResults">
<!-- Search results will be populated here -->
</tbody>
</table>
</div>
<div id="processingStatus" class="alert alert-info mt-3 d-none"></div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Scraping Activity</h5>
<div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="notificationsToggle" checked>
<label class="form-check-label" for="notificationsToggle">Show Notifications</label>
</div>
</div>
</div>
<div class="card-body">
<div class="btn-group mb-3">
<button class="btn btn-outline-secondary time-range-btn" data-hours="6">Last 6
hours</button>
<button class="btn btn-outline-secondary time-range-btn active" data-hours="24">Last 24
hours</button>
<button class="btn btn-outline-secondary time-range-btn" data-hours="72">Last 3
days</button>
</div>
<!-- Scraper State Legend - will be populated by JavaScript -->
<div id="scraper-state-legend" class="scraper-state-legend mb-3 d-none">
<small class="text-muted">Scraper Status Timeline:</small><br>
<span class="badge bg-secondary">Loading state information...</span>
</div>
<div class="chart-wrapper">
<canvas id="activityChart"></canvas>
</div>
<!-- Scraper Activity Timeline Chart -->
<div class="scraper-chart-wrapper">
<h6 class="text-muted mb-2">
<i class="fas fa-power-off"></i> Scraper Activity Timeline
</h6>
<canvas id="scraperActivityChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Recent Activity</h5>
<div class="d-flex align-items-center gap-3">
<div class="form-group mb-0">
<label for="activityPageSize" class="form-label mb-0 me-2">Show:</label>
<select id="activityPageSize" class="form-select form-select-sm activity-controls">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div class="form-group mb-0">
<label for="activityStatusFilter" class="form-label mb-0 me-2">Status:</label>
<select id="activityStatusFilter" class="form-select form-select-sm activity-controls">
<option value="">All</option>
<option value="success">Success</option>
<option value="error">Error</option>
<option value="pending">Pending</option>
<option value="info">Info</option>
</select>
</div>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Time</th>
<th>Action</th>
<th>Status</th>
<th>Description</th>
</tr>
</thead>
<tbody id="activityLog">
<tr>
<td colspan="4" class="text-center">Loading activities...</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination Controls -->
<nav id="activityPagination" aria-label="Activity log pagination" class="mt-3 d-none">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted">
<span id="activityPaginationInfo">Showing 0 - 0 of 0 entries</span>
</div>
<ul class="pagination pagination-sm mb-0">
<li class="page-item" id="activityPrevPage">
<a class="page-link" href="#" aria-label="Previous">
<span aria-hidden="true">«</span>
</a>
</li>
<li class="page-item active" id="activityCurrentPage">
<span class="page-link">1</span>
</li>
<li class="page-item" id="activityNextPage">
<a class="page-link" href="#" aria-label="Next">
<span aria-hidden="true">»</span>
</a>
</li>
</ul>
</div>
</nav>
</div>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block scripts %}
{{ super() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<!-- Modular JavaScript files -->
<script src="{{ url_for('static', filename='js/chart.js') }}"></script>
<script src="{{ url_for('static', filename='js/scraper-control.js') }}"></script>
<script src="{{ url_for('static', filename='js/paper-processor.js') }}"></script>
<script src="{{ url_for('static', filename='js/activity-monitor.js') }}"></script>
<script src="{{ url_for('static', filename='js/scraper-dashboard.js') }}"></script>
<script id="scraper-config" type="application/json">
{
"maxVolume": {{ max_volume }},
"volumeConfig": {{ volume_config if volume_config else 100 }}
}
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const rawConfig = document.getElementById('scraper-config').textContent;
const dashboardConfig = JSON.parse(rawConfig);
window.scraperDashboard = initScraperDashboard(dashboardConfig);
});
</script>
{% endblock scripts %}