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>
- desktop/README and docs/mobile-desktop-apps: minor updates
- desktop renderer index.html: small adjustments
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add POST /api/v1/auth/login (rate-limited) returning API token for mobile
- Fix time entry duration when DB returns timezone-aware datetimes (_naive_dt)
- Add _parse_date_range helper; expose timezone in API info
- Extend time entries API tests
Co-authored-by: Cursor <cursoragent@cursor.com>