Initial commit

This commit is contained in:
2026-05-09 18:41:03 +02:00
commit d384f1f896
18 changed files with 1339 additions and 0 deletions
+21
View File
@@ -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>
+46
View File
@@ -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 }}">&laquo; Prev</a>
{% endif %}
<span>Page {{ page }} of {{ pages }}</span>
{% if page < pages %}
<a href="/books?page={{ page + 1 }}">Next &raquo;</a>
{% endif %}
</div>
{% endif %}
{% else %}
<p class="muted">No books recorded yet.</p>
{% endif %}
{% endblock %}
+115
View File
@@ -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 %} &mdash; 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 %}
+131
View File
@@ -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 %}