better mobile desktop sync

This commit is contained in:
2026-02-11 12:57:39 +01:00
parent 0e2ef4bde3
commit 1a570e0969
2 changed files with 104 additions and 27 deletions

68
app.py
View File

@@ -22,19 +22,8 @@ def get_user_id():
def is_logged_in(): def is_logged_in():
return 'user_id' in session return 'user_id' in session
# --- Routes --- # --- Logic Helpers ---
def get_dashboard_context(user_id):
@app.route('/')
def index():
# If not logged in, show the landing page instead of redirecting
if not is_logged_in():
return render_template('landing.html')
user_id = get_user_id()
# Get all activities
activities = list(db.activities.find({'user_id': user_id}))
# Check for active time entry # Check for active time entry
current_entry = db.time_entries.find_one({ current_entry = db.time_entries.find_one({
'user_id': user_id, 'user_id': user_id,
@@ -49,15 +38,14 @@ def index():
current_entry['activity_name'] = active_activity['name'] current_entry['activity_name'] = active_activity['name']
current_entry['activity_color'] = active_activity.get('color', '#3498db') current_entry['activity_color'] = active_activity.get('color', '#3498db')
# Logic for fetching tasks: # 1. Session Tasks
# 1. Fetch Session Specific Tasks (linked via time_entry_id)
session_tasks = list(db.tasks.find({ session_tasks = list(db.tasks.find({
'user_id': user_id, 'user_id': user_id,
'status': 'open', 'status': 'open',
'time_entry_id': current_entry['_id'] 'time_entry_id': current_entry['_id']
})) }))
# 2. Fetch Contextual Tasks (linked via activity_id, no time_entry_id) # 2. Context Tasks
context_criteria = { context_criteria = {
'user_id': user_id, 'user_id': user_id,
'status': 'open', 'status': 'open',
@@ -67,27 +55,56 @@ def index():
} }
current_subcat = current_entry.get('subcategory') current_subcat = current_entry.get('subcategory')
if current_subcat: if current_subcat:
# If active entry has subcategory: Show tasks matching that subcategory OR general tasks (no subcategory) context_criteria['$or'] = [{'subcategory': current_subcat}, {'subcategory': {'$in': [None, '']}}]
context_criteria['$or'] = [
{'subcategory': current_subcat},
{'subcategory': {'$in': [None, '']}}
]
else: else:
# If active entry has NO subcategory: Show only General tasks (no subcategory assigned)
# This hides specific subcategory tasks when just tracking the general activity
context_criteria['subcategory'] = {'$in': [None, '']} context_criteria['subcategory'] = {'$in': [None, '']}
context_tasks = list(db.tasks.find(context_criteria)) context_tasks = list(db.tasks.find(context_criteria))
active_tasks = session_tasks + context_tasks active_tasks = session_tasks + context_tasks
return current_entry, active_tasks
# --- Routes ---
@app.route('/')
def index():
# If not logged in, show the landing page instead of redirecting
if not is_logged_in():
return render_template('landing.html')
user_id = get_user_id()
# Get all activities
activities = list(db.activities.find({'user_id': user_id}))
# Use shared logic
current_entry, active_tasks = get_dashboard_context(user_id)
return render_template('dashboard.html', return render_template('dashboard.html',
activities=activities, activities=activities,
current_entry=current_entry, current_entry=current_entry,
tasks=active_tasks) tasks=active_tasks)
@app.route('/api/sync_check')
def sync_check():
if not is_logged_in(): return jsonify({'error': 'auth'}), 401
user_id = get_user_id()
current_entry, active_tasks = get_dashboard_context(user_id)
entry_id = str(current_entry['_id']) if current_entry else 'none'
# Create simple hash of tasks (IDs joined string)
# If a task is completed, it drops from this list -> hash changes -> client reloads
task_ids = sorted([str(t['_id']) for t in active_tasks])
tasks_hash = ','.join(task_ids)
return jsonify({
'entry_hash': entry_id,
'tasks_hash': tasks_hash
})
@app.route('/register', methods=['GET', 'POST']) @app.route('/register', methods=['GET', 'POST'])
def register(): def register():
if request.method == 'POST': if request.method == 'POST':
@@ -228,6 +245,7 @@ def start_timer_bg(activity_id):
return jsonify({ return jsonify({
'status': 'success', 'status': 'success',
'entry_id': str(new_entry_id),
'start_time': start_time.isoformat(), 'start_time': start_time.isoformat(),
'activity_name': activity['name'], 'activity_name': activity['name'],
'activity_color': activity.get('color', '#3498db') 'activity_color': activity.get('color', '#3498db')

View File

@@ -20,10 +20,12 @@
max-width: none !important; max-width: none !important;
} }
/* Fix container widths */ /* Fix container widths to be full width */
.main-content { .main-content {
text-align: center; text-align: center;
min-width: 0 !important; /* Override inline style */ width: 100% !important; /* Force full width */
flex: none !important; /* Disable flex scaling from desktop */
min-width: 0 !important;
} }
.main-content h2 { .main-content h2 {
margin-left: auto; margin-left: auto;
@@ -33,6 +35,7 @@
/* Layout Reordering: Sidebar (Active Timer) first */ /* Layout Reordering: Sidebar (Active Timer) first */
.dashboard-container { .dashboard-container {
flex-direction: column; flex-direction: column;
align-items: stretch !important; /* Force children to stretch full width */
} }
.sidebar-container { .sidebar-container {
order: -1; /* Visualize first */ order: -1; /* Visualize first */
@@ -259,6 +262,12 @@
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if(data.status === 'success') { if(data.status === 'success') {
// UPDATE LOCAL HASH TO PREVENT SYNC RELOAD
if (data.entry_id) {
localEntryHash = data.entry_id;
console.log("Local state updated intentionally. Reload prevented.");
}
// 2. Start Local Timer UI // 2. Start Local Timer UI
startTime = new Date(data.start_time).getTime(); startTime = new Date(data.start_time).getTime();
clearInterval(timerInterval); clearInterval(timerInterval);
@@ -365,4 +374,54 @@
</form> </form>
</div> </div>
</div> </div>
<!-- Live Sync Script -->
<script>
// Generate initial state hashes based on what Jinja rendered
const initialEntryHash = "{{ current_entry._id if current_entry else 'none' }}";
// Create comma-separated string of IDs. Since IDs are 24 chars, sorting shouldn't matter for Jinja vs Python if order preserved,
// but app.py sorts. Let's trust they match initially or accept one quick reload on first poll if order differs.
// Actually, dashboard displays in list order. Python sorted them for hash.
// Let's rely on the Python endpoint's logic. We just need to know if we need to reload.
// To avoid immediate reload, we can fetch the initial hash via API or just accept one reload.
// Better: let's store the raw IDs from jinja.
let localEntryHash = initialEntryHash;
// Construct local task hash manually to match Python's sorted logic
let taskIds = [
{% for t in tasks %}
"{{ t._id }}",
{% endfor %}
];
taskIds.sort();
let localTasksHash = taskIds.join(',');
// Polling function
setInterval(() => {
// Don't poll if page is hidden to save battery/data
if (document.hidden) return;
// Don't poll or reload if the Start Modal is open (User is interacting)
if (document.getElementById('startModal').classList.contains('show')) return;
fetch('/api/sync_check')
.then(response => {
if(response.status === 401) window.location.reload(); // Auth lost
return response.json();
})
.then(data => {
const serverEntryHash = data.entry_hash;
const serverTasksHash = data.tasks_hash;
// Compare
if (serverEntryHash !== localEntryHash || serverTasksHash !== localTasksHash) {
console.log("State changed remotely. Syncing...");
// Reload to reflect changes (Timer started/stopped on other device, or task completed)
window.location.reload();
}
})
.catch(err => console.error("Sync check failed", err));
}, 2000); // 2 seconds
</script>
{% endblock %} {% endblock %}