- Settings: add date_format and time_format (model, migration 119, admin UI)
- Use user date/time prefs in templates, calendar, and PDF export
- Expense: eager-load client instead of category in repo, service, and API
- Mobile: clarify server URL and certificate docs, bump to 4.18.0, improve connection diagnostics
- Ignore mobile/android/.gradle/
Co-authored-by: Cursor <cursoragent@cursor.com>
Display formats for dates and times now follow the system settings (Admin
settings) by default. Users can override in their profile (User settings) or
choose "Use system default" so their view matches the rest of the system.
Backend:
- User.date_format and User.time_format are nullable; null means use system.
- Migration 120 makes these columns nullable (existing rows unchanged).
- get_resolved_date_format_key() and get_resolved_time_format_key() in
timezone utils return the effective key (user or system) for templates and API.
- Context processor injects resolved_date_format_key and resolved_time_format_key
so base.html and JS (window.userPrefs) always see the resolved format.
- User settings form: "Use system default" option and save logic for null.
- User.to_dict() includes resolved date_format, time_format, and timezone for
API clients (e.g. mobile).
Web:
- base.html uses resolved keys for window.userPrefs (no hardcoded fallback).
- Replaced display-only strftime() in templates with |user_date, |user_datetime,
|user_time, and |format_date so all visible dates/times respect settings.
Left <input type="date"> values and URL/API params as YYYY-MM-DD where required.
Mobile:
- ApiClient.getCurrentUser() and user prefs provider load resolved prefs from
/api/v1/users/me.
- date_format_utils maps API keys to intl patterns; formatDate, formatTime,
formatDateTime, formatDateRange used for display.
- Time entries screen (filter dialog), time entry form, time entry card, and
home dashboard use user prefs for formatting; API requests still send ISO dates.
Co-authored-by: Cursor <cursoragent@cursor.com>
After creating a lead, redirect to /leads/{id} was raising TemplateNotFound. Add a full lead detail view with contact info, status/score, notes/tags, activity list, and actions: Edit, Add Activity, Convert to Client/Deal, Mark Lost.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace hardcoded dollar symbol and plain 0.00 formatting on Project budget overview, Budget Alerts dashboard, and Budget project detail with the currency from Settings and thousand-separator number format. Add format_currency template filter; inject currency symbol for Chart.js tooltips and axis ticks.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Document that the default admin has no password; users set it on first
login (8+ characters) with AUTH_METHOD=local
- Add security note: self-register creates app users from entered
credentials only, not from DATABASE_URL; avoid using the DB username
(e.g. timetracker) as an app username
- Add "What is the default admin password?" to Common Questions
- Update GETTING_STARTED, DOCKER_COMPOSE_SETUP, DOCKER_STARTUP_TROUBLESHOOTING,
and env.example
- Config: clarify WTF_CSRF_TRUSTED_ORIGINS comment (include exact client URL)
- Mobile: README troubleshooting for DDNS/cert errors and subpath base URL
- Mobile: login screen helper text for exact URL and "Yes, trust" for certs
- Mobile: 404 diagnostics suggest using full base URL when app is at a subpath
- Docs: add NGINX_PUBLIC_DOMAIN.md (Docker vs host nginx, why /api/v1 404)
- Nginx: add example-public-domain.conf for exposing app at a public domain
Performance:
- Fix N+1 queries in reports.py with joinedload for TimeEntry.project,
TimeEntry.user, TimeEntry.task, and Project.client across 6 query locations
- Replace per-task time_entries loops with batch UPDATE queries in tasks.py
- Use efficient subquery for favorite project IDs in projects.py
Architecture:
- Add get_by_id() and get_by_name() methods to ProjectService and ClientService
- Route project/client lookups through service layer in timer.py, projects.py,
and clients.py instead of direct Model.query calls
Security:
- Add sanitize_input() with length limits to form inputs in clients.py,
projects.py, timer.py, issues.py, and auth.py
- Add email format validation for client creation
- Warn at startup when SECRET_KEY uses the default value or is too short
in ProductionConfig
- Replace 7 bare except: pass clauses with specific exception types
(OSError, IOError, TypeError, ValueError) in admin.py, settings.py,
and invoice.py
Authorization:
- Migrate all @admin_required decorators to @admin_or_permission_required()
with granular permissions (manage_roles, manage_kanban, manage_webhooks,
manage_api_tokens, manage_integrations, access_admin) across permissions.py,
kanban.py, webhooks.py, and admin.py (28 routes total)
Frontend:
- Remove 40+ console.log debug statements across 18 JS files
- Replace 42 inline onclick/onchange handlers in base.html with delegated
event listeners using data-dropdown and data-no-propagation attributes
- Migrate 6 inline handlers in time_entries_overview.html to addEventListener
- Extract shared typing detection into typing-utils.js, eliminating 5
duplicate isTyping() implementations across keyboard shortcut files
- Add missing aria-label attributes to icon-only buttons
Dependencies:
- Migrate from pytz to stdlib zoneinfo (Python 3.9+) across all 6 files
that used pytz; replace pytz with tzdata in requirements.txt
- Separate dev/test dependencies into requirements-dev.txt
- Configure RotatingFileHandler (10MB, 5 backups) for app and JSON logs
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the bare data table with a full report layout: titled header with date range and active filters, entries grouped by date with sub-headers, optimized 8-column layout (merged start/end into time range, removed redundant date and source columns, split client into its own column), HH:MM duration format, word-wrapping notes via Paragraph objects, summary totals bar with entry count and billable hours, and page numbers. Pass filter metadata from the export route to the PDF builder.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace WeasyPrint HTML-to-PDF (full page) with a dedicated ReportLab generator that outputs only a table of time entry data.
- Add app/utils/time_entries_pdf.py: A4 landscape, compact table (Date, User, Project, Task, Start, End, Duration, Notes, Tags, Billable, Source), plain-string cells and per-page tables to avoid ReportLab table-split height bug.
- Update timer export route to use build_time_entries_pdf(); remove WeasyPrint and time_entries_export_pdf.html usage for this export.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Fix user dropdown selection: use explicit type-safe comparison
(filters.user_id|string == user.id|string) so the correct user
stays selected after pagination or page reload
- Exclude client_id from filter URL when it is auto-selected
(single client or locked client): add data-auto-client attribute
to client_select hidden inputs and skip them in getFilterParams
so filtering by user alone no longer forces client_id=1 into the URL
Co-authored-by: Cursor <cursoragent@cursor.com>
- Keep date/time fields always editable when duration (HH:MM) is entered
- Backend: when duration and start date+time are provided, use
end = start + duration instead of requiring full start/end or duration-only
- Update help text to explain duration can be used with a specific date
Co-authored-by: Cursor <cursoragent@cursor.com>
Match live layer children to JSON by index and explicitly inject
name and imageUrl for decorative-image nodes, since Konva toJSON()
may not include custom attributes.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Move start/end date filters before project/client for clearer layout
- Add note that exports use current filters; flex-wrap on filter bar
- Explicitly read filter fields (including hidden from client_select) before
FormData so CSV/PDF export and bulk actions get correct params
Co-authored-by: Cursor <cursoragent@cursor.com>
Build tasks URL from request.script_root so reverse-proxy and subpath
configurations work correctly when loading project tasks.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Show client as normal dropdown; pre-select only from URL param
- Use scriptRoot for tasks API URL instead of url_for template
- Use tojson for all JS-translated strings to avoid syntax errors
- Handle 404 when loading tasks (project not found/inactive)
- Clear client when project is selected (mutual exclusivity)
Co-authored-by: Cursor <cursoragent@cursor.com>
Pass prefill_start_date and prefill_end_date from manual_entry and
manual_entry_for_project using the user's timezone.
Co-authored-by: Cursor <cursoragent@cursor.com>
Avoid syntax errors from quotes and ampersands in translated strings
by serializing i18n values with Jinja's tojson filter.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use AppSpacing in home, time entry form, empty state, and time entry card. Add setSyncInterval to AppConfig. Show notes or date range as subtitle on recent entries; style duration. Settings: add dialogs for server URL and API token, show token state. Time entry form: load tags and billable when editing, load tasks for selected project.
Co-authored-by: Cursor <cursoragent@cursor.com>
Persist and load auto_sync and sync_interval in settings. Disable sync interval input when auto-sync is off. Add styling for disabled inputs in settings. Preserve project filter when loading filter projects and add selectProject helper for dashboard.
Co-authored-by: Cursor <cursoragent@cursor.com>
Build task API URLs from url_for in timer page, bulk entry, edit timer, and time entry template edit to support subpath deployment. Wrap timer page script in DOMContentLoaded, load tasks when project is pre-selected, expose selectRecentProject on window, and fix client/project select attachment logic.
Co-authored-by: Cursor <cursoragent@cursor.com>
When module_enabled decorator blocks access, detect JSON/AJAX requests and return 401/403 with JSON body instead of redirect or HTML abort, so API and SPA clients get proper error responses.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use openKeyboardShortcutsModal when available in shortcut manager. Remove duplicate keyboard-shortcuts-enhanced.js and toast-notifications.css from base. Only open shortcuts modal on Shift+? when advanced manager is not loaded to avoid double handling.
Co-authored-by: Cursor <cursoragent@cursor.com>
Add db.session.commit() after client.archive() and client.activate() so changes are persisted. Escape translated strings in showConfirm() with |e to avoid broken JavaScript when quotes appear in translations.
Co-authored-by: Cursor <cursoragent@cursor.com>
Log Time - Task dropdown:
- Detect session expiry when API returns HTML instead of JSON
- Show clearer error message suggesting page refresh
- Include HTTP status in error messages
- Add console.error with URL and project ID for debugging
Time Entries - Filters:
- Fallback to explicitly read select/hidden inputs in getFilterParams
- Add console.debug for filter params and URL (helps diagnose issues)
- Use DOMParser as fallback when response parsing fails
- Only set lastUrl on successful parse to allow retries
- Trigger filter apply on text input change (custom fields)
Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes manual time entry task loading, adds a worked-time helper, makes Time Entries filters reliable, and adds CSV export for the current filtered view.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add issuer fallback from OIDC_ISSUER when userinfo lacks iss (fixes Authelia)
- Fallback to unverified id_token decode for iss when ID token parsing failed
- Wrap authorize_access_token() in dedicated try/except; log token_exchange_failed
and suggest session cookie/proxy checks when state or PKCE validation fails
- Log reason=... before every redirect to login in callback for easier debugging
- Add 'Redirect loop / callback returns to login' troubleshooting to OIDC_SETUP.md
Fixes#486
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add settings.locked_client_id and admin UI to select locked client
- Allow disabling Clients for non-admins while keeping admin access
- Gate Clients UI routes and API endpoints when module is disabled
- Auto-select and enforce the locked client across filters and form submissions
Co-authored-by: Cursor <cursoragent@cursor.com>
Admins can hide whole app modules (Analytics, Finance & Expenses, CRM, etc.)
per role so users in that role neither see them in the nav nor access them
by URL/API.
- Add Role.hidden_module_ids (JSON denylist) and migration
- Extend ModuleRegistry.is_enabled() with role-based hide check; module is
hidden only if ALL of the user's roles hide it (super admins bypass)
- Add Module visibility section to role create/edit form with checkboxes
by category; persist via hidden_modules form field
- Add tests for registry hide/allow semantics and route decorator 403
Closes#484
Co-authored-by: Cursor <cursoragent@cursor.com>
Add a new User Report export that outputs one row per time entry with selectable columns (date/user/project/task/duration/notes by default), while keeping the existing summary/overtime export.
Refs: #483
Co-authored-by: Cursor <cursoragent@cursor.com>
Add has_enabled_modules(category) helper and wrap Finance & Expenses
folder so it only renders when at least one FINANCE module is enabled.
Fixes empty folder visible after disabling all modules in a category.
Closes#481
Co-authored-by: Cursor <cursoragent@cursor.com>
When the IdP issues a large id_token (e.g. with groups claim), storing it in Flask's cookie session can exceed ~4KB and cause the browser to drop the cookie, leading to redirect loops between /auth/oidc/callback and /login.
Store id_token in Redis/in-memory cache; keep only a small reference key (oidc_id_token_key) in the session cookie. On logout, resolve id_token from cache for RP-initiated logout; support legacy session for backwards compatibility. Add regression test for oversized id_token and update OIDC logout tests.
Fixes#486
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace trailing backslash (mobile\\.android\\) with forward slash
(mobile/.android/) so ripgrep and other tools can search the repo.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Use /api/projects/<id>/tasks instead of /api/tasks?project_id= so task
loading matches the working Edit Logged Time flow.
- Add credentials: 'same-origin' and response.ok checks for reliable
session auth and error handling.
- Render JS-embedded i18n strings with |tojson to avoid breakage in
non-English locales.
Fixes#480
Co-authored-by: Cursor <cursoragent@cursor.com>