After saveTask or setTaskStatus triggers loadTasks(), the side panel
now refreshes from the updated server data. If the selected task no
longer exists (deleted or goal changed), the side panel closes.
This ensures Edit Task always matches selectedTaskId.
selectTask() now also sets in-focus class and calls scrollToTask(),
making click behavior consistent with scroll-to-focus behavior.
Removed selectedTaskId assignment in loadTasks since selectTask
already sets it.
- Save button starts disabled and only enables when title or description
differs from the original task values
- updateSaveButton() compares current input values against task data
- input event listeners on title and desc fields call updateSaveButton
- Add global button:disabled style (opacity 0.5, cursor not-allowed)
- Status buttons now form a seamless button group (no gaps, shared borders,
rounded ends) with flex-wrap: nowrap to keep them in one row
- Side panel width increased from 350px to 400px for more content space
Replace <select> in side panel with 4 toggle buttons (To Do / Doing /
Pending / Done). Clicking a button immediately sends the PATCH status
API call. Active button is highlighted with status-specific colors and
shadow. saveTask now only handles title/description changes.
Without position: relative, .task-item offsetParent is the body element,
causing offsetTop to be measured from document root rather than the
scroll container. This makes scrollToTask calculate wrong scrollTop.
requestAnimationFrame waits until after the browser has rendered the
current frame, which includes processing the async scroll event queued
by scrollToTask. This ensures in-focus is set after the scroll event
fires, not overwritten by it.
scrollTop assignment triggers an async scroll event. When all tasks
fit in the viewport, handleScrollFocus recalculates center-aligned
task and picks the last one instead of the saved one. Using setTimeout(0)
defers handler binding to after the queued event fires.
Previously handleScrollFocus() recalculated the centered task during
init, which could select a different task than the one scrollToTask()
targeted due to scroll container clamping or DOM layout timing.
Now the scrolled task directly receives the in-focus class, and
handleScrollFocus is only used during user-initiated scroll events.
Also removes the isInitializing flag as it's no longer needed.
handleScrollFocus now finds the task closest to scrollTop (offset)
instead of closest to the vertical center of the viewport. This
ensures that the saved task matches the user's scroll target.
handleScrollSave now saves the task with in-focus class (determined by
handleScrollFocus as the centered task) rather than recalculating which
task is closest to the top. This ensures consistency between what's
highlighted and what's saved.
- Set selectedTaskId when loading saved task
- Call handleScrollFocus initially to set in-focus class
- Skip saving during initialization unless task matches saved
Move initScrollFocus() after scrollToTask() to prevent handleScrollFocus()
from incorrectly updating selected_task_id before the saved position is restored
- When setting a new task to DOING, the previous DOING task now switches to PENDING instead of TODO
- Task display order now prioritizes by status: DONE < DOING < PENDING < TODO, with order field respected within each status group
Goals table has selected_task_id referencing tasks(id) without
ON DELETE SET NULL. When deleting a task, first clear any
selected_task_id references in goals to prevent FK violation.
Also update schema for future databases.
The Save button already has type=submit which naturally triggers
form submission. The additional click handler dispatching submit
event caused handleTaskSubmit to run twice.
'order' is a reserved keyword in SQLite. Quote all column names
in update_user, update_goal, and update_task to prevent syntax
errors when updating columns with reserved names.
- Add novalidate to task-form to prevent browser validation quirks
- Add explicit click handler for Save button that triggers
form submission via dispatched event for mobile compatibility
On small screens (<=640px), modal action buttons now stack
vertically with full width instead of wrapping unpredictably.
This applies to all modals: note, task, goal.
On small screens (<=480px), the goal selector dropdown could
overflow due to long option text. Match notes page behavior:
set select to width:100% with max-width:100%.
Use INSERT OR IGNORE instead of SELECT-then-INSERT for
default admin user creation, avoiding the race condition
when multiple gunicorn workers initialize simultaneously.
- Implement notes database schema and API endpoints
- Add notes page with filtering, search, and markdown support
- Persist selected goal and task preferences for better UX
- Include responsive design and mobile-friendly layout
- Replace TinyDB (JSON file) with sqlite3 for data persistence
- Add schema.py: table creation + data migration from db.json
- Rewrite database.py: all CRUD operations use sqlite3 directly
- All data retains original IDs via migration script
- Remove tinydb dependency from pyproject.toml
- Complete tasks now displayed in scroll view alongside unfinished tasks
- Priority order: completed tasks first (by finished_time desc), then unfinished (by order asc)
- Time picker-style scroll: wheel scroll snaps per task, center item gets visual focus
- Landscape mode (>=1024px): scroll view + edit panel side by side, panel always visible
- Portrait mode: edit panel slides in from right on tap
- Fixed flex layout so scroll view and edit panel align perfectly in height
- Flask backend with TinyDB database
- Multi-user auth with bcrypt password hashing
- Goal CRUD with activation/deactivation and per-user limits
- Task CRUD with status tracking (todo/doing/pending/done)
- Focus rule: one doing task per goal
- Time picker-style scroll view with drag-and-drop reordering
- Admin panel for user management
- uv environment management