Initial commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}CalibreSync{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/" class="brand">CalibreSync</a>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/books">Books</a>
|
||||
<a href="/settings">Settings</a>
|
||||
</nav>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer>CalibreSync — <a href="/docs" target="_blank">API docs</a></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Books — CalibreSync{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Books <span class="muted">({{ total }})</span></h1>
|
||||
</div>
|
||||
|
||||
{% if books %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Status</th>
|
||||
<th>Source zip</th>
|
||||
<th>Uploaded</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in books %}
|
||||
<tr>
|
||||
<td>{{ b.filename }}</td>
|
||||
<td><span class="badge badge-{{ b.status }}">{{ b.status }}</span></td>
|
||||
<td class="mono small muted">{{ b.zip_source or "—" }}</td>
|
||||
<td>{{ b.uploaded_at[:19].replace("T"," ") if b.uploaded_at else "—" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if pages > 1 %}
|
||||
<div class="pagination">
|
||||
{% if page > 1 %}
|
||||
<a href="/books?page={{ page - 1 }}">« Prev</a>
|
||||
{% endif %}
|
||||
<span>Page {{ page }} of {{ pages }}</span>
|
||||
{% if page < pages %}
|
||||
<a href="/books?page={{ page + 1 }}">Next »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
<p class="muted">No books recorded yet.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,115 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard — CalibreSync{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="page-header">
|
||||
<h1>Dashboard</h1>
|
||||
<div class="header-actions">
|
||||
{% if interval > 0 %}
|
||||
<span class="schedule-badge">
|
||||
Auto every {{ interval }}m
|
||||
{% if next_run %} — next: {{ next_run }}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
<form method="post" action="/sync" style="display:inline">
|
||||
{% if sync_running %}
|
||||
<button class="btn btn-disabled" disabled>Sync running…</button>
|
||||
{% else %}
|
||||
<button class="btn btn-primary">Run Sync Now</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.query_params.get("started") %}
|
||||
<div class="alert alert-success">Sync started in background.</div>
|
||||
{% endif %}
|
||||
{% if request.query_params.get("already_running") %}
|
||||
<div class="alert alert-warning">A sync is already running.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.total_zips }}</div>
|
||||
<div class="stat-label">Zip archives processed</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.uploaded }}</div>
|
||||
<div class="stat-label">Books uploaded</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.skipped }}</div>
|
||||
<div class="stat-label">Duplicates skipped</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.total_books }}</div>
|
||||
<div class="stat-label">Total book records</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Recent sync runs</h2>
|
||||
{% if runs %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Started</th>
|
||||
<th>Finished</th>
|
||||
<th>Status</th>
|
||||
<th>New zips</th>
|
||||
<th>Uploaded</th>
|
||||
<th>Skipped</th>
|
||||
<th>Errors</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in runs %}
|
||||
<tr>
|
||||
<td>{{ r.started_at[:19].replace("T"," ") }}</td>
|
||||
<td>{{ r.finished_at[:19].replace("T"," ") if r.finished_at else "—" }}</td>
|
||||
<td><span class="badge badge-{{ r.status }}">{{ r.status }}</span></td>
|
||||
<td>{{ r.zips_new }}</td>
|
||||
<td>{{ r.books_uploaded }}</td>
|
||||
<td>{{ r.books_skipped }}</td>
|
||||
<td>{{ r.books_errored }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">No sync runs yet. Click "Run Sync" to start.</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>Recent zip archives</h2>
|
||||
{% if zips %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Remote path</th>
|
||||
<th>Size</th>
|
||||
<th>Processed</th>
|
||||
<th>Status</th>
|
||||
<th>Error</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for z in zips %}
|
||||
<tr>
|
||||
<td class="mono">{{ z.remote_path }}</td>
|
||||
<td>{{ (z.file_size / 1048576) | round(1) }} MB</td>
|
||||
<td>{{ z.processed_at[:19].replace("T"," ") if z.processed_at else "—" }}</td>
|
||||
<td><span class="badge badge-{{ z.status }}">{{ z.status }}</span></td>
|
||||
<td class="muted small">{{ z.error_msg or "" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="muted">No zip archives processed yet.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if sync_running %}
|
||||
<script>
|
||||
setTimeout(() => location.reload(), 5000);
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,131 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Settings — CalibreSync{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Settings</h1>
|
||||
|
||||
{% if request.query_params.get("saved") %}
|
||||
<div class="alert alert-success">Settings saved.</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/settings">
|
||||
|
||||
<section class="form-section">
|
||||
<h2>Remote host (SFTP)</h2>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="sftp_host">Host</label>
|
||||
<input id="sftp_host" name="sftp_host" type="text" placeholder="192.168.1.10"
|
||||
value="{{ s.get('sftp_host','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="sftp_port">Port</label>
|
||||
<input id="sftp_port" name="sftp_port" type="number" value="{{ s.get('sftp_port','22') }}" style="width:6rem">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="sftp_user">Username</label>
|
||||
<input id="sftp_user" name="sftp_user" type="text" value="{{ s.get('sftp_user','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="sftp_remote_path">Remote zip directory</label>
|
||||
<input id="sftp_remote_path" name="sftp_remote_path" type="text" placeholder="/mnt/media/zips"
|
||||
value="{{ s.get('sftp_remote_path','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Authentication</label>
|
||||
<div class="radio-group">
|
||||
<label>
|
||||
<input type="radio" name="sftp_auth_method" value="key"
|
||||
{% if s.get('sftp_auth_method','key') == 'key' %}checked{% endif %}
|
||||
onchange="toggleAuth(this.value)">
|
||||
SSH key
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="sftp_auth_method" value="password"
|
||||
{% if s.get('sftp_auth_method') == 'password' %}checked{% endif %}
|
||||
onchange="toggleAuth(this.value)">
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row" id="row-key">
|
||||
<label for="sftp_key">Private key (PEM)</label>
|
||||
<textarea id="sftp_key" name="sftp_key" rows="8" placeholder="-----BEGIN RSA PRIVATE KEY-----
|
||||
...
|
||||
-----END RSA PRIVATE KEY-----"></textarea>
|
||||
{% if has_key %}
|
||||
<p class="muted small">A key is already configured. Paste a new one above to replace it, or leave blank to keep the existing key.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-row" id="row-password" style="display:none">
|
||||
<label for="sftp_password">Password</label>
|
||||
<input id="sftp_password" name="sftp_password" type="password"
|
||||
value="{{ s.get('sftp_password','') }}">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<h2>Calibre-Web</h2>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="calibre_url">URL</label>
|
||||
<input id="calibre_url" name="calibre_url" type="url" placeholder="http://localhost:8083"
|
||||
value="{{ s.get('calibre_url','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="calibre_user">Username</label>
|
||||
<input id="calibre_user" name="calibre_user" type="text" value="{{ s.get('calibre_user','') }}">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="calibre_pass">Password</label>
|
||||
<input id="calibre_pass" name="calibre_pass" type="password"
|
||||
value="{{ s.get('calibre_pass','') }}">
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<h2>Local</h2>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="local_work_dir">Work directory</label>
|
||||
<input id="local_work_dir" name="local_work_dir" type="text" placeholder="/tmp/calibresync"
|
||||
value="{{ s.get('local_work_dir','/tmp/calibresync') }}">
|
||||
<p class="muted small">Temporary storage for downloaded zips and extracted files. Cleaned up after each run.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="form-section">
|
||||
<h2>Automatic sync schedule</h2>
|
||||
|
||||
<div class="form-row">
|
||||
<label for="scheduler_interval_minutes">Run every (minutes)</label>
|
||||
<input id="scheduler_interval_minutes" name="scheduler_interval_minutes" type="number"
|
||||
min="0" step="1" style="width:8rem"
|
||||
value="{{ s.get('scheduler_interval_minutes','0') }}"
|
||||
placeholder="0">
|
||||
<p class="muted small">Set to 0 to disable automatic sync. Changes take effect immediately on save. Examples: 60 = hourly, 1440 = daily.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save settings</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function toggleAuth(method) {
|
||||
document.getElementById("row-key").style.display = method === "key" ? "" : "none";
|
||||
document.getElementById("row-password").style.display = method === "password" ? "" : "none";
|
||||
}
|
||||
// Init on load
|
||||
toggleAuth(document.querySelector('[name=sftp_auth_method]:checked')?.value || "key");
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user