show currrently running activity in logbook
This commit is contained in:
29
app.py
29
app.py
@@ -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']
|
||||||
duration = entry['end_time'] - entry['start_time']
|
|
||||||
entry['duration_str'] = str(duration).split('.')[0] # HH:MM:SS
|
if entry.get('end_time'):
|
||||||
|
duration = entry['end_time'] - entry['start_time']
|
||||||
|
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)}))
|
||||||
|
|||||||
@@ -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;">
|
||||||
{{ entry.duration_str }}
|
<!-- 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 }}
|
||||||
|
</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');
|
||||||
|
|||||||
@@ -1,108 +1,184 @@
|
|||||||
{% 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 style="display: flex; justify-content: space-between; align-items: start;">
|
<div id="detail-content">
|
||||||
<div>
|
<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 %}">
|
||||||
<h2>
|
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||||
{{ entry.activity.name }}
|
<div>
|
||||||
{% if entry.subcategory %}
|
<h2>
|
||||||
<span style="font-size: 0.8rem; background: #eee; padding: 2px 8px; border-radius: 10px; color: #555; vertical-align: middle;">
|
{{ entry.activity.name }}
|
||||||
{{ entry.subcategory }}
|
{% if entry.subcategory %}
|
||||||
</span>
|
<span style="font-size: 0.8rem; background: #eee; padding: 2px 8px; border-radius: 10px; color: #555; vertical-align: middle;">
|
||||||
|
{{ entry.subcategory }}
|
||||||
|
</span>
|
||||||
|
{% 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>
|
||||||
|
<p style="color: #666;">
|
||||||
|
{{ entry.start_time.strftime('%A, %d. %B %Y') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: right;">
|
||||||
|
<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 %}
|
{% endif %}
|
||||||
</h2>
|
<br>
|
||||||
<p style="color: #666;">
|
<a href="{{ url_for('logbook') }}" class="btn" style="background: #95a5a6; font-size: 0.8rem; margin-top: 15px; display:inline-block;">Back to Log</a>
|
||||||
{{ entry.start_time.strftime('%A, %d. %B %Y') }}
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div style="text-align: right;">
|
|
||||||
<div style="font-weight: bold; font-size: 2rem; color: #333;">{{ entry.duration_str }}</div>
|
|
||||||
<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 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">
|
||||||
|
|
||||||
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
<div style="display: flex; gap: 1rem; margin-bottom: 1rem;">
|
||||||
<div style="flex: 1;">
|
<div style="flex: 1;">
|
||||||
<label>Start Time</label>
|
<label>Start Time</label>
|
||||||
<input type="datetime-local" name="start_time" value="{{ entry.start_time.strftime('%Y-%m-%dT%H:%M') }}" required>
|
<input type="datetime-local" name="start_time" value="{{ entry.start_time.strftime('%Y-%m-%dT%H:%M') }}" required>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<label>End Time</label>
|
||||||
|
<!-- 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 '' }}">
|
||||||
|
<small style="color: #666; display: block; margin-top: 5px;">Session ended. Modifying this changes duration.</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1;">
|
|
||||||
<label>End Time</label>
|
<!-- Subcategory Edit -->
|
||||||
<!-- If currently running (end_time is None), prevent editing end time or handle appropriately. Usually logbook is for past entries. -->
|
<label>Subcategory</label>
|
||||||
<input type="datetime-local" name="end_time" value="{{ entry.end_time.strftime('%Y-%m-%dT%H:%M') if entry.end_time else '' }}">
|
<select name="subcategory">
|
||||||
|
<option value="">-- None --</option>
|
||||||
|
{% if entry.activity and entry.activity.subcategories %}
|
||||||
|
{% for sub in entry.activity.subcategories %}
|
||||||
|
<option value="{{ sub }}" {% if sub == entry.subcategory %}selected{% endif %}>{{ sub }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Note</label>
|
||||||
|
<textarea name="note" rows="3" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-family: inherit;">{{ entry.note }}</textarea>
|
||||||
|
|
||||||
|
<div style="margin-top: 10px; display: flex; justify-content: space-between;">
|
||||||
|
<button type="submit" class="btn">Save Changes</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Subcategory Edit -->
|
|
||||||
<label>Subcategory</label>
|
|
||||||
<select name="subcategory">
|
|
||||||
<option value="">-- None --</option>
|
|
||||||
{% if entry.activity and entry.activity.subcategories %}
|
|
||||||
{% for sub in entry.activity.subcategories %}
|
|
||||||
<option value="{{ sub }}" {% if sub == entry.subcategory %}selected{% endif %}>{{ sub }}</option>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label>Note</label>
|
|
||||||
<textarea name="note" rows="3" style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-family: inherit;">{{ entry.note }}</textarea>
|
|
||||||
|
|
||||||
<div style="margin-top: 10px; display: flex; justify-content: space-between;">
|
|
||||||
<button type="submit" class="btn">Save Changes</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div style="border-top: 1px solid #eee; margin-top: 2rem; padding-top: 1rem;">
|
|
||||||
<form method="POST" onsubmit="return confirm('Are you sure you want to delete this entry? This action cannot be undone.');">
|
|
||||||
<input type="hidden" name="action" value="delete">
|
|
||||||
<button type="submit" class="btn btn-danger">Delete Entry</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div style="border-top: 1px solid #eee; margin-top: 2rem; padding-top: 1rem;">
|
||||||
|
<form method="POST" onsubmit="return confirm('Are you sure you want to delete this entry? This action cannot be undone.');">
|
||||||
|
<input type="hidden" name="action" value="delete">
|
||||||
|
<button type="submit" class="btn btn-danger">Delete Entry</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Tasks in this Session</h3>
|
||||||
|
{% if not tasks %}
|
||||||
|
<p style="color: var(--text-secondary);">No tasks were tracked for this session.</p>
|
||||||
|
{% else %}
|
||||||
|
<div class="checklist-container">
|
||||||
|
{% for task in tasks %}
|
||||||
|
<div class="checklist-item {% if task.status == 'completed' %}completed{% endif %}" style="padding: 12px 0;">
|
||||||
|
<!-- Visual Checkbox (Read Only) -->
|
||||||
|
<div style="
|
||||||
|
width: 18px; height: 18px;
|
||||||
|
border: 2px solid {% if task.status == 'completed' %}var(--primary-color){% else %}var(--text-secondary){% endif %};
|
||||||
|
border-radius: 4px;
|
||||||
|
background: {% if task.status == 'completed' %}var(--primary-color){% else %}transparent{% endif %};
|
||||||
|
margin-right: 12px; margin-top: 3px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
">
|
||||||
|
{% if task.status == 'completed' %}
|
||||||
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex-grow: 1;">
|
||||||
|
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="text-decoration: none; color: inherit; font-weight: 500; display: block;">
|
||||||
|
{{ task.name }}
|
||||||
|
</a>
|
||||||
|
{% if task.completed_at %}
|
||||||
|
<small style="color: var(--text-secondary); font-size: 0.8rem;">Completed at {{ task.completed_at.strftime('%H:%M') }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="color: var(--text-secondary); text-decoration: none; font-size: 1.2rem; line-height: 1;">
|
||||||
|
›
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<script>
|
||||||
<h3>Tasks in this Session</h3>
|
// Live Timer Update
|
||||||
{% if not tasks %}
|
setInterval(() => {
|
||||||
<p style="color: var(--text-secondary);">No tasks were tracked for this session.</p>
|
document.querySelectorAll('.live-timer').forEach(el => {
|
||||||
{% else %}
|
const start = parseInt(el.getAttribute('data-start'));
|
||||||
<div class="checklist-container">
|
if (!start) return;
|
||||||
{% for task in tasks %}
|
|
||||||
<div class="checklist-item {% if task.status == 'completed' %}completed{% endif %}" style="padding: 12px 0;">
|
|
||||||
<!-- Visual Checkbox (Read Only) -->
|
|
||||||
<div style="
|
|
||||||
width: 18px; height: 18px;
|
|
||||||
border: 2px solid {% if task.status == 'completed' %}var(--primary-color){% else %}var(--text-secondary){% endif %};
|
|
||||||
border-radius: 4px;
|
|
||||||
background: {% if task.status == 'completed' %}var(--primary-color){% else %}transparent{% endif %};
|
|
||||||
margin-right: 12px; margin-top: 3px;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
">
|
|
||||||
{% if task.status == 'completed' %}
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="flex-grow: 1;">
|
const diff = Date.now() - start;
|
||||||
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="text-decoration: none; color: inherit; font-weight: 500; display: block;">
|
if (diff < 0) return;
|
||||||
{{ task.name }}
|
|
||||||
</a>
|
|
||||||
{% if task.completed_at %}
|
|
||||||
<small style="color: var(--text-secondary); font-size: 0.8rem;">Completed at {{ task.completed_at.strftime('%H:%M') }}</small>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="{{ url_for('task_detail', task_id=task._id) }}" style="color: var(--text-secondary); text-decoration: none; font-size: 1.2rem; line-height: 1;">
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||||
›
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
</a>
|
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||||
</div>
|
|
||||||
{% endfor %}
|
let timeStr = (hours < 10 ? "0" + hours : hours) + ":" +
|
||||||
</div>
|
(minutes < 10 ? "0" + minutes : minutes) + ":" +
|
||||||
{% endif %}
|
(seconds < 10 ? "0" + seconds : seconds);
|
||||||
</div>
|
|
||||||
|
// 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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user