Bring offline mode to full parity with the online dashboard

Fixes subtasks appearing flattened into the main task list (missing
parent filter), and adds everything else needed for full offline
functionality: tag chips/assignment/creation, full field editing
(description, due time, recurrence presets), time tracking with a
live-updating timer, and properly nested subtasks -- all queued
locally and synced via the existing /api/sync/ protocol.

The offline page now reuses the real dashboard's app-layout/header/
sidebar/detail-pane structure instead of a bespoke layout, with an
amber-tinted header and banner so it's unmistakable which mode you're
in. The detail panel mirrors _task_detail.html's fields and actions.

Also fixes a real backend gap this feature depends on: apply_conflict_data()
in sync/views.py silently no-op'd on time_entry conflicts. And computes
duration_seconds client-side on merge, since TimeEntrySyncSerializer
doesn't include it in the wire format at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Keith Smith
2026-09-04 23:45:43 -06:00
co-authored by Claude Sonnet 5
parent fc045a0c4b
commit a6f227e931
7 changed files with 701 additions and 122 deletions
+18
View File
@@ -474,3 +474,21 @@ def apply_conflict_data(entity_type, entity_id, data):
tag.save()
except Tag.DoesNotExist:
pass
elif entity_type == 'time_entry':
try:
entry = TimeEntry.objects.get(id=entity_id)
started_at = data.get('started_at', entry.started_at)
if isinstance(started_at, str):
started_at = parse_datetime(started_at)
ended_at = data.get('ended_at', entry.ended_at)
if ended_at and isinstance(ended_at, str):
ended_at = parse_datetime(ended_at)
entry.started_at = started_at
entry.ended_at = ended_at
entry.notes = data.get('notes', entry.notes)
entry.save()
except TimeEntry.DoesNotExist:
pass