Add logbook entries, data export page, and JSON import

This commit is contained in:
2026-04-26 20:03:58 -05:00
parent 52d1105997
commit 3b2029d3b8
8 changed files with 877 additions and 6 deletions
+14
View File
@@ -334,6 +334,20 @@
{% block content %}{% endblock %}
</div>
<footer style="text-align:center;padding:1.25rem 1rem 1.5rem;margin-top:1rem;
border-top:1px solid var(--border);font-size:0.8rem;">
<a href="{{ url_for('export_page') }}"
style="color:var(--text-muted);text-decoration:none;"
onmouseover="this.style.textDecoration='underline'"
onmouseout="this.style.textDecoration='none'">Export data</a>
<span style="color:var(--text-muted);margin:0 0.5rem;">·</span>
<a href="{{ url_for('import_page') }}"
style="color:var(--text-muted);text-decoration:none;"
onmouseover="this.style.textDecoration='underline'"
onmouseout="this.style.textDecoration='none'">Import data</a>
</footer>
<div id="confirm-modal" role="dialog" aria-modal="true">
<div id="confirm-modal-box">
<p id="confirm-modal-msg"></p>
+32
View File
@@ -216,6 +216,38 @@
{% endif %}
</div>
<!-- Logbook -->
<div class="card">
<h2>Logbook</h2>
{% if logbook_entries %}
<div style="display:flex;flex-direction:column;gap:0.6rem;margin-bottom:1rem;">
{% for entry in logbook_entries %}
<div style="border-left:3px solid var(--border);padding:0.35rem 0.75rem;
display:flex;justify-content:space-between;align-items:flex-start;gap:0.5rem;">
<div>
<div class="text-muted" style="font-size:0.78rem;margin-bottom:0.15rem;">{{ entry.recorded_at }}</div>
<div style="white-space:pre-wrap;">{{ entry.body }}</div>
</div>
<form method="post"
action="{{ url_for('battery_logbook_delete', battery_id=battery.id, entry_id=entry.id) }}"
data-confirm="Delete this logbook entry?" data-confirm-ok="Delete">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted" style="margin-bottom:0.75rem;">No logbook entries yet.</p>
{% endif %}
<h3 style="font-size:1rem;margin:0.75rem 0 0.5rem;color:var(--text-h2);">Add Entry</h3>
<form method="post" action="{{ url_for('battery_logbook_add', battery_id=battery.id) }}">
<div class="form-group" style="margin-bottom:0.5rem;">
<textarea name="body" placeholder="Write a note…" rows="2" required style="min-height:60px;"></textarea>
</div>
<button class="btn btn-primary" type="submit">Add Entry</button>
</form>
</div>
<!-- Edit Details -->
<div class="card">
<h2>Edit Details</h2>
+31
View File
@@ -221,6 +221,37 @@ function addInstallRow() {
{% endif %}
</div>
<div class="card">
<h2>Logbook</h2>
{% if logbook_entries %}
<div style="display:flex;flex-direction:column;gap:0.6rem;margin-bottom:1rem;">
{% for entry in logbook_entries %}
<div style="border-left:3px solid var(--border);padding:0.35rem 0.75rem;
display:flex;justify-content:space-between;align-items:flex-start;gap:0.5rem;">
<div>
<div class="text-muted" style="font-size:0.78rem;margin-bottom:0.15rem;">{{ entry.recorded_at }}</div>
<div style="white-space:pre-wrap;">{{ entry.body }}</div>
</div>
<form method="post"
action="{{ url_for('device_logbook_delete', device_id=device.id, entry_id=entry.id) }}"
data-confirm="Delete this logbook entry?" data-confirm-ok="Delete">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted" style="margin-bottom:0.75rem;">No logbook entries yet.</p>
{% endif %}
<h3 style="font-size:1rem;margin:0.75rem 0 0.5rem;color:var(--text-h2);">Add Entry</h3>
<form method="post" action="{{ url_for('device_logbook_add', device_id=device.id) }}">
<div class="form-group" style="margin-bottom:0.5rem;">
<textarea name="body" placeholder="Write a note…" rows="2" required style="min-height:60px;"></textarea>
</div>
<button class="btn btn-primary" type="submit">Add Entry</button>
</form>
</div>
<div class="card">
<h2>Edit Device</h2>
<form method="post" action="{{ url_for('device_edit', device_id=device.id) }}">
+70
View File
@@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block title %}Export — Battery Tracker{% endblock %}
{% block content %}
<h1 style="margin-bottom:1.25rem;">Export Data</h1>
<div class="card" style="margin-bottom:1rem;">
<h2 style="margin-bottom:0.5rem;">Full Export</h2>
<p style="color:var(--text-muted,#6b7280);margin-bottom:1rem;">
Download your complete dataset — batteries, devices, charge logs, capacity tests, and percentage history.
</p>
<div style="display:flex;gap:0.75rem;flex-wrap:wrap;">
<a href="{{ url_for('export_csv_zip') }}" class="btn btn-primary">Download All CSVs (.zip)</a>
<a href="{{ url_for('export_json') }}" class="btn btn-secondary">Download JSON</a>
</div>
</div>
<div class="card">
<h2 style="margin-bottom:0.5rem;">Individual CSV Files</h2>
<p style="color:var(--text-muted,#6b7280);margin-bottom:1rem;">Download a single table at a time.</p>
<table style="border-collapse:collapse;width:100%;">
<tbody>
<tr>
<td style="padding:0.4rem 0.75rem 0.4rem 0;">
<a href="{{ url_for('export_batteries_csv') }}" class="btn btn-sm btn-secondary">batteries.csv</a>
</td>
<td style="padding:0.4rem 0;color:var(--text-muted,#6b7280);font-size:0.9rem;">
All batteries with device name
</td>
</tr>
<tr>
<td style="padding:0.4rem 0.75rem 0.4rem 0;">
<a href="{{ url_for('export_devices_csv') }}" class="btn btn-sm btn-secondary">devices.csv</a>
</td>
<td style="padding:0.4rem 0;color:var(--text-muted,#6b7280);font-size:0.9rem;">
All devices with installed battery count
</td>
</tr>
<tr>
<td style="padding:0.4rem 0.75rem 0.4rem 0;">
<a href="{{ url_for('export_charge_logs_csv') }}" class="btn btn-sm btn-secondary">charge-logs.csv</a>
</td>
<td style="padding:0.4rem 0;color:var(--text-muted,#6b7280);font-size:0.9rem;">
All charge log entries with battery label
</td>
</tr>
<tr>
<td style="padding:0.4rem 0.75rem 0.4rem 0;">
<a href="{{ url_for('export_capacity_tests_csv') }}" class="btn btn-sm btn-secondary">capacity-tests.csv</a>
</td>
<td style="padding:0.4rem 0;color:var(--text-muted,#6b7280);font-size:0.9rem;">
All capacity test results with battery label
</td>
</tr>
<tr>
<td style="padding:0.4rem 0.75rem 0.4rem 0;">
<a href="{{ url_for('export_pct_logs_csv') }}" class="btn btn-sm btn-secondary">pct-logs.csv</a>
</td>
<td style="padding:0.4rem 0;color:var(--text-muted,#6b7280);font-size:0.9rem;">
Battery percentage history
</td>
</tr>
</tbody>
</table>
</div>
<div style="margin-top:0.75rem;">
<a href="{{ url_for('import_page') }}" class="btn btn-secondary">Import JSON</a>
</div>
{% endblock %}
+73
View File
@@ -0,0 +1,73 @@
{% extends "base.html" %}
{% block title %}Import — Battery Tracker{% endblock %}
{% block content %}
<h1 style="margin-bottom:1.25rem;">Import Data</h1>
<div class="card" style="margin-bottom:1rem;">
<p style="color:var(--text-muted,#6b7280);margin-bottom:1rem;">
Upload a JSON file exported from Battery Tracker.
Devices are matched by name and batteries by label — existing records are skipped (not overwritten).
Charge logs, capacity tests, and percentage logs are always appended.
</p>
<form method="POST" action="{{ url_for('import_page') }}" enctype="multipart/form-data">
<div class="form-group">
<label for="file">JSON Export File</label>
<input type="file" id="file" name="file" accept=".json,application/json" required
style="width:100%;padding:0.45rem 0.65rem;border:1px solid var(--border-input,#d1d5db);
border-radius:4px;font-size:0.9rem;background:var(--bg-input,#fff);
color:var(--text-body,#111827);">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Import</button>
<a href="{{ url_for('export_page') }}" class="btn btn-secondary">Back to Export</a>
</div>
</form>
</div>
{% if results is not none %}
<div class="card">
<h2 style="margin-bottom:1rem;">Import Results</h2>
<table style="border-collapse:collapse;width:100%;max-width:480px;">
<thead>
<tr style="border-bottom:2px solid var(--border,#e5e7eb);">
<th style="text-align:left;padding:0.4rem 0.75rem 0.4rem 0;font-size:0.85rem;color:var(--text-muted,#6b7280);font-weight:600;">Category</th>
<th style="text-align:right;padding:0.4rem 0.75rem;font-size:0.85rem;color:var(--text-muted,#6b7280);font-weight:600;">Created / Appended</th>
<th style="text-align:right;padding:0.4rem 0;font-size:0.85rem;color:var(--text-muted,#6b7280);font-weight:600;">Skipped</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid var(--border,#e5e7eb);">
<td style="padding:0.45rem 0.75rem 0.45rem 0;">Devices</td>
<td style="text-align:right;padding:0.45rem 0.75rem;">{{ results.devices_created }}</td>
<td style="text-align:right;padding:0.45rem 0;">{{ results.devices_skipped }}</td>
</tr>
<tr style="border-bottom:1px solid var(--border,#e5e7eb);">
<td style="padding:0.45rem 0.75rem 0.45rem 0;">Batteries</td>
<td style="text-align:right;padding:0.45rem 0.75rem;">{{ results.batteries_created }}</td>
<td style="text-align:right;padding:0.45rem 0;">{{ results.batteries_skipped }}</td>
</tr>
<tr style="border-bottom:1px solid var(--border,#e5e7eb);">
<td style="padding:0.45rem 0.75rem 0.45rem 0;">Charge Logs</td>
<td style="text-align:right;padding:0.45rem 0.75rem;">{{ results.charge_logs_appended }}</td>
<td style="text-align:right;padding:0.45rem 0;">{{ results.charge_logs_skipped }}</td>
</tr>
<tr style="border-bottom:1px solid var(--border,#e5e7eb);">
<td style="padding:0.45rem 0.75rem 0.45rem 0;">Capacity Tests</td>
<td style="text-align:right;padding:0.45rem 0.75rem;">{{ results.capacity_tests_appended }}</td>
<td style="text-align:right;padding:0.45rem 0;">{{ results.capacity_tests_skipped }}</td>
</tr>
<tr>
<td style="padding:0.45rem 0.75rem 0.45rem 0;">% Logs</td>
<td style="text-align:right;padding:0.45rem 0.75rem;">{{ results.pct_logs_appended }}</td>
<td style="text-align:right;padding:0.45rem 0;">{{ results.pct_logs_skipped }}</td>
</tr>
</tbody>
</table>
<div style="margin-top:1.25rem;display:flex;gap:0.75rem;flex-wrap:wrap;">
<a href="{{ url_for('dashboard') }}" class="btn btn-primary">Go to Dashboard</a>
<a href="{{ url_for('import_page') }}" class="btn btn-secondary">Import Another File</a>
</div>
</div>
{% endif %}
{% endblock %}