show currrently running activity in logbook

This commit is contained in:
2026-02-11 13:08:01 +01:00
parent ac6095a899
commit a5a56923ab
3 changed files with 275 additions and 103 deletions

25
app.py
View File

@@ -518,9 +518,9 @@ def logbook():
total_time_display = "0h" total_time_display = "0h"
# --- Existing Logbook Logic --- # --- Existing Logbook Logic ---
# Agrregation to join activities and tasks # Updated: Remove the 'end_time': {'$ne': None} constraint
pipeline = [ pipeline = [
{'$match': {'user_id': user_id, 'end_time': {'$ne': None}}}, {'$match': {'user_id': user_id}},
{'$sort': {'start_time': -1}}, {'$sort': {'start_time': -1}},
{'$lookup': { {'$lookup': {
'from': 'activities', 'from': 'activities',
@@ -544,8 +544,18 @@ def logbook():
for entry in log: for entry in log:
# Filter tasks to only completed ones for display cleanly in list view # Filter tasks to only completed ones for display cleanly in list view
entry['tasks'] = [t for t in entry['tasks'] if t['status'] == 'completed'] entry['tasks'] = [t for t in entry['tasks'] if t['status'] == 'completed']
if entry.get('end_time'):
duration = entry['end_time'] - entry['start_time'] duration = entry['end_time'] - entry['start_time']
entry['duration_str'] = str(duration).split('.')[0] # HH:MM:SS entry['duration_str'] = str(duration).split('.')[0] # HH:MM:SS
entry['is_running'] = False
else:
# Handle running entry
now = datetime.now()
duration = now - entry['start_time']
# Format nicely removing microseconds
entry['duration_str'] = str(duration).split('.')[0]
entry['is_running'] = True
return render_template('logbook.html', log=log, chart_data=chart_data, current_range=time_range, total_time_display=total_time_display) return render_template('logbook.html', log=log, chart_data=chart_data, current_range=time_range, total_time_display=total_time_display)
@@ -578,6 +588,8 @@ def log_entry_detail(entry_id):
update_fields['start_time'] = datetime.strptime(start_str, '%Y-%m-%dT%H:%M') update_fields['start_time'] = datetime.strptime(start_str, '%Y-%m-%dT%H:%M')
if end_str: if end_str:
update_fields['end_time'] = datetime.strptime(end_str, '%Y-%m-%dT%H:%M') update_fields['end_time'] = datetime.strptime(end_str, '%Y-%m-%dT%H:%M')
# If end_time was empty in form (running), and user didn't set it, it stays None (running)
# or if it was running and user sets a time, it stops.
except ValueError: except ValueError:
flash('Invalid date format') flash('Invalid date format')
return redirect(url_for('log_entry_detail', entry_id=entry_id)) return redirect(url_for('log_entry_detail', entry_id=entry_id))
@@ -611,8 +623,15 @@ def log_entry_detail(entry_id):
if entry.get('end_time') and entry.get('start_time'): if entry.get('end_time') and entry.get('start_time'):
duration = entry['end_time'] - entry['start_time'] duration = entry['end_time'] - entry['start_time']
entry['duration_str'] = str(duration).split('.')[0] entry['duration_str'] = str(duration).split('.')[0]
entry['is_running'] = False
elif entry.get('start_time'):
# Calculate uptime for running entry
duration = datetime.now() - entry['start_time']
entry['duration_str'] = str(duration).split('.')[0] + " (Running)"
entry['is_running'] = True
else: else:
entry['duration_str'] = "Running..." entry['duration_str'] = "--:--"
entry['is_running'] = False
# Fetch ALL Tasks linked to this entry (completed or not) # Fetch ALL Tasks linked to this entry (completed or not)
tasks = list(db.tasks.find({'time_entry_id': ObjectId(entry_id)})) tasks = list(db.tasks.find({'time_entry_id': ObjectId(entry_id)}))

View File

@@ -21,7 +21,8 @@
{% for entry in log %} {% for entry in log %}
<!-- Wrapped whole card in a link to the detail view --> <!-- Wrapped whole card in a link to the detail view -->
<a href="{{ url_for('log_entry_detail', entry_id=entry._id) }}" class="log-entry-item" style="text-decoration: none; color: inherit; display: block;"> <a href="{{ url_for('log_entry_detail', entry_id=entry._id) }}" class="log-entry-item" style="text-decoration: none; color: inherit; display: block;">
<div class="card" style="border-left: 5px solid {{ entry.activity.color }}; transition: transform 0.1s;"> <!-- Highlight running entries with a different border style or shadow -->
<div class="card" style="border-left: 5px solid {{ entry.activity.color }}; transition: transform 0.1s; {% if entry.is_running %}background: #fffdf8; border: 1px solid #ffd700; border-left-width: 5px;{% endif %}">
<div style="display: flex; justify-content: space-between; align-items: center;"> <div style="display: flex; justify-content: space-between; align-items: center;">
<div> <div>
<h3 style="margin: 0;" class="entry-title"> <h3 style="margin: 0;" class="entry-title">
@@ -31,9 +32,19 @@
{{ entry.subcategory }} {{ entry.subcategory }}
</span> </span>
{% endif %} {% endif %}
{% if entry.is_running %}
<span style="font-size: 0.7rem; background: #27ae60; color: white; padding: 2px 6px; border-radius: 4px; vertical-align: middle; margin-left: 5px; text-transform: uppercase; letter-spacing: 0.5px;">
Running
</span>
{% endif %}
</h3> </h3>
<small style="color: #666;"> <small style="color: #666;">
{{ entry.start_time.strftime('%Y-%m-%d %H:%M') }} - {{ entry.end_time.strftime('%H:%M') }} {{ entry.start_time.strftime('%Y-%m-%d %H:%M') }} -
{% if entry.is_running %}
<span style="color: #27ae60; font-weight: bold;">Now</span>
{% else %}
{{ entry.end_time.strftime('%H:%M') }}
{% endif %}
</small> </small>
{% if entry.note %} {% if entry.note %}
<p class="entry-note" style="margin: 5px 0; font-style: italic; color: #555;">"{{ entry.note }}"</p> <p class="entry-note" style="margin: 5px 0; font-style: italic; color: #555;">"{{ entry.note }}"</p>
@@ -41,8 +52,14 @@
<span class="entry-note" style="display:none;"></span> <span class="entry-note" style="display:none;"></span>
{% endif %} {% endif %}
</div> </div>
<div style="font-weight: bold; font-size: 1.2rem;"> <div style="font-weight: bold; font-size: 1.2rem; text-align: right;">
<!-- Live ticking class and data attribute -->
<span class="{% if entry.is_running %}live-timer{% endif %}" {% if entry.is_running %}data-start="{{ entry.start_time.timestamp() * 1000 }}"{% endif %}>
{{ entry.duration_str }} {{ entry.duration_str }}
</span>
{% if entry.is_running %}
<div style="font-size: 0.7rem; color: #27ae60; animation: pulse 2s infinite;">● Live</div>
{% endif %}
</div> </div>
</div> </div>
@@ -94,7 +111,67 @@
<!-- Scripts --> <!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
</style>
<script> <script>
// Live Timer Update
setInterval(() => {
document.querySelectorAll('.live-timer').forEach(el => {
const start = parseInt(el.getAttribute('data-start'));
if (!start) return;
const diff = Date.now() - start;
if (diff < 0) return;
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
el.textContent =
(hours < 10 ? "0" + hours : hours) + ":" +
(minutes < 10 ? "0" + minutes : minutes) + ":" +
(seconds < 10 ? "0" + seconds : seconds);
});
}, 1000);
// Background Sync (Polls for new tasks or status changes)
let lastContentHash = "";
setInterval(() => {
if (document.hidden) return;
// Fetch current page in background to check for updates
fetch(window.location.href)
.then(res => res.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newContainer = doc.getElementById('logbook-container');
const oldContainer = document.getElementById('logbook-container');
if (newContainer && oldContainer) {
const newContent = newContainer.innerHTML;
const oldContent = oldContainer.innerHTML;
// Simple string comparison. If different, replace.
// Note: This replaces the list if *anything* changed (time update server side, new task, stop timer)
if (newContent !== lastContentHash && lastContentHash !== "") {
// Only update DOM if logic changed, ignoring simple server side time drift (handled by JS above)
// But since server renders static time in loop, it will always differ slightly.
// However, we want to sync Structure (new tasks, status).
// Let's replace. The user wants "live sync".
oldContainer.innerHTML = newContent;
}
lastContentHash = newContent;
}
})
.catch(e => console.error("Logbook background sync failed", e));
}, 4000); // Check every 4 seconds
// Search Functionality // Search Functionality
function filterLog() { function filterLog() {
const input = document.getElementById('logSearchInput'); const input = document.getElementById('logSearchInput');

View File

@@ -1,6 +1,8 @@
{% extends "layout.html" %} {% extends "layout.html" %}
{% block content %} {% block content %}
<div class="card" style="border-left: 5px solid {{ entry.activity.color }};"> <!-- ID wrapper for replacement -->
<div id="detail-content">
<div class="card" style="border-left: 5px solid {{ entry.activity.color }}; {% if entry.is_running %}border: 1px solid #ffd700; border-left-width: 5px; background: #fffdf8;{% endif %}">
<div style="display: flex; justify-content: space-between; align-items: start;"> <div style="display: flex; justify-content: space-between; align-items: start;">
<div> <div>
<h2> <h2>
@@ -10,19 +12,34 @@
{{ entry.subcategory }} {{ entry.subcategory }}
</span> </span>
{% endif %} {% endif %}
{% if entry.is_running %}
<span style="font-size: 0.8rem; background: #27ae60; padding: 2px 8px; border-radius: 4px; color: white; vertical-align: middle; margin-left: 10px;">
RUNNING
</span>
{% endif %}
</h2> </h2>
<p style="color: #666;"> <p style="color: #666;">
{{ entry.start_time.strftime('%A, %d. %B %Y') }} {{ entry.start_time.strftime('%A, %d. %B %Y') }}
</p> </p>
</div> </div>
<div style="text-align: right;"> <div style="text-align: right;">
<div style="font-weight: bold; font-size: 2rem; color: #333;">{{ entry.duration_str }}</div> <div style="font-weight: bold; font-size: 2rem; color: #333;">
<span class="{% if entry.is_running %}live-timer{% endif %}" {% if entry.is_running %}data-start="{{ entry.start_time.timestamp() * 1000 }}"{% endif %}>
{{ entry.duration_str }}
</span>
</div>
{% if entry.is_running %}
<form action="{{ url_for('stop_timer') }}" method="POST" style="display: inline-block;">
<button type="submit" class="btn btn-danger" style="font-size: 0.8rem; margin-top: 5px;">Stop Timer</button>
</form>
{% endif %}
<br>
<a href="{{ url_for('logbook') }}" class="btn" style="background: #95a5a6; font-size: 0.8rem; margin-top: 15px; display:inline-block;">Back to Log</a> <a href="{{ url_for('logbook') }}" class="btn" style="background: #95a5a6; font-size: 0.8rem; margin-top: 15px; display:inline-block;">Back to Log</a>
</div> </div>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<h3>Edit Entry</h3> <h3>Edit Entry</h3>
<form method="POST"> <form method="POST">
<input type="hidden" name="action" value="update"> <input type="hidden" name="action" value="update">
@@ -34,8 +51,14 @@
</div> </div>
<div style="flex: 1;"> <div style="flex: 1;">
<label>End Time</label> <label>End Time</label>
<!-- If currently running (end_time is None), prevent editing end time or handle appropriately. Usually logbook is for past entries. --> <!-- If currently running, leave blank or show placeholder -->
{% if entry.is_running %}
<input type="datetime-local" name="end_time" placeholder="Currently Running (Leave empty)" title="Set a time to stop the timer manually">
<small style="color: #666; display: block; margin-top: 5px;">Timer is active. Select a date to stop it retroactively.</small>
{% else %}
<input type="datetime-local" name="end_time" value="{{ entry.end_time.strftime('%Y-%m-%dT%H:%M') if entry.end_time else '' }}"> <input type="datetime-local" name="end_time" value="{{ entry.end_time.strftime('%Y-%m-%dT%H:%M') if entry.end_time else '' }}">
<small style="color: #666; display: block; margin-top: 5px;">Session ended. Modifying this changes duration.</small>
{% endif %}
</div> </div>
</div> </div>
@@ -64,9 +87,9 @@
<button type="submit" class="btn btn-danger">Delete Entry</button> <button type="submit" class="btn btn-danger">Delete Entry</button>
</form> </form>
</div> </div>
</div> </div>
<div class="card"> <div class="card">
<h3>Tasks in this Session</h3> <h3>Tasks in this Session</h3>
{% if not tasks %} {% if not tasks %}
<p style="color: var(--text-secondary);">No tasks were tracked for this session.</p> <p style="color: var(--text-secondary);">No tasks were tracked for this session.</p>
@@ -104,5 +127,58 @@
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
</div>
</div> </div>
<script>
// Live Timer Update
setInterval(() => {
document.querySelectorAll('.live-timer').forEach(el => {
const start = parseInt(el.getAttribute('data-start'));
if (!start) return;
const diff = Date.now() - start;
if (diff < 0) return;
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
let timeStr = (hours < 10 ? "0" + hours : hours) + ":" +
(minutes < 10 ? "0" + minutes : minutes) + ":" +
(seconds < 10 ? "0" + seconds : seconds);
// Only update the time part, keep (Running) suffix if handled elsewhere or replace
el.textContent = timeStr;
});
}, 1000);
// Background Sync Detail View
let lastDetailHash = "";
setInterval(() => {
if (document.hidden) return;
// Skip if user is focusing an input (editing)
if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;
fetch(window.location.href)
.then(res => res.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newContentWrapper = doc.getElementById('detail-content');
const oldContentWrapper = document.getElementById('detail-content');
if (newContentWrapper && oldContentWrapper) {
const newContent = newContentWrapper.innerHTML;
if (newContent !== lastDetailHash && lastDetailHash !== "") {
oldContentWrapper.innerHTML = newContent;
}
lastDetailHash = newContent;
}
})
.catch(e => console.error("Detail sync failed", e));
}, 4000); // Check every 4 seconds
</script>
{% endblock %} {% endblock %}