API.md: document the reminder/due_soon/overdue notification schedule
(including the due_time-aware is_overdue fix and the 1-hour overdue
delay) and how default_reminder_minutes/reminder_at interact.
README.md: add Offline Support (PWA) and Notifications & Reminders to
the feature list - both were fully functional but never listed. Also
fixes a real gap in the manual systemd deployment guide: it only set
up the Celery worker, never Celery Beat, so the daily digest, recurring
task safety net, and now reminders would never fire for anyone
following that guide.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two refinements from live testing of the reminder feature:
- Due-time tasks scheduled "due now" and "overdue" at the exact same
moment. Task._due_and_overdue_moments() now delays "overdue" by an
hour so they don't arrive together. The overdue badge/styling
elsewhere is unaffected - only this notification's timing changes.
- Task.reschedule_reminders() only captures user.default_reminder_minutes
at the moment a task's own due_date/due_time is set, so changing the
profile setting didn't reach tasks whose due date was already set.
User.save() now detects a change to that setting and calls the new
User.reschedule_reminder_notifications(), which recomputes the
"before due" reminder on active due tasks that don't have their own
explicit reminder_at override.
8 new/updated tests cover the overdue delay and the retroactive
rescheduling (including that unrelated profile saves and tasks with an
explicit reminder_at are left untouched).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The web edit/create/quick-add views and the sync endpoint assigned
due_date/due_time/reminder_at straight from request data as raw strings.
Django tolerates this at save() (types get coerced deep in the SQL
layer), but the in-memory instance keeps the raw strings afterward.
Task.reschedule_reminders(), added for per-task reminders and run from
save() on that same instance, was the first code to actually operate on
those fields before a page reload and crashed with a TypeError - which
the service worker's 5xx-treated-as-offline fallback then masked as a
misleading "you're offline" screen instead of a real error.
Fixed by parsing with Django's parse_date/parse_time/parse_datetime at
the point these values are read from the request/payload in
tasks/views.py and sync/views.py, rather than trusting raw strings.
5 new regression tests exercise the actual views/endpoint with raw
date/time strings and assert proper types land on the model.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Only proactive notification was the once-a-day 6-7AM digest. Wires up
the previously-unused User.default_reminder_minutes profile setting
and ScheduledReminder model (Gitea #8) to send timely per-task alerts:
- Task.reschedule_reminders(), hooked into Task.save() via a dirty-check
against due_date/due_time/reminder_at/status/is_deleted, (re)creates up
to 3 ScheduledReminder rows per active due task: a "before due" reminder
(from reminder_at if set, else default_reminder_minutes before due), a
"due now" notification, and an "overdue" notification (mirroring
is_overdue's day-after rule for date-only due tasks). Hooking into
save() means every call site - web views, the API, and sync - picks
this up automatically.
- New Celery task send_scheduled_reminders (beat schedule: every 5 min)
sends due reminders via whichever of email/push the user has enabled,
reusing the existing per-user channel toggles, and logs them to the
Notification table.
- 14 new tests covering the scheduling math, due-date-change/completion/
deletion cleanup, and the sending task's channel and timing behavior.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task.is_overdue only compared due_date to today, so a task due at 9am
stayed "not overdue" until midnight regardless of due_time (Gitea #7).
Now factors in due_time when the due date is today, mirrored in the
offline app's isTaskOverdue().
Also hardened the service worker's install step: cache.addAll() lets
the browser's HTTP cache satisfy each precache fetch, and since static
assets here send no Cache-Control/ETag (just Last-Modified), a
CACHE_NAME bump wasn't reliably guaranteed to pick up fresh files.
Forcing {cache: 'reload'} on each precache fetch fixes that.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Online-side mutations (e.g. deleting a tag) only update the server, not
IndexedDB, so a throttled background sync could leave the offline cache
stale for up to two minutes. Going offline in that window resurrected
deleted/stale data (e.g. a deleted tag reappearing). Removing the throttle
so every real page load pulls fresh state keeps the offline cache in
sync with whatever was just done online.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
renderTaskItem() never applied the "overdue" CSS class at all, so
overdue tasks in offline mode looked like any other task instead of
getting the red highlight/due-date treatment they get online. Adds
isTaskOverdue(), mirroring Task.is_overdue, applied the same way
_task_item.html does it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
/api/sync/ only returns rows changed since a device's last sync token.
Any device that synced before tag/time-entry syncing existed already
has a token from that era; pre-existing tags untouched since then were
never "changed since" that token and so were permanently invisible to
that device, even after tag syncing shipped. Adds a sync_format_version
check that ignores the stored token and forces exactly one full resync
per device when it detects an old-format token, backfilling anything
that was missed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Offline mode had tag chips and per-task tag assignment, but was
missing the dashboard sidebar's Tags section entirely: filtering the
task list by tag, per-tag task counts, and tag create/rename/delete.
Ports that section over (reusing the same tag modal pattern as the
online create/edit flow), combinable with the existing status filters
the same way it works online.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The real dashboard is server-rendered before any client JS runs, so
the first page load after reconnecting always reflects the pre-sync
database state -- the background sync then completes silently, but
nothing told that already-rendered page to update. A task created
offline would appear to vanish until the next navigation happened to
load fresh data. runBackgroundSync() now reports whether it pushed
pending changes, and both places that trigger it automatically reload
the page when it did.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
The offline task app was showing every cached task including completed
ones, with no sorting -- unlike the online dashboard, which hides
completed tasks by default and sorts by due date. Ports the same
filter tabs (All/Today/Upcoming/Overdue/Completed) and sort options
(due date asc/desc, priority high/low) from tasks/views.py's
DashboardView so offline mode isn't a degraded view.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fetch() only rejects on true network failures, not bad status codes,
so a reverse proxy (Nginx Proxy Manager) returning 502/503 while the
app server itself is down was passed through as-is instead of falling
back to the offline experience. Now a >=500 response is treated the
same as a network error.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets users create, edit, and complete tasks with no network connection,
queued locally in IndexedDB and replayed via the existing /api/sync/
protocol once back online -- the same protocol the native Android app
already uses, with zero backend changes. The service worker now routes
the dashboard's offline fallback to a small client-rendered task app
instead of the generic "you're offline" page; every other route keeps
the generic fallback.
Conflicts (server row touched elsewhere since last sync) auto-resolve
as "local wins" rather than surfacing a resolution UI. Scope is title/
status/priority/due-date only -- tags, subtasks, time tracking, and
recurrence editing offline are out of scope for this phase.
Also adds the first real test coverage for sync/views.py (previously
untested): new-item creation, soft-delete-bypasses-conflict-check,
conflict detection/discarding, and both resolve_conflict paths.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Portainer stack variables only populate a container's environment when
docker-compose.yml explicitly references them; the web/celery-worker/
celery-beat services never listed VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY/
VAPID_ADMIN_EMAIL, so settings.VAPID_PUBLIC_KEY read as empty even with
the vars set in Portainer, breaking the push subscribe flow.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wires up the previously-scaffolded VAPID/DeviceToken infrastructure end
to end: browser subscription flow on the Profile page's existing "Push
Notifications" toggle, a service worker push/notificationclick handler,
and server-side sending from the daily task digest. Also broadens that
digest's eligibility query so push-only users (email notifications off)
aren't silently skipped, and adds `generate_vapid_keys` since the pinned
py-vapid's own key generator is broken against current cryptography.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
It's local AI-assistant guidance, not project documentation meant for
the repo. Kept on disk (gitignored) so it still works locally.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a manifest, service worker, and branded icons so the site can be
installed to a home screen/desktop, plus an offline fallback page so a
dropped connection shows something friendlier than the browser's default
error. Icons are rasterized from the existing favicon.svg mark via
rsvg-convert. The manifest and service worker are served through small
Django views (not raw static files) so their asset URLs pick up
WhiteNoise's content-hashed filenames in prod/selfhosted, and the service
worker is served from the site root so its scope covers the whole app.
Does not include true offline task data or Web Push notifications --
those are tracked separately as larger follow-up projects.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
API.md now lists the supported custom RRULE patterns (every Wednesday,
every other Wednesday, every second Tuesday, every 15th, etc.) matching
the newly implemented dateutil.rrule evaluation. CLAUDE.md's Task model
reference also gets corrected: it listed recurrence_type/recurrence_interval
fields that never existed on the model.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Recurring tasks were locked to fixed daily/weekly/biweekly/monthly/yearly
intervals. The `custom` recurrence type and `recurrence_rule` field already
existed in the model and API docs, but RRULE evaluation was a TODO stub
that silently fell back to weekly, and no UI exposed the option.
Implements real RRULE parsing via dateutil.rrule, and adds a builder UI
(day-of-week checkboxes for weekly, day-of-month or Nth-weekday for
monthly) so users can express patterns like "every other Wednesday" or
"every second Tuesday" without hand-writing RRULE strings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Repo moved from git.firebugit.com to git.darksingularity.org; the old
host is gone, which would break both docker-compose's git build
contexts and the Portainer stack setup instructions. Also removes
production secret values (SECRET_KEY, DB passwords, SMTP password)
that were committed in plaintext in the deployment guide.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "Default" option cleared the sort param and fell back to the
model's Meta.ordering (sort_order, -priority, due_date, created_at),
which read as broken/unsorted to users. Default is now due date
(earliest first), matching the existing due_date sort option.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Complete REST API documentation including authentication, rate limiting, all endpoints (users, tasks, tags, time tracking, sharing, sync, notifications), error handling, and usage examples. Intended for developers building applications that integrate with KeepItGoing via the API.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Prevent process_recurring_tasks from creating duplicate tasks for dates that already have completed instances. Previously only checked for pending tasks, causing old completed recurring tasks to reappear as pending when subsequent instances went overdue.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Provides comprehensive guidance for future Claude Code instances including development commands, architecture overview, and common patterns.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes email notification links showing localhost:8000 instead of the
actual domain. The SITE_DOMAIN variable is now passed to web,
celery-worker, and celery-beat containers so emails include the
correct URL.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Critical fix: Task now runs every hour instead of once at 6 AM UTC.
This ensures users in all timezones receive their email at 6 AM local time.
Changes:
- Run task every hour instead of once daily
- Check if it's 6-7 AM in user's timezone (1 hour window)
- Track sent emails in Notification model to prevent duplicates
- Add 'daily_email' notification type
Without this fix, users in timezones where 6 AM UTC is not morning
would never receive their daily email.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace complex notification system with a simple daily email:
- Send ONE email per day between 6-9 AM in user's timezone
- Show tasks due today and overdue tasks
- Only send if user has email_notifications enabled
- Remove all push notification logic
- Keep recurring task processor (runs daily at midnight)
This makes notifications much simpler and less intrusive while
still providing value to users.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update build contexts to pull from git repository URL instead of
local directory. This allows Portainer to build images directly
from the repository without requiring SSH access to the server.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Change healthchecks to use 'ps aux | grep' instead of 'pgrep' since
procps may not be available in existing images. This works with the
base Python image without requiring a rebuild.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This allows deploying from git repository in Portainer by providing
an example environment variables file. Users can copy this and fill
in their actual values in Portainer's environment variables section.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace celery inspect ping with pgrep check for reliability.
The inspect command was failing in healthcheck context while
the worker process itself was running fine.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix import error in notifications/tasks.py (timezone.datetime -> datetime)
- Add healthchecks to celery-worker and celery-beat containers
- Add procps package to Dockerfile for pgrep command
- Add email environment variables to celery containers
The import error was causing celery workers to crash when loading tasks.
Missing healthchecks prevented proper container health monitoring.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes ModuleNotFoundError by using Python's built-in zoneinfo module
instead of the external pytz dependency. zoneinfo is the standard
library solution for timezone handling in Python 3.9+.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes 500 error when user timezone is invalid or missing.
Now gracefully falls back to UTC if timezone conversion fails.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Tasks were incorrectly showing as overdue when they were due today.
The issue was that the overdue check was comparing the due date
against UTC's "today" instead of the user's local "today".
Now uses the user's timezone setting to determine the current date
when checking if a task is overdue. This ensures tasks due "today"
only become overdue when the user's local date advances to tomorrow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Previously priority sorting was alphabetical (high < low < medium < urgent)
instead of by importance. Now uses Django Case/When to map priority strings
to numeric values: urgent=4, high=3, medium=2, low=1.
Fixes both queryset sorting and list sorting for overdue filter.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add custom scrollbar styles for Chromium/Webkit browsers to match the
dark theme aesthetic. Firefox already had proper scrollbar styling, but
Chrome was showing default light gray scrollbars.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds automatic creation of next task instance when recurring tasks are completed.
- Add calculate_next_due_date() and create_next_recurrence() methods to Task model
- Update task completion handlers in views and sync API to create next recurrence
- Add hourly Celery task to process any missed recurring tasks
- Support daily, weekly, biweekly, monthly, yearly recurrence patterns
- Respect recurrence_end_date limits
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changed mobile overlay from 50% to 75% opacity (25% transparent)
to make the sidebar content more visible when open on mobile devices.
Updated rgba(0, 0, 0, 0.5) to rgba(0, 0, 0, 0.75)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Middleware now:
- Detects mobile app vs browser via User-Agent
- Mobile app: Removes frame-blocking headers (allows iframe)
- Browsers: Adds CSP and X-Frame-Options headers (security)
This ensures:
✓ Browsers get full CSP protection
✓ Mobile app can embed content
✓ No need for django-csp package
✓ All security managed in one place
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The CSP_FRAME_ANCESTORS = ("'none'",) setting in production.py was
blocking iframe embedding even after removing X-Frame-Options.
Updated middleware to:
- Detect Android WebView via 'wv' in User-Agent (more reliable)
- Remove both X-Frame-Options AND Content-Security-Policy headers
- This allows mobile app iframe embedding while keeping browser protection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>