Introduce a centralized OTel layer (app/telemetry/otel_setup.py) that reuses
the existing OTLP endpoint and token, exports traces and metrics over OTLP/HTTP,
and instruments Flask plus SQLAlchemy. Manual OTLP log export remains for base
and product analytics; log records now include trace_id, span_id, and
event_category where tracing is active.
Business spans and product metrics cover invoices, timers, reports, auth,
webhook delivery, and scheduled jobs. RED-style HTTP metrics are recorded in
after_request alongside existing Prometheus counters. ENABLE_TRACING and
ENABLE_METRICS default on when credentials exist; graceful no-op when they do not.
Privacy is preserved: user_id appears on traces only when detailed analytics is
opted in; metrics never carry user_id; _remove_pii behavior for analytics is
unchanged. Responses inject traceparent when tracing is enabled for future
browser correlation.
Tests: test_otel_integration.py and per-test reset_for_testing() in conftest
so each app factory can reinitialize OTel.
Add Settings.mail_test_recipient (migration 142) and wire Admin → Email Configuration to save, load, and prefill the SMTP test recipient field.
Extract invoice email PDF/HTML building into _build_invoice_email_payload for reuse. New send_invoice_template_test_email sends a real message with subject "Invoice template test: …", reusing production rendering and PDF attachment, without updating invoice status or creating InvoiceEmail records.
Add POST /admin/email-templates/<id>/send-test (optional recipient override, optional invoice_id) and Send test email UI on template view and edit pages. Extend smoke and unit tests.
Switch product and installation telemetry to OTLP/Grafana across runtime config, CI injection, docs, and tests to unify telemetry transport and simplify privacy-focused opt-in behavior.
Implement issue #575 by introducing token-based invoice number patterns in settings and unifying number generation across invoice creation paths. This removes hardcoded INV/date formatting and aligns export filenames and bootstrap schemas with stored invoice numbers.
- New scopes read:inventory and write:inventory; existing read/write:projects
still grant same inventory access for backward compatibility
- require_api_token() accepts tuple of scopes (any one required); inventory
endpoints accept (read:inventory | read:projects) and (write:inventory | write:projects)
- ApiTokenService: add new scopes to allowed list; document in API_TOKEN_SCOPES.md
- Add tests for inventory report endpoints with scope checks
- Add keyboard_shortcuts_defaults utility for default bindings and overrides
- Update Settings keyboard shortcuts template for customization UI
- Add KEYBOARD_SHORTCUTS_DEVELOPER.md for implementation and extension
- Register send_base_telemetry_heartbeat_with_app cron at 03:00 UTC
- setup: call check_and_send_telemetry when user opts in during setup
- admin: call check_and_send_telemetry when toggling detailed analytics on
- Delegate track_event, identify_user, track_page_view to telemetry service
- Only send detailed analytics when user has opted in (is_detailed_analytics_enabled)
- Call send_base_first_seen() once at app startup (idempotent per install)
- posthog_funnels: require telemetry_enabled for funnel tracking
- posthog_monitoring: require telemetry_enabled for error/performance events
- Webhook models: remove duplicate index definitions so db.create_all()
no longer raises 'index already exists' (columns already have index=True)
- ImportService: fix circular import by late-importing ClientService,
ProjectService, TimeTrackingService in __init__
- reports: fix F823 by renaming unpack variable _ to _entry_count to avoid
shadowing gettext _ in export_task_excel()
- Code quality: add .flake8 with extend-ignore so flake8 CI passes;
simplify pyproject.toml isort config (drop unsupported options)
- Format: run black and isort on app/
- tests: restore minimal app fixture in test_import_export_models
- Add RecurringInvoiceRepository and RecurringInvoiceService; refactor recurring_invoice model
- Add GanttService and move gantt logic from route to service
- Expand ReportingService and simplify reports route
- Add license_utils and user license template/settings
- Refactor routes to use scope_filter, api_responses, and services (API v1, timer, admin, invoices, etc.)
- Extend invoice_service for recurring; cache and scope_filter utils; base/template updates
- Mileage: Add GET /mileage/export/csv and /mileage/export/pdf with same
filters as list (status, project, client, date range, search). Export
buttons in list header; JS builds export URL from current filter form.
- Mileage PDF: New app/utils/mileage_pdf.py (ReportLab, landscape A4,
totals row for distance and amount).
- Per diem: Add Client filter to list (with client-lock/single-client
handling). Add GET /per-diem/export/csv and /per-diem/export/pdf.
- Per diem PDF: New app/utils/per_diem_pdf.py (same style as mileage).
- Export links always use current filters (no need to submit first).
- CHANGELOG and docs/import_export/README updated.
- get_overtime_ytd(user): returns overtime from Jan 1 through today
- get_overtime_last_12_months(user): returns rolling 12-month overtime
- Reuses calculate_period_overtime; no new DB columns
Architecture and maintainability improvements per production-readiness plan:
- API v1: Split monolithic api_v1.py into per-resource blueprints
(api_v1_projects, api_v1_tasks, api_v1_clients, api_v1_invoices,
api_v1_expenses, api_v1_payments, api_v1_mileage, api_v1_deals,
api_v1_leads, api_v1_contacts). Register all in blueprint_registry;
keep info, health, auth and remaining routes in api_v1.py.
- Bootstrap: Move setup_logging to app/utils/setup_logging.py and
legacy migrations (task management, issues tables) to
app/utils/legacy_migrations.py. Use SQLAlchemy 2-compatible
db.engine.begin() in legacy_migrations.
- Dashboard: Add AnalyticsService.get_dashboard_top_projects and
get_time_by_project_chart; thin main dashboard route to call
services only and remove inline TimeEntry aggregation.
- Docs: Update ARCHITECTURE.md (module table, API structure, data
flow, design decisions), DEVELOPMENT.md (workflow, build steps,
test examples), CHANGELOG.md (Unreleased refactor entry).
Dashboard:
- Add time-by-project chart (last 7 days) with Chart.js horizontal bar; link to Summary report
Summary report:
- Add time-by-project (last 30d) bar chart and daily trend (14d) line chart
- Add one-page PDF export (today/week/month hours + top projects table)
Post-timer flow:
- After stop, show toast "Logged Xh on [Project]" with action link "View time entries"
- Toast manager: optional actionLink/actionLabel for action links in toasts
- Session carries timer_stopped_toast to dashboard; no duplicate flash
Remind to log:
- User setting "Remind me to log time at end of day" + time picker (Settings)
- Hourly job: send one email per day if user has <0.5h logged that day (user TZ)
- Migration 135: notification_remind_to_log, reminder_to_log_time on users
- In _on_page(), only skip IMAGE elements when their rect does not
intersect the page (was skipping when top-left was outside, which
hid overflowing images).
- Coerce element x, y, width, height to float to avoid type issues.
- In _draw_image_on_canvas(), clip all image drawing to the page
rectangle (beginPath + rect + clipPath) so the visible portion
is shown and overflow is clipped like in the preview.
- Try template image file path before get_image_base64 for more
reliable loading across deployments.
- Add INFO log when drawing template images for debugging.
Fixes#537 (export not showing decorative image when over border).
Before deleting a user, explicitly remove their donation_interactions rows so the delete succeeds even when the table is missing (e.g. after support banner was removed via license). Also handle missing donation_interactions table in safe_commit like time_entry_approvals.
Switch embedded invoice PDFs to Factur-X CII payloads and tighten the PDF/A-3 and AS4 handling so exports better match the standards they advertise. Document the experimental native Peppol transport path and cover the new validation and embedding behavior with focused tests.
Implement native PEPPOL transport plumbing (identifier validation, SMP/SML discovery, and AS4 send path) and make ZUGFeRD/PDF export fail fast when embedding or PDF/A-3 normalization fails. Add settings, migrations, validators, tests, and docs so compliance issues are visible and verifiable.
- UBL: add unitCode C62 to InvoicedQuantity (required by EN 16931 / Peppol BR-CL-23)
- ZugFerd: attach XML as Associated File with relationship Alternative
- ZugFerd: inject ZUGFeRD XMP RDF for validators
- Docs: document AF/XMP, PDF/A-3 limitation, and validation (b2brouter, portinvoice)
- Tests: assert UBL contains InvoicedQuantity with unitCode in ZugFerd and Peppol tests
Implement the advertised 'Task tags and categorization' feature that was
listed in docs but missing from the task edit form.
- Add tags column to tasks table (String 500, comma-separated)
- Add tags field to task create and edit forms with validation
- Display tags as badges on task view page and list pages
- Add tags filter to main tasks list and My Tasks with AJAX support
- Include tags in task export (CSV) and data export
- Add tags to Task API (create, update, list with filter)
- Add tag_list property to Task model for convenient parsing
Also: fix(oidc) use domain instead of IP for HTTPS metadata fetch
to fix TLS SNI (Fixes#540)
- Add run_seed() in app/utils/seed_dev_data.py: users, clients, projects,
tasks, time entries, expenses, comments, warehouses, stock items,
warehouse stock, stock movements, currencies, tax rules, invoices,
invoice items, and payments. Only runs when FLASK_ENV=development.
- Register 'flask seed' CLI command with options (users, clients,
projects-per-client, tasks-per-project, days-back).
- Add scripts/seed-dev-data.py and docker/seed-dev-data.sh for local
and Docker runs. Include seed scripts in image via Dockerfile chmod.
- Document in docs/development/SEED_DEV_DATA.md; update
DATABASE_RECOVERY.md, DOCKER_COMPOSE_SETUP.md, and development README.
- Add render.yaml Blueprint: PostgreSQL + Python web service, auto-deploy on push
- Add demo mode (DEMO_MODE, DEMO_USERNAME, DEMO_PASSWORD): single fixed user only
- Login page shows demo credentials when demo mode is active
- Disable self-registration, admin user creation, and OIDC user creation in demo mode
- DB init creates demo user with password when DEMO_MODE is true
- Add docs/deploy/RENDER.md with deployment and demo mode instructions
Decorative images now survive save/load and no longer cause a black PDF preview:
- Sync imageUrl onto groups before generateCode() so template_json has
correct source (invoice and quote layout editors).
- Inject name/imageUrl into design_json with position-based matching so
reordering does not swap or drop URLs.
- Restore name and imageUrl from saved JSON onto canvas on load
(synchronous) so Konva custom attrs are not required.
- Omit decorative image elements with empty source from template_json;
placeholders stay visible in the editor but are not sent to ReportLab.
- ReportLab: explicitly skip decorative images with empty source; validate
base64 data URI payload and decode in try/except to avoid bad PDF output.
Documentation: PDF_LAYOUT_CUSTOMIZATION.md and PDF_EDITOR_ENHANCED_FEATURES.md
updated with decorative image description, state persistence details, and
troubleshooting. CHANGELOG.md updated under [Unreleased] Fixed.
- Add optional embedding of EN 16931 UBL XML in invoice PDFs (ZugFerd/Factur-X)
when 'Embed EN 16931 XML in invoice PDFs' is enabled in Admin > Peppol e-Invoicing.
Exported PDFs then contain ZUGFeRD-invoice.xml for hybrid human- and machine-readable
invoices; same UBL as Peppol, usable via Peppol or email.
- New setting invoices_zugferd_pdf (migration 128), pikepdf dependency, and
app.utils.zugferd helper (best-effort supplier/customer from Settings and client).
- Wire embed in export_invoice_pdf (and fallback path); admin checkbox and persistence.
- Docs: PEPPOL_EINVOICING.md retitled to 'Peppol and ZugFerd', new section for
ZugFerd embedding; README and CHANGELOG updated; migration 128 noted.
- Tests: test_zugferd.py (embed adds attachment with expected XML; invalid PDF
returns original bytes and error).
- Add user_clients table and UserClient model for many-to-many user-client assignment
- Add 'subcontractor' system role; users with this role see only assigned clients and their projects
- User helpers: is_scope_restricted, get_allowed_client_ids(), get_allowed_project_ids()
- Admin user form: assign clients when role is Subcontractor (multi-select, JS toggle)
- Scope filtering: clients, projects, time entries, reports, invoices, timer, API v1
- Direct access to out-of-scope client/project returns 403 (web and API)
- Migration 127_add_user_clients_table; scope_filter utility and ProjectService scope_client_ids
- Docs: SUBCONTRACTOR_ROLE.md, ADVANCED_PERMISSIONS.md, RBAC, CLIENT_PORTAL, README, CHANGELOG
Addresses GitHub Discussion #476 (user with limited clients/projects).
Add has_endpoint() helper to check if a Flask endpoint exists before
rendering navigation links. When stripe is not installed, the
payment_gateways blueprint fails to register but is_module_enabled()
can still return true, causing url_for('payment_gateways.list_gateways')
to raise BuildError when rendering base.html.
Only show the Payment Gateways nav link when the endpoint is actually
registered. This fixes inventory route tests that failed when following
redirects to the movements list page in environments without stripe.
Related to issue #385 (stock item devaluation).
Make https://timetracker.drytrix.com/support.html visible so users can purchase a key to hide donate/support UI (one key per instance, €25 one-time).
- Add SUPPORT_PURCHASE_URL config and support_purchase_url in template context
- Donate page: 'Remove Donation Messages' section and CTA, link in Other Ways to Help
- Admin Settings: Support visibility copy and 'Get key at Support & Purchase' button
- User Settings: line and link for admins to purchase a key
- Support banner: 'Purchase key to hide' link
- Dashboard widget: 'Want to hide this widget? Purchase a key'
- README: Support section bullet and intro line for support/purchase key
- SUPPORT_VISIBILITY.md: 'How to get a code' subsection, issuing codes note
- docs/README.md: Support visibility in Configuration with purchase link
- messages.po: add new translatable strings
- API: include time_entry_requirements in GET /users/me (require_task, require_description, min length)
- Timer: validate task/description requirements when starting timer; pass time_approvals_enabled and can_request_approval to view_timer; pass entry_ids_with_pending_approval to time_entries_overview
- Import: pass skip_entry_requirements=True when creating entries from CSV import
- Add /reports/time-entries page listing all time entries (billed and unbilled)
- Columns: Date, Start, Stop, Duration, Project, Task, Notes, Billed, Client
- Filters: date range, user, project, client, task, billed (all/yes/no)
- Export to Excel and CSV with same filters; add Billed column to excel export
- Resolve client from entry.client or project.client in export
- Add Time Entries Report card to Reports index
- Use Project.client_obj in joinedload (fixes loader strategy error on export).
- Null-safe user/project/client in CSV rows; client column uses Project.client.
- Try/except and logger.exception in both CSV routes; SQLAlchemy handler
logs full traceback and writes to stderr for Docker logs.
Refs #496
Co-authored-by: Cursor <cursoragent@cursor.com>
- Admin PDF preview: build all_line_items on invoice wrapper and resolve
table data from element data source (invoice.all_line_items or
invoice.items) so preview matches exported PDF.
- ReportLab: when template uses invoice.items, append both extra_goods
and expenses to table data so all line types appear in PDF.
- Export PDF: explicitly load items, extra_goods, and expenses before
generation so data is in session.
- Docs: recommend invoice.all_line_items for custom templates; document
backward compatibility and preview behavior.
Refs #503
Co-authored-by: Cursor <cursoragent@cursor.com>
- Lead to Client: stop passing status into Client(); set after create (#514)
- Leads/Deals activity: parse datetime-local single string in timezone util (#513)
- Deal page: add audit tracking for Deal, entity history support, History section (#515)
Refs #513, #514, #515
Co-authored-by: Cursor <cursoragent@cursor.com>
- Respect user date format and week start: Flatpickr on user-date-input
fields, get_resolved_week_start_day for calendar/pickers, Week Starts On
from My Settings. Update CALENDAR_FEATURES_README.
- Fix time entry notes not saving on edit: sync Toast UI editor to hidden
textarea on all forms so the submitted form gets the current note (edit_timer).
- Time entries PDF export: wrap Task, Client, and Project columns like Notes
so long text breaks across lines (Issue #489).
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add Support visibility in Settings: users see a stable System ID and can
enter a code to permanently hide donate buttons, support banner, and
donate widgets.
- Verification supports two modes:
- Ed25519: server stores only public key; codes are signatures generated
offline with the private key (no secret on server).
- HMAC: server stores a secret; code = HMAC(secret, system_id).
- Add User.ui_show_donate and Settings.system_instance_id (migration 121).
- Add donate_hide_code utility (HMAC + Ed25519 verify) and config for
DONATE_HIDE_PUBLIC_KEY(_FILE) and DONATE_HIDE_UNLOCK_SECRET(_FILE).
- Wrap all donate UI in base, dashboard, about, help with conditional.
- Add admin doc SUPPORT_VISIBILITY.md; ignore docs/internal/ and
code-generation script in .gitignore.
- Add translations for new strings.
Co-authored-by: Cursor <cursoragent@cursor.com>
PDF invoices were missing extra goods (and expenses) because the ReportLab
template renderer only used invoice.items as the table data source.
- Add invoice.all_line_items to template context: merged list of items,
extra_goods, and expenses with normalized description/quantity/price fields
- Update default template schema to use invoice.all_line_items instead of
invoice.items for the items table
- Add migration to update existing saved templates with the new data source
- Update PDF layout designer: add all_line_items and extra_goods loop options,
default items table to all_line_items
- Add expenses to fallback ReportLab generator for consistency with
pdf_default.html
Fixes#503
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace hardcoded EUR/dollar symbols with format_currency filter that
respects Settings.currency. Add thousands separators (#,##0.00) to all
monetary displays. Expand currency selection to SUPPORTED_CURRENCIES
in per diem rates and mileage forms.
- template_filters: Fix currency_symbol/currency_icon fallback to use
Settings instead of hardcoded dollar sign
- Per diem: list, view, rate_form; pass currency and supported_currencies
- Mileage: list, view, form; same pattern
- Expenses: dashboard and list use format_currency
- Inventory reports: dashboard and valuation use format_currency
Fixes#498
Co-authored-by: Cursor <cursoragent@cursor.com>
- 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>
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>
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>
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>
- 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>