better mobile desktop sync
This commit is contained in:
68
app.py
68
app.py
@@ -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')
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
Reference in New Issue
Block a user