Copy disabled (too large)
Download .txt
Showing preview only (41,077K chars total). Download the full file to get everything.
Repository: DRYTRIX/TimeTracker
Branch: main
Commit: 9773d577255e
Files: 1899
Total size: 38.5 MB
Directory structure:
gitextract__fbo_k07/
├── .bandit
├── .coveragerc
├── .editorconfig
├── .flake8
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ └── translation_fix.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── workflows/
│ │ ├── build-desktop.yml
│ │ ├── build-mobile.yml
│ │ ├── cd-development.yml
│ │ ├── cd-release.yml
│ │ ├── ci-comprehensive.yml
│ │ ├── ci.yml
│ │ ├── crowdin-sync.yml
│ │ ├── migration-check.yml
│ │ └── static.yml
│ └── workflows-archive/
│ ├── ci.yml.backup
│ └── docker-publish.yml.backup
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── INSTALLATION.md
├── LICENSE
├── Makefile
├── README.md
├── app/
│ ├── __init__.py
│ ├── blueprint_registry.py
│ ├── config/
│ │ ├── __init__.py
│ │ ├── analytics_defaults.py
│ │ └── support_ui.py
│ ├── config.py
│ ├── constants.py
│ ├── integrations/
│ │ ├── __init__.py
│ │ ├── activitywatch.py
│ │ ├── asana.py
│ │ ├── base.py
│ │ ├── caldav_calendar.py
│ │ ├── github.py
│ │ ├── gitlab.py
│ │ ├── google_calendar.py
│ │ ├── jira.py
│ │ ├── linear.py
│ │ ├── microsoft_teams.py
│ │ ├── outlook_calendar.py
│ │ ├── peppol.py
│ │ ├── peppol_as4.py
│ │ ├── peppol_identifiers.py
│ │ ├── peppol_smp.py
│ │ ├── peppol_transport.py
│ │ ├── quickbooks.py
│ │ ├── registry.py
│ │ ├── slack.py
│ │ ├── trello.py
│ │ └── xero.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── activity.py
│ │ ├── api_idempotency_key.py
│ │ ├── api_token.py
│ │ ├── audit_log.py
│ │ ├── budget_alert.py
│ │ ├── calendar_event.py
│ │ ├── calendar_integration.py
│ │ ├── client.py
│ │ ├── client_attachment.py
│ │ ├── client_note.py
│ │ ├── client_notification.py
│ │ ├── client_portal_customization.py
│ │ ├── client_portal_dashboard_preference.py
│ │ ├── client_prepaid_consumption.py
│ │ ├── client_time_approval.py
│ │ ├── comment.py
│ │ ├── comment_attachment.py
│ │ ├── contact.py
│ │ ├── contact_communication.py
│ │ ├── currency.py
│ │ ├── custom_field_definition.py
│ │ ├── custom_report.py
│ │ ├── deal.py
│ │ ├── deal_activity.py
│ │ ├── donation_interaction.py
│ │ ├── expense.py
│ │ ├── expense_category.py
│ │ ├── expense_gps.py
│ │ ├── extra_good.py
│ │ ├── focus_session.py
│ │ ├── gamification.py
│ │ ├── import_export.py
│ │ ├── integration.py
│ │ ├── integration_external_event_link.py
│ │ ├── invoice.py
│ │ ├── invoice_approval.py
│ │ ├── invoice_email.py
│ │ ├── invoice_image.py
│ │ ├── invoice_pdf_template.py
│ │ ├── invoice_peppol.py
│ │ ├── invoice_template.py
│ │ ├── issue.py
│ │ ├── kanban_column.py
│ │ ├── lead.py
│ │ ├── lead_activity.py
│ │ ├── link_template.py
│ │ ├── mileage.py
│ │ ├── payment_gateway.py
│ │ ├── payments.py
│ │ ├── per_diem.py
│ │ ├── permission.py
│ │ ├── project.py
│ │ ├── project_attachment.py
│ │ ├── project_cost.py
│ │ ├── project_stock_allocation.py
│ │ ├── project_template.py
│ │ ├── purchase_order.py
│ │ ├── push_subscription.py
│ │ ├── quote.py
│ │ ├── quote_attachment.py
│ │ ├── quote_image.py
│ │ ├── quote_template.py
│ │ ├── quote_version.py
│ │ ├── rate_override.py
│ │ ├── recurring_block.py
│ │ ├── recurring_invoice.py
│ │ ├── recurring_task.py
│ │ ├── reporting.py
│ │ ├── salesman_email_mapping.py
│ │ ├── saved_filter.py
│ │ ├── settings.py
│ │ ├── stock_item.py
│ │ ├── stock_lot.py
│ │ ├── stock_movement.py
│ │ ├── stock_reservation.py
│ │ ├── supplier.py
│ │ ├── supplier_stock_item.py
│ │ ├── task.py
│ │ ├── task_activity.py
│ │ ├── tax_rule.py
│ │ ├── team_chat.py
│ │ ├── time_entry.py
│ │ ├── time_entry_approval.py
│ │ ├── time_entry_template.py
│ │ ├── time_off.py
│ │ ├── timesheet_period.py
│ │ ├── timesheet_policy.py
│ │ ├── user.py
│ │ ├── user_client.py
│ │ ├── user_favorite_project.py
│ │ ├── user_smart_notification_dismissal.py
│ │ ├── warehouse.py
│ │ ├── warehouse_stock.py
│ │ ├── webhook.py
│ │ ├── weekly_time_goal.py
│ │ └── workflow.py
│ ├── repositories/
│ │ ├── __init__.py
│ │ ├── base_repository.py
│ │ ├── client_repository.py
│ │ ├── comment_repository.py
│ │ ├── expense_repository.py
│ │ ├── invoice_repository.py
│ │ ├── payment_repository.py
│ │ ├── project_repository.py
│ │ ├── recurring_invoice_repository.py
│ │ ├── task_repository.py
│ │ ├── time_entry_repository.py
│ │ └── user_repository.py
│ ├── resources/
│ │ └── icc/
│ │ ├── LICENSE.txt
│ │ └── sRGB-v2-nano.icc
│ ├── routes/
│ │ ├── activity_feed.py
│ │ ├── admin.py
│ │ ├── analytics.py
│ │ ├── api/
│ │ │ ├── __init__.py
│ │ │ └── v1/
│ │ │ └── __init__.py
│ │ ├── api.py
│ │ ├── api_docs.py
│ │ ├── api_v1.py
│ │ ├── api_v1_ai.py
│ │ ├── api_v1_clients.py
│ │ ├── api_v1_common.py
│ │ ├── api_v1_contacts.py
│ │ ├── api_v1_deals.py
│ │ ├── api_v1_expenses.py
│ │ ├── api_v1_invoices.py
│ │ ├── api_v1_leads.py
│ │ ├── api_v1_mileage.py
│ │ ├── api_v1_payments.py
│ │ ├── api_v1_projects.py
│ │ ├── api_v1_tasks.py
│ │ ├── api_v1_time_entries.py
│ │ ├── audit_logs.py
│ │ ├── auth.py
│ │ ├── budget_alerts.py
│ │ ├── calendar.py
│ │ ├── client_notes.py
│ │ ├── client_portal.py
│ │ ├── client_portal_customization.py
│ │ ├── clients.py
│ │ ├── comments.py
│ │ ├── contacts.py
│ │ ├── custom_field_definitions.py
│ │ ├── custom_reports.py
│ │ ├── deals.py
│ │ ├── expense_categories.py
│ │ ├── expenses.py
│ │ ├── gantt.py
│ │ ├── import_export.py
│ │ ├── integrations.py
│ │ ├── inventory.py
│ │ ├── invoice_approvals.py
│ │ ├── invoices.py
│ │ ├── invoices_refactored.py
│ │ ├── issues.py
│ │ ├── kanban.py
│ │ ├── kiosk.py
│ │ ├── leads.py
│ │ ├── link_templates.py
│ │ ├── main.py
│ │ ├── mileage.py
│ │ ├── offers.py
│ │ ├── payment_gateways.py
│ │ ├── payments.py
│ │ ├── per_diem.py
│ │ ├── permissions.py
│ │ ├── project_templates.py
│ │ ├── projects.py
│ │ ├── projects_refactored_example.py
│ │ ├── push_notifications.py
│ │ ├── quotes.py
│ │ ├── recurring_invoices.py
│ │ ├── recurring_tasks.py
│ │ ├── reports.py
│ │ ├── salesman_reports.py
│ │ ├── saved_filters.py
│ │ ├── scheduled_reports.py
│ │ ├── settings.py
│ │ ├── setup.py
│ │ ├── tasks.py
│ │ ├── team_chat.py
│ │ ├── time_approvals.py
│ │ ├── time_entry_templates.py
│ │ ├── timer.py
│ │ ├── timer_refactored.py
│ │ ├── user.py
│ │ ├── webhooks.py
│ │ ├── weekly_goals.py
│ │ ├── workflows.py
│ │ └── workforce.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── client_schema.py
│ │ ├── comment_schema.py
│ │ ├── expense_schema.py
│ │ ├── invoice_schema.py
│ │ ├── payment_schema.py
│ │ ├── project_schema.py
│ │ ├── task_schema.py
│ │ ├── time_entry_schema.py
│ │ ├── user_schema.py
│ │ └── version_check.py
│ ├── services/
│ │ ├── __init__.py
│ │ ├── ai_categorization_service.py
│ │ ├── ai_suggestion_service.py
│ │ ├── analytics_service.py
│ │ ├── api_token_service.py
│ │ ├── backup_service.py
│ │ ├── base_crud_service.py
│ │ ├── calendar_integration_service.py
│ │ ├── client_activity_feed_service.py
│ │ ├── client_approval_service.py
│ │ ├── client_notification_service.py
│ │ ├── client_report_service.py
│ │ ├── client_service.py
│ │ ├── comment_service.py
│ │ ├── currency_service.py
│ │ ├── custom_report_service.py
│ │ ├── email_service.py
│ │ ├── enhanced_ocr_service.py
│ │ ├── expense_service.py
│ │ ├── export_service.py
│ │ ├── gamification_service.py
│ │ ├── gantt_service.py
│ │ ├── global_search_service.py
│ │ ├── gps_tracking_service.py
│ │ ├── health_service.py
│ │ ├── import_service.py
│ │ ├── integration_service.py
│ │ ├── inventory_report_service.py
│ │ ├── invoice_approval_service.py
│ │ ├── invoice_service.py
│ │ ├── ldap_service.py
│ │ ├── llm_service.py
│ │ ├── notification_service.py
│ │ ├── payment_gateway_service.py
│ │ ├── payment_service.py
│ │ ├── peppol_service.py
│ │ ├── permission_service.py
│ │ ├── pomodoro_service.py
│ │ ├── project_service.py
│ │ ├── project_template_service.py
│ │ ├── quote_service.py
│ │ ├── recurring_invoice_service.py
│ │ ├── reporting_service.py
│ │ ├── scheduled_report_service.py
│ │ ├── stats_service.py
│ │ ├── support_prompt_service.py
│ │ ├── task_service.py
│ │ ├── time_approval_service.py
│ │ ├── time_entry_bulk_service.py
│ │ ├── time_entry_csv_import_service.py
│ │ ├── time_tracking_service.py
│ │ ├── unpaid_hours_service.py
│ │ ├── usage_stats_service.py
│ │ ├── user_service.py
│ │ ├── version_service.py
│ │ ├── workflow_engine.py
│ │ └── workforce_governance_service.py
│ ├── static/
│ │ ├── activity-feed.js
│ │ ├── admin-version-update.js
│ │ ├── ai-helper.js
│ │ ├── base-init.js
│ │ ├── calendar.css
│ │ ├── calendar.js
│ │ ├── charts.js
│ │ ├── commands.js
│ │ ├── css/
│ │ │ ├── brand-colors.css
│ │ │ ├── gantt-chart.css
│ │ │ └── rtl-support.css
│ │ ├── dashboard-enhancements.js
│ │ ├── dashboard-widgets.js
│ │ ├── data-tables-enhanced.css
│ │ ├── data-tables-enhanced.js
│ │ ├── date-picker-init.js
│ │ ├── enhanced-search.js
│ │ ├── enhanced-tables.js
│ │ ├── enhanced-ui.css
│ │ ├── enhanced-ui.js
│ │ ├── error-handling-enhanced.js
│ │ ├── floating-actions.js
│ │ ├── floating-timer-bar.js
│ │ ├── form-bridge.css
│ │ ├── form-validation.css
│ │ ├── form-validation.js
│ │ ├── global-fab.js
│ │ ├── idle.js
│ │ ├── images/
│ │ │ └── og-image-placeholder.md
│ │ ├── interactions.js
│ │ ├── js/
│ │ │ ├── command-palette.js
│ │ │ ├── gantt-color-picker.js
│ │ │ ├── integration_wizard.js
│ │ │ ├── ldap_wizard.js
│ │ │ ├── oidc_wizard.js
│ │ │ ├── setup-wizard.js
│ │ │ └── sw.js
│ │ ├── keyboard-shortcuts-advanced.js
│ │ ├── keyboard-shortcuts-enhanced.js
│ │ ├── keyboard-shortcuts.css
│ │ ├── keyboard-shortcuts.js
│ │ ├── kiosk-barcode.js
│ │ ├── kiosk-mode.css
│ │ ├── kiosk-mode.js
│ │ ├── kiosk-timer.js
│ │ ├── manifest.json
│ │ ├── mentions.js
│ │ ├── mobile.js
│ │ ├── offline-sync.js
│ │ ├── onboarding-enhanced.js
│ │ ├── onboarding.js
│ │ ├── pwa-enhancements.js
│ │ ├── quick-actions.js
│ │ ├── reports-enhanced.js
│ │ ├── smart-notifications.js
│ │ ├── src/
│ │ │ └── input.css
│ │ ├── support-ui.js
│ │ ├── test.txt
│ │ ├── time-entries-inline-edit.js
│ │ ├── toast-notifications.css
│ │ ├── toast-notifications.js
│ │ ├── typing-utils.js
│ │ ├── ui-enhancements.css
│ │ ├── ui-enhancements.js
│ │ └── uploads/
│ │ └── logos/
│ │ └── .gitkeep
│ ├── telemetry/
│ │ ├── __init__.py
│ │ ├── otel_setup.py
│ │ └── service.py
│ ├── templates/
│ │ ├── _components.html
│ │ ├── admin/
│ │ │ ├── api_tokens.html
│ │ │ ├── backups.html
│ │ │ ├── clear_cache.html
│ │ │ ├── custom_field_definitions/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ ├── dashboard.html
│ │ │ ├── email_support.html
│ │ │ ├── email_templates/
│ │ │ │ ├── create.html
│ │ │ │ ├── edit.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── integrations/
│ │ │ │ ├── list.html
│ │ │ │ └── setup.html
│ │ │ ├── ldap_setup_wizard.html
│ │ │ ├── link_templates/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ ├── modules.html
│ │ │ ├── oidc_debug.html
│ │ │ ├── oidc_setup_wizard.html
│ │ │ ├── oidc_user_detail.html
│ │ │ ├── pdf_layout.html
│ │ │ ├── permissions/
│ │ │ │ └── list.html
│ │ │ ├── quote_pdf_layout.html
│ │ │ ├── restore.html
│ │ │ ├── roles/
│ │ │ │ ├── form.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── salesman_email_mappings.html
│ │ │ ├── settings.html
│ │ │ ├── system_info.html
│ │ │ ├── telemetry.html
│ │ │ ├── user_form.html
│ │ │ ├── users/
│ │ │ │ └── roles.html
│ │ │ ├── users.html
│ │ │ └── webhooks/
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── analytics/
│ │ │ ├── dashboard.html
│ │ │ ├── dashboard_improved.html
│ │ │ └── mobile_dashboard.html
│ │ ├── approvals/
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── audit_logs/
│ │ │ ├── entity_history.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── auth/
│ │ │ ├── change_password.html
│ │ │ ├── edit_profile.html
│ │ │ ├── emails/
│ │ │ │ └── password_reset.html
│ │ │ ├── forgot_password.html
│ │ │ ├── login.html
│ │ │ ├── profile.html
│ │ │ ├── reset_password.html
│ │ │ ├── two_factor.html
│ │ │ └── two_factor_setup.html
│ │ ├── base.html
│ │ ├── budget/
│ │ │ ├── dashboard.html
│ │ │ └── project_detail.html
│ │ ├── calendar/
│ │ │ ├── event_detail.html
│ │ │ ├── event_form.html
│ │ │ ├── integrations.html
│ │ │ └── view.html
│ │ ├── chat/
│ │ │ ├── channel.html
│ │ │ └── index.html
│ │ ├── client_notes/
│ │ │ └── edit.html
│ │ ├── client_portal/
│ │ │ ├── activity_feed.html
│ │ │ ├── approval_detail.html
│ │ │ ├── approvals.html
│ │ │ ├── base.html
│ │ │ ├── dashboard.html
│ │ │ ├── documents.html
│ │ │ ├── error.html
│ │ │ ├── invoice_detail.html
│ │ │ ├── invoices.html
│ │ │ ├── issue_detail.html
│ │ │ ├── issues.html
│ │ │ ├── login.html
│ │ │ ├── new_issue.html
│ │ │ ├── notifications.html
│ │ │ ├── project_comments.html
│ │ │ ├── projects.html
│ │ │ ├── quote_detail.html
│ │ │ ├── quotes.html
│ │ │ ├── reports.html
│ │ │ ├── set_password.html
│ │ │ ├── time_entries.html
│ │ │ └── widgets/
│ │ │ ├── invoices.html
│ │ │ ├── pending_actions.html
│ │ │ ├── projects.html
│ │ │ ├── stats.html
│ │ │ └── time_entries.html
│ │ ├── clients/
│ │ │ ├── _clients_list.html
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── comments/
│ │ │ ├── _comment.html
│ │ │ ├── _comments_section.html
│ │ │ └── edit.html
│ │ ├── components/
│ │ │ ├── activity_feed_widget.html
│ │ │ ├── bulk_actions_widget.html
│ │ │ ├── cards.html
│ │ │ ├── chat_user_selector.html
│ │ │ ├── client_select.html
│ │ │ ├── keyboard_shortcuts_help.html
│ │ │ ├── multi_select.html
│ │ │ ├── offline_indicator.html
│ │ │ ├── persistent_chat_widget.html
│ │ │ ├── save_filter_widget.html
│ │ │ ├── support_modal.html
│ │ │ └── ui.html
│ │ ├── contacts/
│ │ │ ├── communication_form.html
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── deals/
│ │ │ ├── activity_form.html
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ ├── pipeline.html
│ │ │ └── view.html
│ │ ├── email/
│ │ │ ├── client_notification.html
│ │ │ ├── client_portal_password_setup.html
│ │ │ ├── comment_mention.html
│ │ │ ├── invoice.html
│ │ │ ├── overdue_invoice.html
│ │ │ ├── quote.html
│ │ │ ├── quote_accepted.html
│ │ │ ├── quote_approval_rejected.html
│ │ │ ├── quote_approval_request.html
│ │ │ ├── quote_approved.html
│ │ │ ├── quote_expired.html
│ │ │ ├── quote_expiring.html
│ │ │ ├── quote_rejected.html
│ │ │ ├── quote_sent.html
│ │ │ ├── scheduled_report.html
│ │ │ ├── task_assigned.html
│ │ │ ├── test_email.html
│ │ │ ├── unpaid_hours_report.html
│ │ │ └── weekly_summary.html
│ │ ├── errors/
│ │ │ ├── 400.html
│ │ │ ├── 403.html
│ │ │ ├── 404.html
│ │ │ ├── 500.html
│ │ │ └── generic.html
│ │ ├── expense_categories/
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── expenses/
│ │ │ ├── dashboard.html
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── gantt/
│ │ │ └── view.html
│ │ ├── import_export/
│ │ │ └── index.html
│ │ ├── integrations/
│ │ │ ├── activitywatch_setup.html
│ │ │ ├── caldav_setup.html
│ │ │ ├── health.html
│ │ │ ├── list.html
│ │ │ ├── manage.html
│ │ │ ├── view.html
│ │ │ ├── wizard_asana.html
│ │ │ ├── wizard_base.html
│ │ │ ├── wizard_github.html
│ │ │ ├── wizard_gitlab.html
│ │ │ ├── wizard_jira.html
│ │ │ ├── wizard_microsoft_teams.html
│ │ │ ├── wizard_outlook_calendar.html
│ │ │ ├── wizard_quickbooks.html
│ │ │ ├── wizard_trello.html
│ │ │ └── wizard_xero.html
│ │ ├── inventory/
│ │ │ ├── adjustments/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ ├── low_stock/
│ │ │ │ └── list.html
│ │ │ ├── movements/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ ├── purchase_orders/
│ │ │ │ ├── form.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── reports/
│ │ │ │ ├── dashboard.html
│ │ │ │ ├── low_stock.html
│ │ │ │ ├── movement_history.html
│ │ │ │ ├── turnover.html
│ │ │ │ └── valuation.html
│ │ │ ├── reservations/
│ │ │ │ └── list.html
│ │ │ ├── stock_items/
│ │ │ │ ├── form.html
│ │ │ │ ├── history.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── stock_levels/
│ │ │ │ ├── item.html
│ │ │ │ ├── list.html
│ │ │ │ └── warehouse.html
│ │ │ ├── suppliers/
│ │ │ │ ├── form.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── transfers/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ └── warehouses/
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── invoice_approvals/
│ │ │ ├── list.html
│ │ │ ├── request.html
│ │ │ └── view.html
│ │ ├── invoices/
│ │ │ ├── _invoices_list.html
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── generate_from_time.html
│ │ │ ├── list.html
│ │ │ ├── pdf_default.html
│ │ │ ├── pdf_styles_default.css
│ │ │ └── view.html
│ │ ├── issues/
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ ├── new.html
│ │ │ └── view.html
│ │ ├── kanban/
│ │ │ ├── board.html
│ │ │ ├── columns.html
│ │ │ ├── create_column.html
│ │ │ └── edit_column.html
│ │ ├── kiosk/
│ │ │ ├── base.html
│ │ │ ├── dashboard.html
│ │ │ └── login.html
│ │ ├── leads/
│ │ │ ├── activity_form.html
│ │ │ ├── convert_to_client.html
│ │ │ ├── convert_to_deal.html
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── main/
│ │ │ ├── about.html
│ │ │ ├── dashboard.html
│ │ │ ├── donate.html
│ │ │ ├── help.html
│ │ │ └── search.html
│ │ ├── mileage/
│ │ │ ├── form.html
│ │ │ ├── gps.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── offline.html
│ │ ├── partials/
│ │ │ ├── _bottom_nav.html
│ │ │ └── _command_palette.html
│ │ ├── payment_gateways/
│ │ │ ├── create.html
│ │ │ ├── list.html
│ │ │ └── pay.html
│ │ ├── payments/
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── per_diem/
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ ├── rate_form.html
│ │ │ ├── rates_list.html
│ │ │ └── view.html
│ │ ├── project_templates/
│ │ │ ├── create.html
│ │ │ ├── create_project.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── projects/
│ │ │ ├── _kanban_tailwind.html
│ │ │ ├── _projects_list.html
│ │ │ ├── add_cost.html
│ │ │ ├── add_good.html
│ │ │ ├── archive.html
│ │ │ ├── create.html
│ │ │ ├── dashboard.html
│ │ │ ├── edit.html
│ │ │ ├── edit_cost.html
│ │ │ ├── edit_good.html
│ │ │ ├── goods.html
│ │ │ ├── list.html
│ │ │ ├── time_entries_overview.html
│ │ │ └── view.html
│ │ ├── quotes/
│ │ │ ├── _edit_quote_form_scripts.html
│ │ │ ├── _quotes_list.html
│ │ │ ├── accept.html
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ ├── pdf_default.html
│ │ │ ├── pdf_styles_default.css
│ │ │ └── view.html
│ │ ├── recurring_invoices/
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── recurring_tasks/
│ │ │ ├── form.html
│ │ │ └── list.html
│ │ ├── reports/
│ │ │ ├── builder.html
│ │ │ ├── custom_view.html
│ │ │ ├── export_form.html
│ │ │ ├── index.html
│ │ │ ├── iterative_view.html
│ │ │ ├── project_report.html
│ │ │ ├── saved_views_list.html
│ │ │ ├── schedule_form.html
│ │ │ ├── scheduled.html
│ │ │ ├── summary.html
│ │ │ ├── task_report.html
│ │ │ ├── time_entries_report.html
│ │ │ ├── unpaid_hours_report.html
│ │ │ ├── user_report.html
│ │ │ └── week_in_review.html
│ │ ├── saved_filters/
│ │ │ └── list.html
│ │ ├── settings/
│ │ │ └── keyboard_shortcuts.html
│ │ ├── setup/
│ │ │ └── initial_setup.html
│ │ ├── tasks/
│ │ │ ├── _kanban.html
│ │ │ ├── _tasks_list.html
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ ├── my_tasks.html
│ │ │ ├── overdue.html
│ │ │ └── view.html
│ │ ├── time_entry_templates/
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── timer/
│ │ │ ├── _time_entries_list.html
│ │ │ ├── bulk_entry.html
│ │ │ ├── calendar.html
│ │ │ ├── edit_timer.html
│ │ │ ├── manual_entry.html
│ │ │ ├── time_entries_export_pdf.html
│ │ │ ├── time_entries_overview.html
│ │ │ ├── timer_page.html
│ │ │ └── view_timer.html
│ │ ├── user/
│ │ │ ├── license.html
│ │ │ ├── profile.html
│ │ │ └── settings.html
│ │ ├── weekly_goals/
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── index.html
│ │ │ └── view.html
│ │ └── workforce/
│ │ └── dashboard.html
│ └── utils/
│ ├── api_auth.py
│ ├── api_deprecation.py
│ ├── api_idempotency.py
│ ├── api_rate_limit.py
│ ├── api_responses.py
│ ├── audit.py
│ ├── auth_method.py
│ ├── backup.py
│ ├── budget_forecasting.py
│ ├── cache.py
│ ├── cache_redis.py
│ ├── cii_invoice.py
│ ├── cli.py
│ ├── client_lock.py
│ ├── config_manager.py
│ ├── context_processors.py
│ ├── data_export.py
│ ├── data_import.py
│ ├── datetime_utils.py
│ ├── db.py
│ ├── decorators.py
│ ├── donate_hide_code.py
│ ├── email.py
│ ├── env_validation.py
│ ├── error_handlers.py
│ ├── error_handling.py
│ ├── event_bus.py
│ ├── excel_export.py
│ ├── file_upload.py
│ ├── i18n.py
│ ├── i18n_helpers.py
│ ├── installation.py
│ ├── integration_http.py
│ ├── integration_sync_context.py
│ ├── invoice_numbering.py
│ ├── invoice_pdf_postprocess.py
│ ├── invoice_validators.py
│ ├── keyboard_shortcuts_defaults.py
│ ├── legacy_migrations.py
│ ├── license_utils.py
│ ├── logger.py
│ ├── mileage_pdf.py
│ ├── module_helpers.py
│ ├── module_registry.py
│ ├── ocr.py
│ ├── oidc_metadata.py
│ ├── overtime.py
│ ├── pagination.py
│ ├── pdf_generator.py
│ ├── pdf_generator_fallback.py
│ ├── pdf_generator_reportlab.py
│ ├── pdf_template_schema.py
│ ├── pdfa3.py
│ ├── per_diem_pdf.py
│ ├── performance.py
│ ├── permissions.py
│ ├── permissions_seed.py
│ ├── posthog_funnels.py
│ ├── posthog_monitoring.py
│ ├── posthog_segmentation.py
│ ├── powerpoint_export.py
│ ├── prepaid_hours.py
│ ├── query_logging.py
│ ├── query_optimization.py
│ ├── quote_access.py
│ ├── rate_limiting.py
│ ├── role_migration.py
│ ├── route_helpers.py
│ ├── safe_template_render.py
│ ├── scheduled_tasks.py
│ ├── scope_filter.py
│ ├── search.py
│ ├── secret_crypto.py
│ ├── seed_dev_data.py
│ ├── setup_logging.py
│ ├── stripe_integration.py
│ ├── summary_report_pdf.py
│ ├── support_report_generation.py
│ ├── telemetry.py
│ ├── template_filters.py
│ ├── time_entries_pdf.py
│ ├── time_entry_validation.py
│ ├── time_rounding.py
│ ├── timezone.py
│ ├── transactions.py
│ ├── validation.py
│ ├── version_compare.py
│ ├── webhook_dispatcher.py
│ ├── webhook_service.py
│ └── zugferd.py
├── app.py
├── babel.cfg
├── crowdin.yml
├── desktop/
│ ├── .npmrc
│ ├── README.md
│ ├── assets/
│ │ ├── .gitkeep
│ │ ├── README.md
│ │ └── icon.icns
│ ├── dist-renderer/
│ │ ├── assets/
│ │ │ ├── index-BqW2gGjC.js
│ │ │ └── index-D2aGha3a.css
│ │ └── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── scripts/
│ │ ├── build-all-platforms.js
│ │ └── clean-cache.js
│ ├── src/
│ │ ├── main/
│ │ │ ├── main.js
│ │ │ ├── preload.js
│ │ │ ├── tray.js
│ │ │ └── window.js
│ │ ├── renderer/
│ │ │ ├── css/
│ │ │ │ ├── brand-colors.css
│ │ │ │ ├── splash.css
│ │ │ │ └── styles.css
│ │ │ ├── index.html
│ │ │ ├── js/
│ │ │ │ ├── api/
│ │ │ │ │ └── client.js
│ │ │ │ ├── app.js
│ │ │ │ ├── bundle.js
│ │ │ │ ├── connection/
│ │ │ │ │ ├── connection_manager.js
│ │ │ │ │ ├── connection_state.js
│ │ │ │ │ ├── request_policy.js
│ │ │ │ │ └── timer_operations.js
│ │ │ │ ├── state.js
│ │ │ │ ├── storage/
│ │ │ │ │ └── storage.js
│ │ │ │ ├── ui/
│ │ │ │ │ └── notifications.js
│ │ │ │ └── utils/
│ │ │ │ └── helpers.js
│ │ │ └── splash.html
│ │ ├── renderer-react/
│ │ │ ├── index.html
│ │ │ └── src/
│ │ │ ├── main.jsx
│ │ │ ├── services/
│ │ │ │ ├── api.js
│ │ │ │ ├── diagnostics.js
│ │ │ │ └── store.js
│ │ │ ├── styles/
│ │ │ │ └── app.css
│ │ │ └── sync/
│ │ │ └── syncEngine.js
│ │ └── shared/
│ │ └── config.js
│ ├── test/
│ │ ├── api-client.test.js
│ │ ├── connection_manager.test.js
│ │ ├── integration_info_server.test.js
│ │ ├── react_renderer_package.test.js
│ │ └── timer_operations.test.js
│ └── vite.config.mjs
├── docker/
│ ├── Dockerfile.certgen
│ ├── Dockerfile.mkcert
│ ├── STARTUP_MIGRATION_CONFIG.md
│ ├── TROUBLESHOOTING_DB_CONNECTION.md
│ ├── debug_startup.sh
│ ├── docker-compose.analytics.yml
│ ├── docker-compose.https-auto.yml
│ ├── docker-compose.https-mkcert.yml
│ ├── docker-compose.local-test.yml
│ ├── docker-compose.remote-dev.yml
│ ├── docker-compose.remote.yml
│ ├── docs
│ ├── entrypoint-local-test-simple.sh
│ ├── entrypoint-local-test.sh
│ ├── entrypoint.py
│ ├── entrypoint.sh
│ ├── entrypoint_fixed.sh
│ ├── entrypoint_simple.sh
│ ├── fix-all-column-issues.py
│ ├── fix-all-issues.py
│ ├── fix-column-name-mismatch.py
│ ├── fix-docker-permissions.py
│ ├── fix-docker-permissions.sh
│ ├── fix-duplicate-columns.py
│ ├── fix-invoice-tables.py
│ ├── fix-invoices-now.py
│ ├── fix-permissions-aggressive.py
│ ├── fix-schema.py
│ ├── fix-settings-table.py
│ ├── fix-settings-table.sql
│ ├── fix-upload-permissions.py
│ ├── fix-upload-permissions.sh
│ ├── force-schema-update.py
│ ├── generate-mkcert-certs.sh
│ ├── init-database-enhanced.py
│ ├── init-database-simple.py
│ ├── init-database-sql.py
│ ├── init-database.py
│ ├── init-db.sh
│ ├── init.sh
│ ├── init.sql
│ ├── logrotate.conf.example
│ ├── migrate-add-company-branding.py
│ ├── migrate-add-missing-settings-columns.py
│ ├── migrate-add-project-costs.py
│ ├── migrate-add-task-columns.py
│ ├── migrate-add-tasks.py
│ ├── migrate-avatar-storage.py
│ ├── migrate-field-names.py
│ ├── migrate-logo-upload.py
│ ├── seed-dev-data.sh
│ ├── simple_test.sh
│ ├── start-enhanced.py
│ ├── start-fixed.py
│ ├── start-fixed.sh
│ ├── start-minimal.sh
│ ├── start-new.sh
│ ├── start-simple.sh
│ ├── start.py
│ ├── start.sh
│ ├── startup_with_migration.py
│ ├── supervisord.conf
│ ├── test-database-complete.py
│ ├── test-db-simple.py
│ ├── test-db.py
│ ├── test-packages.py
│ ├── test-pdf-generation.py
│ ├── test-permissions.py
│ ├── test-routing.py
│ ├── test-schema-fixed.py
│ ├── test-schema.py
│ ├── test-startup.sh
│ ├── test_db_connection.py
│ └── verify-database.py
├── docker-compose.example.yml
├── docker-compose.yml
├── docs/
│ ├── ADVANCED_PERMISSIONS.md
│ ├── API.md
│ ├── APPLY_FIXES_NOW.md
│ ├── APPLY_KANBAN_MIGRATION.md
│ ├── ARCHITECTURE.md
│ ├── ARCHITECTURE_AUDIT.md
│ ├── ASSETS.md
│ ├── AVATAR_PERSISTENCE_SUMMARY.md
│ ├── AVATAR_STORAGE_MIGRATION.md
│ ├── BRANDING.md
│ ├── BRAND_GUIDELINES.md
│ ├── BREAK_TIME_FEATURE.md
│ ├── BUDGET_ALERTS_AND_FORECASTING.md
│ ├── BUILD_CONFIGURATION.md
│ ├── BUILD_SCRIPTS.md
│ ├── BUILD_WINDOWS_PERMISSIONS.md
│ ├── BULK_TASK_OPERATIONS.md
│ ├── BULK_TIME_ENTRY_README.md
│ ├── CALENDAR_AGENDA_FEATURE.md
│ ├── CALENDAR_FEATURES_README.md
│ ├── CLIENT_FEATURES_COMPLETE_IMPLEMENTATION.md
│ ├── CLIENT_FEATURES_FINAL_IMPLEMENTATION.md
│ ├── CLIENT_FEATURES_IMPLEMENTATION.md
│ ├── CLIENT_FEATURES_IMPLEMENTATION_STATUS.md
│ ├── CLIENT_FEATURE_RECOMMENDATIONS.md
│ ├── CLIENT_MANAGEMENT_README.md
│ ├── CLIENT_NOTES_FEATURE.md
│ ├── CLIENT_PORTAL.md
│ ├── CODEBASE_AUDIT.md
│ ├── CODE_BASED_ANALYSIS_REPORT.md
│ ├── COMMAND_PALETTE_DEMO.html
│ ├── COMMAND_PALETTE_USAGE.md
│ ├── COMPLETE_IMPROVEMENTS_SUMMARY.md
│ ├── CONTRIBUTING_TRANSLATIONS.md
│ ├── CRM_FEATURES_IMPLEMENTATION.md
│ ├── CRM_IMPLEMENTATION_SUMMARY.md
│ ├── DATABASE_RECOVERY.md
│ ├── DATABASE_STARTUP_FIX_README.md
│ ├── DEFAULT_DATA_SEEDING.md
│ ├── DESKTOP_SETTINGS.md
│ ├── DEVELOPMENT.md
│ ├── DIAGNOSIS_STEPS.md
│ ├── DOCS_AUDIT.md
│ ├── DOCUMENTATION_REORGANIZATION_SUMMARY.md
│ ├── DOCUMENTATION_RESTRUCTURE_SUMMARY.md
│ ├── ENHANCED_DATABASE_STARTUP.md
│ ├── ENHANCED_INVOICE_SYSTEM_README.md
│ ├── ERROR_HANDLER_IMPROVEMENTS.md
│ ├── EXPENSE_TRACKING.md
│ ├── EXTRA_GOODS_FEATURE.md
│ ├── FAVORITE_PROJECTS_FEATURE.md
│ ├── FEATURES_COMPLETE.md
│ ├── FINAL_SYMLINK_FIX.md
│ ├── FIX_SYMLINK_ISSUE.md
│ ├── FIX_SYMLINK_PERMISSIONS.md
│ ├── FRONTEND.md
│ ├── GETTING_STARTED.md
│ ├── GITHUB_WORKFLOW_IMAGES.md
│ ├── IMPLEMENTATION_COMPLETE_SUMMARY.md
│ ├── IMPLEMENTATION_STATUS_UPDATE.md
│ ├── IMPORT_EXPORT_GUIDE.md
│ ├── INCOMPLETE_IMPLEMENTATIONS_ANALYSIS.md
│ ├── INVOICE_EXPENSES.md
│ ├── INVOICE_EXTRA_GOODS_PDF_EXPORT.md
│ ├── INVOICE_FEATURE_README.md
│ ├── INVOICE_INTERFACE_IMPROVEMENTS.md
│ ├── KEYBOARD_SHORTCUTS_DEVELOPER.md
│ ├── KEYBOARD_SHORTCUTS_IMPLEMENTATION.md
│ ├── KIOSK_MODE_INVENTORY_ANALYSIS.md
│ ├── KIOSK_MODE_INVENTORY_SUMMARY.md
│ ├── KIOSK_REVIEW_AND_IMPROVEMENTS.md
│ ├── LOGO_UPLOAD_IMPLEMENTATION_SUMMARY.md
│ ├── LOGO_UPLOAD_SYSTEM_README.md
│ ├── MOBILE_IMPROVEMENTS.md
│ ├── MULTISELECT_FILTERS_TESTING.md
│ ├── ONEDRIVE_FIX.md
│ ├── PAYMENT_TRACKING.md
│ ├── PDF_EDITOR_ENHANCED_FEATURES.md
│ ├── PDF_EDITOR_QUICK_START.md
│ ├── PDF_GENERATION_TROUBLESHOOTING.md
│ ├── PDF_LAYOUT_CUSTOMIZATION.md
│ ├── PERFORMANCE.md
│ ├── PRODUCT_UX_AUDIT.md
│ ├── PROFILE_PICTURE_UPLOAD_FIX.md
│ ├── PROJECT_ANALYSIS_REPORT.md
│ ├── PROJECT_ARCHIVING_GUIDE.md
│ ├── QUICK_FIX.md
│ ├── QUICK_FIX_SYMLINK.md
│ ├── QUICK_REFERENCE_GUIDE.md
│ ├── QUICK_START_CODE_SIGNING.md
│ ├── QUICK_WINS_IMPLEMENTATION.md
│ ├── QUICK_WINS_UI.md
│ ├── README.md
│ ├── REPORTLAB_MIGRATION_CHECKLIST.md
│ ├── REPORTLAB_MIGRATION_SUMMARY.md
│ ├── REQUIREMENTS.md
│ ├── SOLUTION_GUIDE.md
│ ├── SUBCONTRACTOR_ROLE.md
│ ├── TASK_MANAGEMENT.md
│ ├── TASK_MANAGEMENT_README.md
│ ├── TELEMETRY_QUICK_START.md
│ ├── TELEMETRY_TRANSPARENCY.md
│ ├── TESTING_COVERAGE_GUIDE.md
│ ├── TESTING_QUICK_REFERENCE.md
│ ├── TEST_AVATAR_PERSISTENCE.md
│ ├── TIMETRACKER_TEMPLATES_IMPLEMENTATION.md
│ ├── TIME_ENTRY_TEMPLATES.md
│ ├── TIME_ROUNDING_PREFERENCES.md
│ ├── TOAST_NOTIFICATION_DEMO.html
│ ├── TOAST_NOTIFICATION_SYSTEM.md
│ ├── TOAST_NOTIFICATION_VISUAL_GUIDE.md
│ ├── TRANSLATION_SYSTEM.md
│ ├── TROUBLESHOOTING_BUILD.md
│ ├── TROUBLESHOOTING_OIDC_DNS.md
│ ├── TROUBLESHOOTING_QUOTES_TEMPLATE_ID.md
│ ├── TROUBLESHOOTING_TRANSACTION_ERROR.md
│ ├── UI_GUIDELINES.md
│ ├── UPLOADS_PERSISTENCE.md
│ ├── WEEKLY_TIME_GOALS.md
│ ├── WINDOWS_BUILD.md
│ ├── WINDOWS_CODE_SIGNING.md
│ ├── admin/
│ │ ├── README.md
│ │ ├── SUPPORT_CONVERSION_AB_TESTS.md
│ │ ├── configuration/
│ │ │ ├── DESKTOP_BUILD_WINDOWS_TROUBLESHOOTING.md
│ │ │ ├── DOCKER_COMPOSE_SETUP.md
│ │ │ ├── DOCKER_PUBLIC_SETUP.md
│ │ │ ├── DOCKER_STARTUP_TROUBLESHOOTING.md
│ │ │ ├── EMAIL_CONFIGURATION.md
│ │ │ ├── LDAP_SETUP.md
│ │ │ ├── NGINX_PUBLIC_DOMAIN.md
│ │ │ ├── OIDC_SETUP.md
│ │ │ ├── PEPPOL_EINVOICING.md
│ │ │ └── SUPPORT_VISIBILITY.md
│ │ ├── deployment/
│ │ │ ├── OFFICIAL_BUILDS.md
│ │ │ ├── PORTAINER_DEPLOYMENT.md
│ │ │ ├── RELEASE_PROCESS.md
│ │ │ └── VERSION_MANAGEMENT.md
│ │ ├── monitoring/
│ │ │ ├── ANALYTICS_FILES_MANIFEST.md
│ │ │ ├── ANALYTICS_IMPLEMENTATION_SUMMARY.md
│ │ │ ├── ANALYTICS_QUICK_START.md
│ │ │ ├── POSTHOG_ADVANCED_FEATURES.md
│ │ │ ├── POSTHOG_ENHANCEMENTS_SUMMARY.md
│ │ │ ├── POSTHOG_QUICK_REFERENCE.md
│ │ │ ├── README_TELEMETRY_POLICY.md
│ │ │ ├── TELEMETRY_CHEAT_SHEET.md
│ │ │ ├── TELEMETRY_IMPLEMENTATION_SUMMARY.md
│ │ │ └── TELEMETRY_POSTHOG_MIGRATION.md
│ │ └── security/
│ │ ├── AUTOMATIC_HTTPS_SUMMARY.md
│ │ ├── CSRF_CONFIGURATION.md
│ │ ├── CSRF_DOCKER_CONFIGURATION_SUMMARY.md
│ │ ├── CSRF_INTEGRATION_REVIEW.md
│ │ ├── CSRF_IP_ACCESS_FIX.md
│ │ ├── CSRF_IP_ACCESS_GUIDE.md
│ │ ├── CSRF_IP_FIX_SUMMARY.md
│ │ ├── CSRF_TOKEN_FIX_SUMMARY.md
│ │ ├── CSRF_TROUBLESHOOTING.md
│ │ ├── HTTPS_MKCERT_GUIDE.md
│ │ ├── P0_SECURITY_IMPROVEMENTS.md
│ │ ├── README_HTTPS.md
│ │ └── README_HTTPS_AUTO.md
│ ├── all_tracked_events.md
│ ├── analytics.md
│ ├── api/
│ │ ├── API_CONSISTENCY_AUDIT.md
│ │ ├── API_ENHANCEMENTS.md
│ │ ├── API_TOKEN_SCOPES.md
│ │ ├── API_VERSIONING.md
│ │ ├── README.md
│ │ ├── RESPONSE_FORMAT.md
│ │ └── REST_API.md
│ ├── assets/
│ │ └── README.md
│ ├── bugfixes/
│ │ └── template_application_fix.md
│ ├── cicd/
│ │ ├── BUILD_CONFIGURATION_SUMMARY.md
│ │ ├── CI_CD_DOCUMENTATION.md
│ │ ├── CI_CD_FIXES.md
│ │ ├── CI_CD_FIXES_ROUND_2.md
│ │ ├── CI_CD_IMPLEMENTATION_SUMMARY.md
│ │ ├── CI_CD_QUICK_START.md
│ │ ├── CI_CD_WORKFLOW_ARCHITECTURE.md
│ │ ├── FINAL_CI_CD_SUMMARY.md
│ │ ├── GITHUB_ACTIONS_SETUP.md
│ │ ├── GITHUB_ACTIONS_VERIFICATION.md
│ │ ├── PIPELINE_CLEANUP_PLAN.md
│ │ ├── QUICK_REFERENCE_TESTING.md
│ │ ├── QUICK_START_BUILD.md
│ │ ├── README_BUILD_CONFIGURATION.md
│ │ ├── README_CI_CD_SECTION.md
│ │ ├── STREAMLINED_CI_CD.md
│ │ └── TESTING_WORKFLOW_STRATEGY.md
│ ├── competitive-analysis/
│ │ ├── GAP_RUBRIC.md
│ │ ├── PHASE_1_PRD.md
│ │ ├── PHASE_2_PRD.md
│ │ └── README.md
│ ├── deploy/
│ │ └── RENDER.md
│ ├── development/
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── CONTRIBUTOR_GUIDE.md
│ │ ├── FRONTEND_QUALITY_GATES.md
│ │ ├── LOCAL_DEVELOPMENT_WITH_ANALYTICS.md
│ │ ├── LOCAL_TESTING_WITH_SQLITE.md
│ │ ├── MODULE_INTEGRATION_IMPLEMENTATION.md
│ │ ├── MODULE_INTEGRATION_PLAN.md
│ │ ├── MODULE_STRUCTURE_ANALYSIS.md
│ │ ├── PROJECT_STRUCTURE.md
│ │ ├── RBAC_PERMISSION_MODEL.md
│ │ ├── README.md
│ │ ├── SEED_DEV_DATA.md
│ │ └── SERVICE_LAYER_AND_BASE_CRUD.md
│ ├── events.md
│ ├── features/
│ │ ├── ALEMBIC_MIGRATION_README.md
│ │ ├── ALEMBIC_MIGRATION_SUMMARY.md
│ │ ├── BADGES.md
│ │ ├── CALDAV_INTEGRATION.md
│ │ ├── CALDAV_QUICK_SETUP.md
│ │ ├── CALENDAR_QUICK_START.md
│ │ ├── CALENDAR_QUICK_WINS_SUMMARY.md
│ │ ├── CALENDAR_QUICK_WINS_VISUAL_GUIDE.md
│ │ ├── CSV_EXPORT_ENHANCED.md
│ │ ├── INVENTORY_IMPLEMENTATION_STATUS.md
│ │ ├── INVENTORY_MANAGEMENT_PLAN.md
│ │ ├── INVENTORY_MISSING_FEATURES.md
│ │ ├── KEYBOARD_AND_NOTIFICATIONS_FIX.md
│ │ ├── KEYBOARD_SHORTCUTS_ENHANCED.md
│ │ ├── KEYBOARD_SHORTCUTS_FINAL_FIX.md
│ │ ├── KEYBOARD_SHORTCUTS_FIXED.md
│ │ ├── KEYBOARD_SHORTCUTS_README.md
│ │ ├── LAYOUT_IMPROVEMENTS_COMPLETE.md
│ │ ├── MIGRATION_INSTRUCTIONS.md
│ │ ├── MULTISELECT_FILTERS.md
│ │ ├── OVERTIME_TRACKING.md
│ │ ├── PROJECT_COSTS_FEATURE.md
│ │ ├── PROJECT_COSTS_IMPLEMENTATION_SUMMARY.md
│ │ ├── PROJECT_DASHBOARD.md
│ │ ├── QUICK_START_PROJECT_COSTS.md
│ │ ├── RUN_BLACK_FORMATTING.md
│ │ ├── SMART_NOTIFICATIONS.md
│ │ ├── TIME_ENTRY_DUPLICATION.md
│ │ ├── TIME_ENTRY_TEMPLATES.md
│ │ ├── USER_DELETION.md
│ │ ├── WORKFORCE_DELETE.md
│ │ ├── activity_feed.md
│ │ ├── kanban/
│ │ │ ├── CUSTOM_KANBAN_README.md
│ │ │ ├── DEBUG_KANBAN_COLUMNS.md
│ │ │ ├── KANBAN_AUTO_REFRESH_COMPLETE.md
│ │ │ ├── KANBAN_CUSTOMIZATION.md
│ │ │ ├── KANBAN_REFRESH_FINAL_FIX.md
│ │ │ └── KANBAN_REFRESH_SOLUTION.md
│ │ └── webhooks.md
│ ├── guides/
│ │ ├── DEPLOYMENT_GUIDE.md
│ │ ├── IMPROVEMENTS_QUICK_REFERENCE.md
│ │ ├── QUICK_START_GUIDE.md
│ │ ├── QUICK_START_LOCAL_DEVELOPMENT.md
│ │ └── README.md
│ ├── implementation-notes/
│ │ ├── ACTIVITY_LOGGING_INTEGRATION_GUIDE.md
│ │ ├── ADVANCED_FEATURES_IMPLEMENTATION_GUIDE.md
│ │ ├── ADVANCED_PERMISSIONS_IMPLEMENTATION_SUMMARY.md
│ │ ├── ADVANCED_REPORT_BUILDER_IMPLEMENTATION.md
│ │ ├── ANALYTICS_IMPROVEMENTS_SUMMARY.md
│ │ ├── APPLICATION_REVIEW_2025.md
│ │ ├── ARCHITECTURE_MIGRATION_GUIDE.md
│ │ ├── AVATAR_PERSISTENCE_CHANGELOG.md
│ │ ├── BROWSER_CACHE_FIX.md
│ │ ├── BUGFIX_DB_IMPORT.md
│ │ ├── BUGFIX_METADATA_RESERVED.md
│ │ ├── BULK_OPERATIONS_IMPROVEMENTS.md
│ │ ├── CALENDAR_IMPROVEMENTS_SUMMARY.md
│ │ ├── CHANGES_SUMMARY.md
│ │ ├── CHANGES_SUMMARY_TESTING_WORKFLOW.md
│ │ ├── COMMAND_PALETTE_CHANGELOG.md
│ │ ├── COMMAND_PALETTE_IMPROVEMENTS.md
│ │ ├── COMMENT_ATTACHMENTS_IMPLEMENTATION.md
│ │ ├── COMMENT_ATTACHMENTS_OPTIMIZATION.md
│ │ ├── COMPLETE_ADVANCED_FEATURES_SUMMARY.md
│ │ ├── COMPLETE_IMPLEMENTATION_CHECKLIST.md
│ │ ├── COMPLETE_IMPLEMENTATION_FINAL.md
│ │ ├── COMPLETE_IMPLEMENTATION_REVIEW.md
│ │ ├── COMPLETE_IMPLEMENTATION_SUMMARY.md
│ │ ├── COMPREHENSIVE_IMPLEMENTATION_STATUS.md
│ │ ├── COMPREHENSIVE_IMPLEMENTATION_SUMMARY.md
│ │ ├── CONFIGURATION_FINAL_SUMMARY.md
│ │ ├── COVERAGE_FIX_SUMMARY.md
│ │ ├── DASHBOARD_NAVBAR_IMPROVEMENTS.md
│ │ ├── DEFAULT_DATA_SEEDING_FIX_CHANGELOG.md
│ │ ├── DELETION_AND_STATUS_IMPROVEMENTS.md
│ │ ├── DOCUMENTATION_RESTRUCTURE_SUMMARY.md
│ │ ├── ENHANCEMENT_PLAN_IMPLEMENTATION_STATUS.md
│ │ ├── ENHANCEMENT_PLAN_PROGRESS_SUMMARY.md
│ │ ├── FEATURE_IMPLEMENTATION_PROGRESS.md
│ │ ├── FINAL_IMPLEMENTATION_REPORT.md
│ │ ├── FINAL_IMPLEMENTATION_SUMMARY.md
│ │ ├── FINAL_SUMMARY.md
│ │ ├── FORCE_NO_CACHE_FIX.md
│ │ ├── HIGH_IMPACT_FEATURES.md
│ │ ├── HIGH_IMPACT_SUMMARY.md
│ │ ├── IMPLEMENTATION_COMPLETE.md
│ │ ├── IMPLEMENTATION_COMPLETE_SUMMARY.md
│ │ ├── IMPLEMENTATION_PROGRESS_2025.md
│ │ ├── IMPLEMENTATION_STATUS.md
│ │ ├── IMPLEMENTATION_SUMMARY.md
│ │ ├── IMPLEMENTATION_SUMMARY_CONTINUED.md
│ │ ├── IMPLEMENTATION_SUMMARY_DEFAULT_DATA_SEEDING.md
│ │ ├── INTEGRATION_REFACTORING_PLAN.md
│ │ ├── INVENTORY_PO_FORM_JSON.md
│ │ ├── KANBAN_IMPROVEMENTS.md
│ │ ├── KEYBOARD_SHORTCUTS_SUMMARY.md
│ │ ├── MANUAL_ENTRY_WORKED_TIME_FIX_559.md
│ │ ├── MIGRATION_018_FIX_SUMMARY.md
│ │ ├── MIGRATION_VALIDATION_FIX.md
│ │ ├── NOTIFICATION_SYSTEM_SUMMARY.md
│ │ ├── OIDC_IMPROVEMENTS.md
│ │ ├── OIDC_LOGOUT_FIX_SUMMARY.md
│ │ ├── PROGRESS_UPDATE.md
│ │ ├── PROJECT_ANALYSIS_AND_IMPROVEMENTS.md
│ │ ├── QUICK_FIX_SUMMARY.md
│ │ ├── QUICK_START_ARCHITECTURE.md
│ │ ├── QUICK_WINS_IMPLEMENTATION.md
│ │ ├── QUICK_WINS_SUMMARY.md
│ │ ├── README_IMPROVEMENTS.md
│ │ ├── README_NEW_ARCHITECTURE.md
│ │ ├── REPORTS_IMPROVEMENTS_SUMMARY.md
│ │ ├── ROUTE_REGISTRATION_AND_TEMPLATES_COMPLETE.md
│ │ ├── SESSION_CLOSE_ERROR_FIX.md
│ │ ├── SESSION_SUMMARY.md
│ │ ├── SMOKETEST_FIXES_SUMMARY.md
│ │ ├── STYLING_CONSISTENCY_SUMMARY.md
│ │ ├── TESTING_COMPLETE.md
│ │ ├── TOAST_NOTIFICATION_IMPROVEMENTS.md
│ │ ├── TRANSLATION_FIXES_SUMMARY.md
│ │ ├── TRANSLATION_IMPROVEMENTS_SUMMARY.md
│ │ ├── UI_IMPROVEMENTS_SUMMARY.md
│ │ ├── UX_QUICK_WINS_IMPLEMENTATION.md
│ │ └── VERSION_MANAGEMENT_SUMMARY.md
│ ├── import_export/
│ │ └── README.md
│ ├── integrations/
│ │ ├── ACTIVITYWATCH.md
│ │ ├── LINEAR.md
│ │ └── XERO.md
│ ├── mobile-desktop-apps/
│ │ ├── FINAL_REVIEW.md
│ │ ├── IMPLEMENTATION_COMPLETE.md
│ │ ├── IMPLEMENTATION_SUMMARY.md
│ │ ├── README.md
│ │ └── REVIEW.md
│ ├── pdf_template_alternatives_research.md
│ ├── privacy.md
│ ├── reports/
│ │ ├── ALL_BUGFIXES_SUMMARY.md
│ │ ├── README.md
│ │ ├── TRANSLATION_ANALYSIS_REPORT.md
│ │ ├── UNPAID_BY_SALESMAN_AND_SCHEDULED_REPORTS.md
│ │ └── i18n_audit_report.md
│ ├── telemetry-architecture.md
│ ├── testing/
│ │ ├── TESTING_STRATEGY.md
│ │ ├── TEST_PERFORMANCE_OPTIMIZATIONS.md
│ │ ├── TEST_REPORT.md
│ │ └── TEST_RESULTS_AVATAR_PERSISTENCE.md
│ └── user-guides/
│ └── DUPLICATING_TIME_ENTRIES.md
├── donate_hide_public.pem
├── env.example
├── env.local-test.example
├── examples/
│ └── zapier/
│ └── webhook_time_entry_created.json
├── grafana/
│ └── provisioning/
│ └── datasources/
│ └── prometheus.yml
├── loki/
│ └── loki-config.yml
├── migrations/
│ ├── MIGRATION_GUIDE.md
│ ├── README.md
│ ├── add_analytics_column.sql
│ ├── add_analytics_setting.py
│ ├── add_project_costs.sql
│ ├── alembic.ini
│ ├── ensure_uploads_persistence.py
│ ├── env.py
│ ├── fix_invoice_currency.py
│ ├── fix_invoice_pdf_template_items_source.py
│ ├── legacy_schema_migration.py
│ ├── manage_migrations.py
│ ├── migrate_existing_database.py
│ ├── migrate_to_client_model.py
│ ├── migration_019_kanban_columns.py
│ ├── script.py.mako
│ ├── test_migration_system.py
│ └── versions/
│ ├── 001_initial_schema.py
│ ├── 002_add_user_full_name.py
│ ├── 003_add_user_theme_preference.py
│ ├── 004_add_task_activities_table.py
│ ├── 005_add_missing_columns.py
│ ├── 006_add_logo_and_task_timestamps.py
│ ├── 007_add_invoice_and_more_settings_columns.py
│ ├── 008_align_invoices_and_settings_more.py
│ ├── 009_add_invoice_created_by.py
│ ├── 010_enforce_single_active_timer.py
│ ├── 011_add_user_preferred_language.py
│ ├── 012_add_pdf_template_fields.py
│ ├── 013_add_comments_table.py
│ ├── 014_add_payment_tracking.py
│ ├── 015_add_user_oidc_fields.py
│ ├── 016_add_focus_recurring_rates_filters_and_project_budget.py
│ ├── 017_reporting_invoicing_extensions.py
│ ├── 018_add_project_costs_table.py
│ ├── 019_add_kanban_columns_table.py
│ ├── 020_add_user_avatar.py
│ ├── 021_add_extra_goods_table.py
│ ├── 022_add_project_code_field.py
│ ├── 023_add_user_favorite_projects.py
│ ├── 024_add_client_notes_table.py
│ ├── 026_add_project_archiving_metadata.py
│ ├── 027_add_user_time_rounding_preferences.py
│ ├── 028_add_weekly_time_goals.py
│ ├── 029_add_expenses_table.py
│ ├── 030_add_permission_system.py
│ ├── 031_add_standard_hours_per_day.py
│ ├── 032_add_api_tokens.py
│ ├── 033_add_email_settings.py
│ ├── 034_add_calendar_events_table.py
│ ├── 035_enhance_payments_table.py
│ ├── 036_add_pdf_design_json.py
│ ├── 037_advanced_expenses.py
│ ├── 038_fix_advanced_expenses_schema.py
│ ├── 039_add_budget_alerts_table.py
│ ├── 040_add_import_export_tables.py
│ ├── 041_add_invoice_pdf_templates_table.py
│ ├── 042_client_prepaid_hours.py
│ ├── 043_add_project_id_to_kanban_columns.py
│ ├── 044_add_audit_logs_table.py
│ ├── 045_add_recurring_invoices_and_email_tracking.py
│ ├── 046_add_webhooks_system.py
│ ├── 047_add_client_portal_fields.py
│ ├── 048_add_client_portal_credentials.py
│ ├── 049_add_client_password_setup_token.py
│ ├── 050_add_offers_table.py
│ ├── 051_rename_offers_to_quotes_and_add_features.py
│ ├── 052_add_quote_discount_fields.py
│ ├── 053_add_quote_payment_terms.py
│ ├── 054_add_quote_comments.py
│ ├── 055_add_quote_attachments.py
│ ├── 056_add_quote_approval_workflow.py
│ ├── 057_add_quote_templates.py
│ ├── 058_add_quote_versions.py
│ ├── 059_add_inventory_management.py
│ ├── 060_add_supplier_management.py
│ ├── 061_add_purchase_orders.py
│ ├── 062_add_performance_indexes.py
│ ├── 063_add_crm_features.py
│ ├── 064_add_kiosk_mode_settings.py
│ ├── 065_add_new_features.py
│ ├── 066_add_integration_framework.py
│ ├── 067_add_integration_credentials.py
│ ├── 067b_alias_067_add_integration_credentials.py
│ ├── 068_add_user_password_hash.py
│ ├── 069_add_workflow_automation.py
│ ├── 070_add_time_entry_approvals.py
│ ├── 071_add_recurring_tasks.py
│ ├── 072_add_client_portal_customization_and_team_chat.py
│ ├── 073_add_ai_features_and_gps_tracking.py
│ ├── 074_add_password_change_required.py
│ ├── 075_add_client_custom_fields_and_link_templates.py
│ ├── 076_add_client_billing_to_time_entries.py
│ ├── 077_add_ui_feature_flags.py
│ ├── 078_add_system_ui_feature_flags.py
│ ├── 079_rename_user_badges_metadata_column.py
│ ├── 080_fix_metadata_column_names.py
│ ├── 081_add_all_integration_credentials.py
│ ├── 082_add_global_integrations.py
│ ├── 083_add_paid_status_to_time_entries.py
│ ├── 084_add_custom_field_definitions.py
│ ├── 085_add_project_custom_fields.py
│ ├── 086_add_project_and_client_attachments.py
│ ├── 087_add_salesman_email_mapping.py
│ ├── 088_add_salesman_splitting_to_reports.py
│ ├── 089_allow_auto_entries_without_project_or_client.py
│ ├── 089_fix_roles_permissions_sequences.py
│ ├── 090_add_push_subscriptions_table.py
│ ├── 090_enhance_report_builder_iteration.py
│ ├── 092_add_missing_module_visibility_flags.py
│ ├── 093_remove_ui_allow_module_flags.py
│ ├── 094_add_donation_interactions.py
│ ├── 095_add_missing_ui_show_issues.py
│ ├── 096_add_missing_portal_issues_enabled.py
│ ├── 097_add_stock_lots_for_devaluation.py
│ ├── 098_add_invoice_peppol_transmissions.py
│ ├── 099_add_peppol_settings_columns.py
│ ├── 100_add_comment_attachments.py
│ ├── 100_add_gantt_colors_and_modules_disabled.py
│ ├── 101_add_issues_table.py
│ ├── 102_add_missing_quotes_template_id.py
│ ├── 103_add_missing_quotes_quote_number.py
│ ├── 104_add_missing_quotes_columns.py
│ ├── 105_fix_client_notifications_cascade_delete.py
│ ├── 106_add_reportlab_template_json.py
│ ├── 107_increase_invoice_prefix_length.py
│ ├── 108_add_decorative_images.py
│ ├── 109_add_pdf_template_date_format.py
│ ├── 110_add_disabled_module_ids.py
│ ├── 111_add_use_last_month_dates.py
│ ├── 112_add_invoices_peppol_compliant.py
│ ├── 113_add_invoice_buyer_reference.py
│ ├── 114_enhance_audit_logs_for_timeentry.py
│ ├── 115_add_exclude_weekends_to_weekly_goals.py
│ ├── 116_merge_three_heads.py
│ ├── 117_add_user_calendar_type_colors.py
│ ├── 118_add_locked_client_id.py
│ ├── 118_add_role_hidden_module_ids.py
│ ├── 119_add_settings_date_time_format.py
│ ├── 120_user_nullable_date_time_format.py
│ ├── 121_add_ui_show_donate_and_system_instance_id.py
│ ├── 122_add_settings_donate_ui_hidden.py
│ ├── 123_add_calendar_default_view.py
│ ├── 124_add_time_entry_requirements.py
│ ├── 125_add_default_daily_working_hours.py
│ ├── 126_add_overtime_include_weekends_to_users.py
│ ├── 127_add_user_clients_table.py
│ ├── 128_add_invoices_zugferd_pdf.py
│ ├── 129_add_task_tags.py
│ ├── 129_merge_118_128_heads.py
│ ├── 130_add_peppol_transport_mode_and_native.py
│ ├── 131_add_donation_interaction_variant.py
│ ├── 132_add_timesheet_governance_and_time_off.py
│ ├── 133_merge_132_and_129_task_tags_heads.py
│ ├── 134_add_overtime_weekly_mode.py
│ ├── 135_add_remind_to_log_settings.py
│ ├── 136_seed_overtime_leave_type.py
│ ├── 137_add_break_time_to_time_entries.py
│ ├── 138_add_default_break_rules_settings.py
│ ├── 139_add_keyboard_shortcuts_overrides.py
│ ├── 140_add_client_portal_dashboard_preferences.py
│ ├── 141_add_invoice_number_pattern.py
│ ├── 142_add_mail_test_recipient.py
│ ├── 143_add_task_custom_fields.py
│ ├── 144_api_idempotency_keys.py
│ ├── 145_add_quotes_requires_approval_columns.py
│ ├── 146_add_quote_item_position.py
│ ├── 147_add_quote_item_line_kind.py
│ ├── 148_add_user_dismissed_release_version.py
│ ├── 149_add_user_support_stats_reports_generated.py
│ ├── 150_add_smart_notifications.py
│ ├── 151_add_ai_helper_settings.py
│ ├── 152_add_user_totp_2fa.py
│ ├── 153_add_user_auth_provider.py
│ ├── 20250127_000001_add_client_features.py
│ ├── 20251220_000001_add_integration_external_event_links.py
│ └── add_quick_wins_features.py
├── mobile/
│ ├── README.md
│ ├── android/
│ │ ├── app/
│ │ │ ├── build.gradle
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java/
│ │ │ │ └── io/
│ │ │ │ └── flutter/
│ │ │ │ └── plugins/
│ │ │ │ └── GeneratedPluginRegistrant.java
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── timetracker/
│ │ │ └── mobile/
│ │ │ └── MainActivity.kt
│ │ ├── build.gradle
│ │ ├── gradle/
│ │ │ └── wrapper/
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ │ ├── gradle.properties
│ │ ├── gradlew
│ │ ├── gradlew.bat
│ │ ├── local.properties
│ │ └── settings.gradle
│ ├── assets/
│ │ ├── .gitkeep
│ │ └── icon/
│ │ └── README.md
│ ├── flutter_launcher_icons_ios.yaml
│ ├── ios/
│ │ ├── Flutter/
│ │ │ ├── Generated.xcconfig
│ │ │ ├── ephemeral/
│ │ │ │ ├── flutter_lldb_helper.py
│ │ │ │ └── flutter_lldbinit
│ │ │ └── flutter_export_environment.sh
│ │ ├── Podfile
│ │ └── Runner/
│ │ ├── Assets.xcassets/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── GeneratedPluginRegistrant.h
│ │ ├── GeneratedPluginRegistrant.m
│ │ └── Info.plist
│ ├── lib/
│ │ ├── core/
│ │ │ ├── config/
│ │ │ │ └── app_config.dart
│ │ │ ├── constants/
│ │ │ │ └── app_constants.dart
│ │ │ ├── telemetry/
│ │ │ │ └── mobile_otel.dart
│ │ │ └── theme/
│ │ │ ├── app_theme.dart
│ │ │ └── app_tokens.dart
│ │ ├── data/
│ │ │ ├── api/
│ │ │ │ └── api_client.dart
│ │ │ ├── models/
│ │ │ │ ├── project.dart
│ │ │ │ ├── task.dart
│ │ │ │ ├── time_entry.dart
│ │ │ │ ├── time_entry_requirements.dart
│ │ │ │ ├── timer.dart
│ │ │ │ └── user_prefs.dart
│ │ │ └── storage/
│ │ │ ├── local_storage.dart
│ │ │ └── sync_service.dart
│ │ ├── domain/
│ │ │ ├── repositories/
│ │ │ │ └── time_tracking_repository.dart
│ │ │ └── usecases/
│ │ │ └── sync_usecase.dart
│ │ ├── main.dart
│ │ ├── presentation/
│ │ │ ├── providers/
│ │ │ │ ├── api_provider.dart
│ │ │ │ ├── finance_workforce_providers.dart
│ │ │ │ ├── projects_provider.dart
│ │ │ │ ├── tasks_provider.dart
│ │ │ │ ├── theme_mode_provider.dart
│ │ │ │ ├── time_entries_provider.dart
│ │ │ │ ├── time_entry_requirements_provider.dart
│ │ │ │ ├── timer_provider.dart
│ │ │ │ └── user_prefs_provider.dart
│ │ │ ├── screens/
│ │ │ │ ├── finance_workforce_screen.dart
│ │ │ │ ├── home_screen.dart
│ │ │ │ ├── login_screen.dart
│ │ │ │ ├── projects_screen.dart
│ │ │ │ ├── settings_screen.dart
│ │ │ │ ├── splash_screen.dart
│ │ │ │ ├── time_entries_screen.dart
│ │ │ │ ├── time_entry_form_screen.dart
│ │ │ │ └── timer_screen.dart
│ │ │ └── widgets/
│ │ │ ├── empty_state.dart
│ │ │ ├── error_view.dart
│ │ │ ├── start_timer_sheet.dart
│ │ │ ├── time_entry_card.dart
│ │ │ └── timer_widget.dart
│ │ └── utils/
│ │ ├── auth/
│ │ │ └── auth_service.dart
│ │ ├── date_format_utils.dart
│ │ ├── network/
│ │ │ ├── connection_diagnostics.dart
│ │ │ ├── connection_diagnostics_io.dart
│ │ │ └── connection_diagnostics_stub.dart
│ │ └── ssl/
│ │ ├── certificate_error.dart
│ │ ├── certificate_error_io.dart
│ │ ├── certificate_error_stub.dart
│ │ └── ssl_utils.dart
│ ├── pubspec.yaml
│ └── test/
│ ├── api_client_test.dart
│ ├── models_test.dart
│ └── widget_test.dart
├── nginx/
│ └── conf.d/
│ ├── example-public-domain.conf
│ └── https.conf
├── package.json
├── postcss.config.js
├── prometheus/
│ └── prometheus.yml
├── promtail/
│ └── promtail-config.yml
├── pyproject.toml
├── pytest.ini
├── render.yaml
├── requirements-dev.txt
├── requirements-test.txt
├── requirements.txt
├── scripts/
│ ├── README-BUILD.md
│ ├── apply_migration.py
│ ├── audit_i18n.py
│ ├── audit_migrations_portability.py
│ ├── build-all.bat
│ ├── build-all.sh
│ ├── build-desktop-no-sign.bat
│ ├── build-desktop-no-sign.sh
│ ├── build-desktop-simple.bat
│ ├── build-desktop-windows.bat
│ ├── build-desktop-windows.ps1
│ ├── build-desktop.bat
│ ├── build-desktop.sh
│ ├── build-mobile.bat
│ ├── build-mobile.sh
│ ├── check-desktop-assets.sh
│ ├── check_audit_logging.py
│ ├── check_audit_logs.py
│ ├── check_routes.py
│ ├── clear-all-electron-cache.bat
│ ├── clear-all-electron-cache.sh
│ ├── clear-electron-builder-cache.bat
│ ├── clear-electron-builder-cache.sh
│ ├── complete_all_translations.py
│ ├── complete_dutch_translations.py
│ ├── complete_nl_translations.py
│ ├── complete_spanish_translations.py
│ ├── complete_spanish_translations_final.py
│ ├── deploy-public.bat
│ ├── deploy-public.sh
│ ├── extract_translations.py
│ ├── fill_po_argos.py
│ ├── fix-desktop-build.bat
│ ├── fix-desktop-build.sh
│ ├── fix-onedrive-lock.ps1
│ ├── fix-windows-build.bat
│ ├── fix-windows-build.sh
│ ├── fix_missing_columns.py
│ ├── fix_quotes_template_id.py
│ ├── fix_translation_placeholders.py
│ ├── generate-certs.sh
│ ├── generate-changelog.py
│ ├── generate-icons.js
│ ├── generate-macos-icon.sh
│ ├── generate-mobile-icon.bat
│ ├── generate-mobile-icon.py
│ ├── generate-mobile-icon.sh
│ ├── generate_pwa_icons.py
│ ├── prepare-desktop-assets.sh
│ ├── quick_test_summary.py
│ ├── reset-dev-db.bat
│ ├── reset-dev-db.py
│ ├── reset-dev-db.sh
│ ├── run-tests.bat
│ ├── run-tests.sh
│ ├── run_model_tests.py
│ ├── run_tests.sh
│ ├── run_tests_individually.py
│ ├── run_tests_script.py
│ ├── sanitize_po_format_strings.py
│ ├── seed-dev-data.py
│ ├── setup-dev-analytics.bat
│ ├── setup-dev-analytics.sh
│ ├── setup-https-mkcert.bat
│ ├── setup-https-mkcert.sh
│ ├── setup-migrations.bat
│ ├── setup-migrations.sh
│ ├── start-https.bat
│ ├── start-https.sh
│ ├── start-local-test.bat
│ ├── start-local-test.ps1
│ ├── start-local-test.sh
│ ├── sync-desktop-version.py
│ ├── sync-mobile-version.py
│ ├── sync_translations.py
│ ├── test-build-desktop.bat
│ ├── test-docker-network.bat
│ ├── test-docker-network.sh
│ ├── test_audit_routes.py
│ ├── translate_all_spanish.py
│ ├── translate_dutch.py
│ ├── translate_spanish.py
│ ├── validate-setup.bat
│ ├── validate-setup.py
│ ├── validate-setup.sh
│ ├── verify-desktop-setup.sh
│ ├── verify_and_fix_schema.py
│ ├── verify_audit_setup.py
│ ├── verify_csrf_config.bat
│ ├── verify_csrf_config.sh
│ ├── version-manager.bat
│ ├── version-manager.ps1
│ ├── version-manager.py
│ └── version-manager.sh
├── setup.py
├── tailwind.config.js
├── tests/
│ ├── conftest.py
│ ├── factories.py
│ ├── models/
│ │ └── test_import_export_models.py
│ ├── smoke_test_email.py
│ ├── smoke_test_prepaid_hours.py
│ ├── smoke_test_project_dashboard.py
│ ├── smoke_test_user_settings.py
│ ├── test_activity_feed.py
│ ├── test_admin_dashboard_charts.py
│ ├── test_admin_email_routes.py
│ ├── test_admin_settings_logo.py
│ ├── test_admin_users.py
│ ├── test_analytics.py
│ ├── test_api_audit_activities_v1.py
│ ├── test_api_budget_alerts_v1.py
│ ├── test_api_calendar_v1.py
│ ├── test_api_client_notes_v1.py
│ ├── test_api_comments_v1.py
│ ├── test_api_comprehensive.py
│ ├── test_api_contract.py
│ ├── test_api_credit_notes_v1.py
│ ├── test_api_deprecation_headers.py
│ ├── test_api_expenses_v1.py
│ ├── test_api_favorites_v1.py
│ ├── test_api_invoice_templates_api_v1.py
│ ├── test_api_invoice_templates_v1.py
│ ├── test_api_invoices_v1.py
│ ├── test_api_kanban_v1.py
│ ├── test_api_mileage_v1.py
│ ├── test_api_payments_v1.py
│ ├── test_api_per_diem_v1.py
│ ├── test_api_project_costs_v1.py
│ ├── test_api_purchase_orders_v1.py
│ ├── test_api_recurring_invoices_v1.py
│ ├── test_api_route_contract.py
│ ├── test_api_saved_filters_v1.py
│ ├── test_api_tax_currency_v1.py
│ ├── test_api_time_entry_templates_v1.py
│ ├── test_api_v1.py
│ ├── test_api_v1_inventory_movements.py
│ ├── test_audit_log_model.py
│ ├── test_audit_log_routes.py
│ ├── test_audit_logging.py
│ ├── test_audit_trail_smoke.py
│ ├── test_basic.py
│ ├── test_budget_alert_model.py
│ ├── test_budget_alerts_smoke.py
│ ├── test_budget_forecasting.py
│ ├── test_bulk_task_operations.py
│ ├── test_calendar_event_model.py
│ ├── test_calendar_routes.py
│ ├── test_cii_invoice.py
│ ├── test_client_note_model.py
│ ├── test_client_notes_routes.py
│ ├── test_client_portal.py
│ ├── test_client_prepaid_model.py
│ ├── test_client_single_simplification.py
│ ├── test_comprehensive_tracking.py
│ ├── test_config_priority.py
│ ├── test_currency_display.py
│ ├── test_custom_field_definitions.py
│ ├── test_delete_actions.py
│ ├── test_demo_mode_and_safe_templates.py
│ ├── test_email.py
│ ├── test_enhanced_ui.py
│ ├── test_error_handling.py
│ ├── test_excel_export.py
│ ├── test_expenses.py
│ ├── test_extra_good_model.py
│ ├── test_factories_smoke.py
│ ├── test_favorite_projects.py
│ ├── test_i18n.py
│ ├── test_import_export.py
│ ├── test_installation_config.py
│ ├── test_integration/
│ │ ├── test_activitywatch_integration.py
│ │ ├── test_caldav_integration.py
│ │ ├── test_inventory_integration.py
│ │ └── test_jira_integration.py
│ ├── test_invoice_currency_fix.py
│ ├── test_invoice_currency_smoke.py
│ ├── test_invoice_email.py
│ ├── test_invoice_expenses.py
│ ├── test_invoice_numbering.py
│ ├── test_invoice_pdf_postprocess.py
│ ├── test_invoice_validators.py
│ ├── test_invoices.py
│ ├── test_keyboard_shortcuts.py
│ ├── test_keyboard_shortcuts_api.py
│ ├── test_keyboard_shortcuts_input_fix.py
│ ├── test_ldap_auth.py
│ ├── test_ldap_setup_wizard.py
│ ├── test_logo_pdf.py
│ ├── test_models/
│ │ ├── test_expense_category.py
│ │ ├── test_inventory_models.py
│ │ ├── test_mileage.py
│ │ ├── test_per_diem.py
│ │ ├── test_purchase_order.py
│ │ ├── test_supplier.py
│ │ └── test_webhook.py
│ ├── test_models_comprehensive.py
│ ├── test_models_extended.py
│ ├── test_multiselect_filters.py
│ ├── test_new_features.py
│ ├── test_oidc_logout.py
│ ├── test_oidc_session_cookie_bloat.py
│ ├── test_onboarding.py
│ ├── test_otel_integration.py
│ ├── test_overtime.py
│ ├── test_overtime_leave.py
│ ├── test_overtime_smoke.py
│ ├── test_payment_model.py
│ ├── test_payment_routes.py
│ ├── test_payment_smoke.py
│ ├── test_pdf_layout.py
│ ├── test_pdfa3.py
│ ├── test_peppol_identifiers.py
│ ├── test_peppol_service.py
│ ├── test_permissions.py
│ ├── test_permissions_routes.py
│ ├── test_prepaid_allocator.py
│ ├── test_profile_avatar.py
│ ├── test_project_archiving.py
│ ├── test_project_archiving_models.py
│ ├── test_project_costs.py
│ ├── test_project_dashboard.py
│ ├── test_project_inactive_status.py
│ ├── test_quick_wins.py
│ ├── test_reports_task_report.py
│ ├── test_repositories/
│ │ ├── __init__.py
│ │ ├── test_base_repository.py
│ │ └── test_time_entry_repository.py
│ ├── test_role_module_visibility.py
│ ├── test_routes/
│ │ ├── test_api_search.py
│ │ ├── test_api_smart_notifications.py
│ │ ├── test_api_v1_calendar_templates_refactored.py
│ │ ├── test_api_v1_expenses_complete.py
│ │ ├── test_api_v1_inventory_reports.py
│ │ ├── test_api_v1_inventory_transfers.py
│ │ ├── test_api_v1_invoices_tasks_expenses_refactored.py
│ │ ├── test_api_v1_mileage_refactored.py
│ │ ├── test_api_v1_payments_refactored.py
│ │ ├── test_api_v1_projects_refactored.py
│ │ ├── test_api_v1_quotes_refactored.py
│ │ ├── test_api_v1_recurring_invoices_credit_notes.py
│ │ ├── test_api_v1_reports_refactored.py
│ │ ├── test_api_v1_time_entries_complete.py
│ │ ├── test_api_v1_time_entries_refactored.py
│ │ ├── test_api_version_check.py
│ │ ├── test_auth.py
│ │ ├── test_inventory_routes.py
│ │ ├── test_main_dashboard_cached.py
│ │ ├── test_purchase_order_routes.py
│ │ ├── test_quotes_web.py
│ │ ├── test_reports_scope.py
│ │ ├── test_supplier_routes.py
│ │ └── test_timer_scope.py
│ ├── test_routes.py
│ ├── test_security.py
│ ├── test_service_worker.py
│ ├── test_services/
│ │ ├── __init__.py
│ │ ├── test_api_token_service.py
│ │ ├── test_comment_service.py
│ │ ├── test_export_service.py
│ │ ├── test_invoice_service.py
│ │ ├── test_notification_service.py
│ │ ├── test_payment_service.py
│ │ ├── test_payment_service_complete.py
│ │ ├── test_project_service.py
│ │ ├── test_recurring_invoice_service.py
│ │ ├── test_reporting_service.py
│ │ ├── test_stats_service.py
│ │ ├── test_task_service.py
│ │ ├── test_time_entry_bulk_service.py
│ │ ├── test_time_tracking_service.py
│ │ ├── test_time_tracking_service_complete.py
│ │ └── test_version_service.py
│ ├── test_silent_exception_fixes.py
│ ├── test_single_active_timer_setting.py
│ ├── test_support_services.py
│ ├── test_system_ui_flags.py
│ ├── test_task_edit_project.py
│ ├── test_tasks_filters_ui.py
│ ├── test_tasks_templates.py
│ ├── test_telemetry.py
│ ├── test_telemetry_consent_and_base.py
│ ├── test_time_entry_duplication.py
│ ├── test_time_entry_freeze.py
│ ├── test_time_entry_resume.py
│ ├── test_time_entry_templates.py
│ ├── test_time_rounding.py
│ ├── test_time_rounding_param.py
│ ├── test_timer_edit_own_time_entries.py
│ ├── test_timezone.py
│ ├── test_ui_quick_wins.py
│ ├── test_uploads_persistence.py
│ ├── test_user_report_entries_export_excel.py
│ ├── test_user_settings.py
│ ├── test_utils/
│ │ ├── test_api_auth_enhanced.py
│ │ ├── test_cache.py
│ │ ├── test_integration_sync_context.py
│ │ ├── test_scope_filter.py
│ │ ├── test_version_compare.py
│ │ └── test_webhook_service.py
│ ├── test_utils.py
│ ├── test_version_reading.py
│ ├── test_weekly_goals.py
│ └── test_zugferd.py
└── translations/
├── .keep
├── ar/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── de/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── en/
│ └── LC_MESSAGES/
│ └── messages.po
├── es/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── fi/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── fr/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── he/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── it/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── nb/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── nl/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── no/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
└── pt/
└── LC_MESSAGES/
└── messages.po
================================================
FILE CONTENTS
================================================
================================================
FILE: .bandit
================================================
[bandit]
exclude_dirs = tests,migrations,venv,.venv,htmlcov
skips = B101,B601
================================================
FILE: .coveragerc
================================================
[run]
source = app
omit =
*/tests/*
*/test_*.py
*/__pycache__/*
*/venv/*
*/env/*
# Exclude infrastructure/CLI utilities from unit test coverage
app/utils/backup.py
app/utils/cli.py
app/utils/pdf_generator.py
app/utils/pdf_generator_fallback.py
[report]
precision = 2
show_missing = True
skip_covered = False
[html]
directory = htmlcov
================================================
FILE: .editorconfig
================================================
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
[*.py]
indent_style = space
indent_size = 4
max_line_length = 120
[*.{json,yml,yaml}]
indent_style = space
indent_size = 2
================================================
FILE: .flake8
================================================
[flake8]
max-line-length = 120
max-complexity = 10
# C901=complexity, E501=line length, F401=unused import, E402=import not at top,
# F541=f-string no placeholder, E712=comparison to True/False, F841=unused variable, E741=ambiguous name,
# F811=redefinition (e.g. import inside function), W293=blank line whitespace, E203=whitespace before ':'
extend-ignore = C901, E501, F401, E402, F541, E712, F841, E741, F811, W293, E203
exclude = .git,__pycache__,migrations,.venv,venv,build,dist
================================================
FILE: .gitattributes
================================================
# Enforce LF endings for executable scripts to avoid /usr/bin/env CRLF issues
*.sh text eol=lf
*.py text eol=lf
# Optional: keep everything else automatic
* text=auto
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: drytrix
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment**
- **TimeTracker version:** (e.g. from `setup.py` or About page)
- **Deployment:** Docker / bare metal / other
- **OS:** [e.g. Ubuntu 22.04, Windows 11]
- **Browser:** [e.g. Chrome 120, Firefox 121] (if applicable)
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows, macOS, Linux]
- Browser [e.g. Chrome, Safari, Firefox]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone 14, Android]
- OS: [e.g. iOS 17, Android 14]
- Browser [e.g. Safari, Chrome]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here (logs, config, etc.).
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/ISSUE_TEMPLATE/translation_fix.yml
================================================
name: Translation improvement
description: Suggest a correction or better wording for interface text in a specific language (no Git required).
title: "[i18n] "
labels: []
body:
- type: markdown
attributes:
value: |
Thank you for helping improve translations. Before submitting, skim **Rules for translators** in `docs/CONTRIBUTING_TRANSLATIONS.md` in this repository (placeholders, context, what not to change).
- type: dropdown
id: language
attributes:
label: Language
description: Locale code for the translation you are improving.
options:
- nl (Nederlands)
- de (Deutsch)
- fr (Français)
- it (Italiano)
- fi (Suomi)
- es (Español)
- no (Norsk)
- ar (العربية)
- he (עברית)
- en (English)
validations:
required: true
- type: input
id: location
attributes:
label: Where did you see this?
description: Page name, menu path, or URL path (e.g. Dashboard, Settings, /login).
validations:
required: true
- type: textarea
id: current_text
attributes:
label: Current text (as shown in the app)
description: Copy the exact wording from the UI in the language you selected.
validations:
required: true
- type: textarea
id: suggested_text
attributes:
label: Suggested text
description: Your improved translation. Keep any %(name)s-style placeholders identical to the current text if present.
validations:
required: true
- type: textarea
id: notes
attributes:
label: Notes (optional)
description: Why this reads better, grammar fix, formal vs informal tone, screenshot description, etc.
- type: markdown
attributes:
value: |
**Optional:** Attach a screenshot in a follow-up comment if the form does not allow images.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description
Brief description of the change and why it's needed.
## Type of change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would change existing behavior)
- [ ] Documentation update
- [ ] Refactor (no functional change)
## Checklist
- [ ] My code follows the project's style guidelines (Black, flake8).
- [ ] I have added/updated tests for my changes.
- [ ] All tests pass locally (e.g. `pytest`).
- [ ] I have updated the documentation if needed.
- [ ] For user-facing changes, I have added an entry to the **Unreleased** section of [CHANGELOG.md](../CHANGELOG.md).
## Related issues
Fixes # (issue number, if applicable)
---
See [CONTRIBUTING.md](../CONTRIBUTING.md) and [CHANGELOG.md](../CHANGELOG.md) for guidelines.
================================================
FILE: .github/workflows/build-desktop.yml
================================================
name: Build Desktop Apps
env:
NODE_VERSION: '24'
on:
push:
branches: [ main, develop ]
paths:
- 'desktop/**'
- '.github/workflows/build-desktop.yml'
pull_request:
branches: [ main ]
paths:
- 'desktop/**'
workflow_dispatch:
jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Generate desktop icons
run: |
npm install
npm run generate:icons
- name: Install dependencies
working-directory: desktop
run: npm ci
- name: Build Windows
working-directory: desktop
env:
# Code signing (optional - only signs if secrets are configured)
CSC_LINK: ${{ secrets.WINDOWS_CODE_SIGN_CERT }}
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CODE_SIGN_PASSWORD }}
run: npm run build:win
- name: Upload Windows installer
uses: actions/upload-artifact@v4
with:
name: windows-installer
path: desktop/dist/*.exe
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Generate desktop icons
run: |
npm install
npm run generate:icons
- name: Install dependencies
working-directory: desktop
run: npm ci
- name: Build Linux
working-directory: desktop
run: npm run build:linux
- name: Upload Linux packages
uses: actions/upload-artifact@v4
with:
name: linux-packages
path: |
desktop/dist/*.AppImage
desktop/dist/*.deb
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Generate desktop icons
run: |
npm install
npm run generate:icons
chmod +x scripts/generate-macos-icon.sh
./scripts/generate-macos-icon.sh
- name: Install dependencies
working-directory: desktop
run: npm ci
- name: Build macOS
working-directory: desktop
run: npm run build:mac
- name: Upload macOS DMG
uses: actions/upload-artifact@v4
with:
name: macos-dmg
path: desktop/dist/*.dmg
================================================
FILE: .github/workflows/build-mobile.yml
================================================
name: Build Mobile Apps
on:
push:
branches: [ main, develop ]
paths:
- 'mobile/**'
- '.github/workflows/build-mobile.yml'
pull_request:
branches: [ main ]
paths:
- 'mobile/**'
workflow_dispatch:
jobs:
build-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.35.4'
channel: 'stable'
- name: Disable Flutter analytics
run: flutter config --no-analytics
- name: Install dependencies
working-directory: mobile
run: flutter pub get
- name: Generate app icons
run: |
pip install Pillow
python scripts/generate-mobile-icon.py
- name: Generate launcher icons
working-directory: mobile
run: dart run flutter_launcher_icons
- name: Run tests
working-directory: mobile
run: flutter test
- name: Build APK
working-directory: mobile
env:
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_TOKEN: ${{ secrets.OTEL_EXPORTER_OTLP_TOKEN }}
run: |
flutter build apk --release \
--dart-define=OTEL_EXPORTER_OTLP_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-}" \
--dart-define=OTEL_EXPORTER_OTLP_TOKEN="${OTEL_EXPORTER_OTLP_TOKEN:-}"
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: android-apk
path: mobile/build/app/outputs/flutter-apk/app-release.apk
build-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.35.4'
channel: 'stable'
- name: Disable Flutter analytics
run: flutter config --no-analytics
- name: Install dependencies
working-directory: mobile
run: flutter pub get
- name: Generate iOS platform files
working-directory: mobile
run: flutter create --platforms=ios .
- name: Generate app icons
run: |
pip install Pillow
python scripts/generate-mobile-icon.py
- name: Generate launcher icons
working-directory: mobile
run: |
dart run flutter_launcher_icons
dart run flutter_launcher_icons -f flutter_launcher_icons_ios.yaml
# Simulator build avoids Apple Development Team / provisioning (device ipa still needs signing locally).
# Release mode is not supported for the iOS simulator; use debug (default) or --profile.
- name: Run tests
working-directory: mobile
run: flutter test
- name: Build iOS (simulator)
working-directory: mobile
env:
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_TOKEN: ${{ secrets.OTEL_EXPORTER_OTLP_TOKEN }}
run: |
flutter build ios --simulator \
--dart-define=OTEL_EXPORTER_OTLP_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-}" \
--dart-define=OTEL_EXPORTER_OTLP_TOKEN="${OTEL_EXPORTER_OTLP_TOKEN:-}"
- name: Create iOS archive
if: success()
working-directory: mobile
run: |
mkdir -p dist
if [ -d "build/ios/iphonesimulator/Runner.app" ]; then
cd build/ios/iphonesimulator
zip -r ../../../dist/TimeTracker-iOS.zip Runner.app
cd ../../..
else
echo "Warning: Runner.app not found at build/ios/iphonesimulator/Runner.app"
ls -la build/ios/ || true
ls -la build/ || true
fi
- name: Upload iOS build
if: success()
uses: actions/upload-artifact@v4
with:
name: ios-build
path: mobile/dist/*.zip
if-no-files-found: warn
================================================
FILE: .github/workflows/cd-development.yml
================================================
name: CD - Development Build
on:
pull_request:
branches: [ 'rc', 'rc/**' ]
# Only trigger builds when actual code changes
# This uses explicit paths to skip documentation, markdown, and other non-code changes
paths:
- 'app/**'
- 'migrations/**'
- 'requirements*.txt'
- 'setup.py'
- 'Dockerfile'
- 'docker-compose*.yml'
- 'package*.json'
- 'tailwind.config.js'
- 'postcss.config.js'
- '.github/workflows/cd-development.yml'
- 'babel.cfg'
- 'pytest.ini'
- 'Makefile'
workflow_dispatch:
inputs:
force_build:
description: 'Force build even if tests fail'
required: false
type: boolean
default: false
create_release:
description: 'Create GitHub release (default: false for regular builds)'
required: false
type: boolean
default: false
# Concurrency control: cancel in-progress builds when new commits are pushed
# This prevents wasting resources on outdated builds
concurrency:
group: dev-build-${{ github.ref }}
cancel-in-progress: true
# Required permissions for creating releases and pushing images
permissions: write-all
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
PYTHON_VERSION: '3.11'
jobs:
# ============================================================================
# Quick Test Suite for Development
# ============================================================================
quick-tests:
name: Quick Test Suite
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test_password
POSTGRES_USER: test_user
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
pip install -e .
- name: Run smoke tests
env:
PYTHONPATH: ${{ github.workspace }}
run: |
pytest -m smoke -v --tb=short --no-cov -n auto
- name: Validate database migrations
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
echo "🔍 Validating database migrations..."
# Check if there are migration-related changes
if git diff --name-only HEAD~1 2>/dev/null | grep -E "(app/models/|migrations/)" > /dev/null; then
echo "📋 Migration-related changes detected"
# Initialize fresh database
flask db upgrade
# Test migration rollback
CURRENT_MIGRATION=$(flask db current)
echo "Current migration: $CURRENT_MIGRATION"
if [ -n "$CURRENT_MIGRATION" ] && [ "$CURRENT_MIGRATION" != "None" ]; then
echo "Testing migration operations..."
flask db upgrade head
echo "✅ Migration validation passed"
fi
else
echo "ℹ️ No migration-related changes detected"
fi
# ============================================================================
# Build and Push Development Image
# ============================================================================
build-and-push:
name: Build and Push Development Image
runs-on: ubuntu-latest
needs: quick-tests
if: always() && (needs.quick-tests.result == 'success' || github.event.inputs.force_build == 'true')
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=develop
type=raw,value=dev-{{date 'YYYYMMDD-HHmmss'}}
type=sha,prefix=dev-,format=short
labels: |
org.opencontainers.image.description=Self-hosted time tracking web application for projects, clients, and reports.
- name: Determine version
id: version
run: |
BUILD_NUMBER=${{ github.run_number }}
COMMIT_SHA=${GITHUB_SHA::8}
VERSION="dev-${BUILD_NUMBER}-${COMMIT_SHA}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "📦 Building version: $VERSION"
- name: Inject donate-hide public key (optional)
env:
DONATE_HIDE_PUBLIC_KEY_PEM: ${{ secrets.DONATE_HIDE_PUBLIC_KEY_PEM }}
run: |
if [ -n "$DONATE_HIDE_PUBLIC_KEY_PEM" ]; then
echo "✅ DONATE_HIDE_PUBLIC_KEY_PEM set — writing donate_hide_public.pem for Docker build"
echo "$DONATE_HIDE_PUBLIC_KEY_PEM" > donate_hide_public.pem
else
echo "⚠️ DONATE_HIDE_PUBLIC_KEY_PEM not set — Support visibility verification disabled in image"
fi
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_VERSION=${{ steps.version.outputs.version }}
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
cache-to: type=inline
- name: Generate deployment manifest
run: |
cat > deployment-dev.yml << EOF
# TimeTracker Development Deployment
# Generated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')
# Version: ${{ steps.version.outputs.version }}
# Commit: ${{ github.sha }}
version: '3.8'
services:
app:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
container_name: timetracker-dev
ports:
- "8080:8080"
environment:
- TZ=Europe/Brussels
- DATABASE_URL=postgresql://timetracker:timetracker@db:5432/timetracker
- SECRET_KEY=\${SECRET_KEY}
- FLASK_ENV=development
- APP_VERSION=${{ steps.version.outputs.version }}
depends_on:
- db
restart: unless-stopped
db:
image: postgres:16-alpine
container_name: timetracker-dev-db
environment:
- POSTGRES_DB=timetracker
- POSTGRES_USER=timetracker
- POSTGRES_PASSWORD=\${POSTGRES_PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
db_data:
EOF
echo "📄 Deployment manifest created"
cat deployment-dev.yml
- name: Upload deployment manifest
uses: actions/upload-artifact@v4
with:
name: deployment-manifest-dev
path: deployment-dev.yml
- name: Create GitHub Release (Development)
# Only create releases when explicitly requested via workflow_dispatch
# This prevents cluttering the releases page with every dev build
if: github.event_name == 'workflow_dispatch' && github.event.inputs.create_release == 'true'
continue-on-error: true
uses: actions/github-script@v7
with:
script: |
const version = '${{ steps.version.outputs.version }}';
const tagName = `dev-${version}`;
try {
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `Development Build ${version}`,
body: `## Development Build
**Version:** ${version}
**Commit:** ${context.sha.substring(0, 7)}
**PR:** #${{ github.event.pull_request.number }}
**Target Branch:** ${{ github.base_ref }}
**Build:** #${context.runNumber}
### Docker Image
\`\`\`
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
\`\`\`
### Quick Start
\`\`\`bash
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop
docker-compose -f deployment-dev.yml up -d
\`\`\`
### Changes
${context.payload.head_commit?.message || 'See commit history'}
---
*This is a manually triggered development build.*
`,
draft: false,
prerelease: true
});
console.log('✅ Development release created');
} catch (error) {
if (error.status === 422) {
console.log('⚠️ Release already exists, skipping');
} else if (error.status === 403) {
console.log('⚠️ GitHub Actions does not have permission to create releases');
console.log('📝 To fix: Go to Settings → Actions → General → Workflow permissions');
console.log('📝 Select "Read and write permissions" and save');
} else {
throw error;
}
}
# ============================================================================
# Notification
# ============================================================================
notify:
name: Send Notifications
runs-on: ubuntu-latest
needs: [quick-tests, build-and-push]
if: always()
steps:
- name: Determine build status
id: status
run: |
if [ "${{ needs.build-and-push.result }}" == "success" ]; then
echo "status=✅ Success" >> $GITHUB_OUTPUT
echo "color=28a745" >> $GITHUB_OUTPUT
else
echo "status=❌ Failed" >> $GITHUB_OUTPUT
echo "color=dc3545" >> $GITHUB_OUTPUT
fi
- name: Create summary
run: |
echo "## 🚀 Development Build ${{ steps.status.outputs.status }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Target Branch:** ${{ github.base_ref }}" >> $GITHUB_STEP_SUMMARY
echo "**Source Branch:** ${{ github.head_ref }}" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "**Build:** #${{ github.run_number }}" >> $GITHUB_STEP_SUMMARY
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Test Results" >> $GITHUB_STEP_SUMMARY
echo "- Tests: ${{ needs.quick-tests.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Build: ${{ needs.build-and-push.result }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.build-and-push.result }}" == "success" ]; then
echo "### 🐳 Docker Image" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:develop" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Note about release creation
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ github.event.inputs.create_release }}" == "true" ]; then
echo "📦 GitHub release created" >> $GITHUB_STEP_SUMMARY
else
echo "ℹ️ No GitHub release created (use workflow_dispatch with create_release=true to create one)" >> $GITHUB_STEP_SUMMARY
fi
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "---" >> $GITHUB_STEP_SUMMARY
echo "💡 **Tip:** This workflow runs on PRs to RC branches. Documentation and test-only changes are skipped." >> $GITHUB_STEP_SUMMARY
================================================
FILE: .github/workflows/cd-release.yml
================================================
name: CD - Release Build
# This workflow builds and publishes official releases
#
# Testing Strategy:
# - Full test suite runs on PRs via ci-comprehensive.yml
# - This workflow focuses on building and publishing
# - Security audit still runs to catch any last-minute issues
# - Tests can optionally be run via workflow_dispatch for manual releases
#
# Workflow is triggered by:
# - Push to main/master (after PR merge from RC)
# - Git tags (v*.*.*)
# - Release events
# - Manual workflow_dispatch
on:
push:
branches: [ main, master ]
tags: [ 'v*.*.*' ]
release:
types: [ published ]
workflow_dispatch:
inputs:
version:
description: 'Release version (e.g., v1.2.3) - must match version in setup.py'
required: true
type: string
skip_tests:
description: 'Skip tests (opt-in only; default is to run tests on manual release)'
required: false
type: boolean
default: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# Docker Hub repo to also publish release images to.
# Requires GitHub Actions secrets:
# - DOCKERHUB_USERNAME
# - DOCKERHUB_TOKEN (recommended) or DOCKERHUB_PASSWORD
DOCKERHUB_IMAGE: driesp/timetracker
PYTHON_VERSION: '3.11'
NODE_VERSION: '24'
jobs:
# ============================================================================
# Full Test Suite (Optional - tests already ran on PR)
# ============================================================================
full-test-suite:
name: Full Test Suite (Optional)
runs-on: ubuntu-latest
# Skip by default since tests already ran on PR
# Only run if explicitly requested via workflow_dispatch
if: github.event_name == 'workflow_dispatch' && github.event.inputs.skip_tests != 'true'
timeout-minutes: 30
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test_password
POSTGRES_USER: test_user
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
pip install -e .
- name: Validate database migrations
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
echo "🔍 Validating database migrations..."
# Check if there are migration-related changes
if git diff --name-only HEAD~1 2>/dev/null | grep -E "(app/models/|migrations/)" > /dev/null; then
echo "📋 Migration-related changes detected"
# Initialize fresh database
flask db upgrade
# Test migration rollback
CURRENT_MIGRATION=$(flask db current)
echo "Current migration: $CURRENT_MIGRATION"
if [ -n "$CURRENT_MIGRATION" ] && [ "$CURRENT_MIGRATION" != "None" ]; then
echo "Testing migration operations..."
flask db upgrade head
echo "✅ Migration validation passed"
fi
# Test with sample data
python -c "
from app import create_app, db
from app.models.user import User
from app.models.project import Project
from app.models.client import Client
app = create_app()
with app.app_context():
user = User(username='test_user', role='user')
db.session.add(user)
db.session.commit()
client = Client(name='Test Client', description='Test client')
db.session.add(client)
db.session.commit()
project = Project(name='Test Project', client_id=client.id, description='Test project')
db.session.add(project)
db.session.commit()
print('✅ Sample data created and validated successfully')
"
else
echo "ℹ️ No migration-related changes detected"
fi
- name: Initialize database schema for tests
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
echo "🔧 Applying migrations to ensure test schema exists..."
flask db upgrade
echo "Current migration: $(flask db current)"
- name: Run complete test suite
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
PYTHONPATH: ${{ github.workspace }}
run: |
pytest -v -n auto --cov=app --cov-report=xml --cov-report=html --cov-report=term-missing \
--junitxml=junit.xml --maxfail=5
- name: Upload coverage reports
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
flags: release
name: release-tests
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-release
path: |
htmlcov/
coverage.xml
junit.xml
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: junit.xml
check_name: Release Test Results
# ============================================================================
# Security Audit (always runs for releases)
# ============================================================================
security-audit:
name: Security Audit
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install security tools
run: |
pip install safety
- name: Run Safety
run: |
safety check --file requirements.txt --json > safety-report.json || true
safety check --file requirements.txt || true
- name: Upload security reports
if: always()
uses: actions/upload-artifact@v4
with:
name: security-audit-reports
path: |
safety-report.json
# ============================================================================
# Determine Version
# ============================================================================
determine-version:
name: Determine Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
is_prerelease: ${{ steps.version.outputs.is_prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Extract version from setup.py
id: extract_version
run: |
# Extract version from setup.py
VERSION=$(grep -oP "version='\K[^']+" setup.py)
if [ -z "$VERSION" ]; then
echo "❌ ERROR: Could not extract version from setup.py"
exit 1
fi
echo "extracted_version=$VERSION" >> $GITHUB_OUTPUT
echo "📝 Extracted version from setup.py: $VERSION"
- name: Determine version and validate
id: version
run: |
SETUP_VERSION="${{ steps.extract_version.outputs.extracted_version }}"
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
# For manual triggers, use the input version
VERSION="${{ github.event.inputs.version }}"
IS_PRERELEASE="false"
elif [[ "${{ github.event_name }}" == "release" ]]; then
# For release events, use the release tag
VERSION="${{ github.event.release.tag_name }}"
IS_PRERELEASE="${{ github.event.release.prerelease }}"
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
# For tag pushes, use the tag name
VERSION=${GITHUB_REF#refs/tags/}
IS_PRERELEASE="false"
else
# For push to main/master, use setup.py version
VERSION="v${SETUP_VERSION}"
IS_PRERELEASE="false"
fi
# Ensure version starts with 'v'
if [[ ! $VERSION =~ ^v ]]; then
VERSION="v${VERSION}"
fi
# Extract version without 'v' prefix for comparison
VERSION_NO_V="${VERSION#v}"
# Validate that the version matches setup.py
if [[ "$VERSION_NO_V" != "$SETUP_VERSION" ]]; then
echo "❌ ERROR: Version mismatch!"
echo " Tag version: $VERSION_NO_V"
echo " setup.py version: $SETUP_VERSION"
echo ""
echo "Please update setup.py to version '$VERSION_NO_V' or use version 'v$SETUP_VERSION'"
exit 1
fi
# Check if tag already exists (prevent duplicates)
if git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "❌ ERROR: Tag '$VERSION' already exists!"
echo " This version has already been released."
echo " Please update the version in setup.py to a new version number."
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT
echo "✅ Version validation passed"
echo "📦 Version: $VERSION"
echo "🏷️ Prerelease: $IS_PRERELEASE"
# ============================================================================
# Build and Push Release Image
# ============================================================================
build-and-push:
name: Build and Push Release Image
runs-on: ubuntu-latest
needs: [security-audit, determine-version]
# Note: full-test-suite is optional, so we don't depend on it
# Tests already ran on PR before merge
permissions:
contents: read
packages: write
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN || secrets.DOCKERHUB_PASSWORD }}
continue-on-error: true
- name: Set lowercase image name
id: image-name
run: |
# Convert repository name to lowercase for Docker image compatibility
IMAGE_NAME_LOWER=$(echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
echo "IMAGE_NAME_LOWER=${IMAGE_NAME_LOWER}" >> $GITHUB_OUTPUT
echo "✅ Image name set to: ${IMAGE_NAME_LOWER}"
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
# Use lowercase image name for compatibility with Docker registries
images: |
${{ steps.image-name.outputs.IMAGE_NAME_LOWER }}
${{ env.DOCKERHUB_IMAGE }}
tags: |
type=semver,pattern={{version}},value=${{ needs.determine-version.outputs.version }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.determine-version.outputs.version }}
type=semver,pattern={{major}},value=${{ needs.determine-version.outputs.version }}
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=stable,enable=${{ needs.determine-version.outputs.is_prerelease == 'false' }}
labels: |
org.opencontainers.image.description=Self-hosted time tracking web application for projects, clients, and reports.
- name: Inject analytics configuration from GitHub Secrets
env:
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_TOKEN: ${{ secrets.OTEL_EXPORTER_OTLP_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
run: |
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🔐 INJECTING ANALYTICS CREDENTIALS FROM GITHUB SECRET STORE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📍 Location: Settings → Secrets and variables → Actions"
echo "📝 Target File: app/config/analytics_defaults.py"
echo ""
# Show file before injection
echo "📄 File content BEFORE injection (showing placeholders):"
echo "──────────────────────────────────────────────────────────────────"
grep -E "(OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT|OTEL_EXPORTER_OTLP_TOKEN_DEFAULT|SENTRY_DSN_DEFAULT)" app/config/analytics_defaults.py || true
echo "──────────────────────────────────────────────────────────────────"
echo ""
# Verify secrets are available
echo "🔍 Verifying GitHub Secrets availability..."
if [ -z "$OTEL_EXPORTER_OTLP_ENDPOINT" ] || [ -z "$OTEL_EXPORTER_OTLP_TOKEN" ]; then
echo "⚠️ Grafana OTLP secrets not fully set (telemetry sink disabled in this build)"
echo " → To enable: Add OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_TOKEN in Settings → Secrets"
else
echo "✅ Grafana OTLP secrets found in GitHub Secret Store"
fi
if [ -z "$SENTRY_DSN" ]; then
echo "⚠️ SENTRY_DSN secret not set (optional)"
echo " → Sentry error tracking will be disabled"
else
echo "✅ SENTRY_DSN secret found in GitHub Secret Store"
echo " → Format: ${SENTRY_DSN:0:25}***${SENTRY_DSN: -10} (${#SENTRY_DSN} characters)"
fi
echo ""
# Perform replacement (use empty string if secrets not set)
echo "🔧 Injecting secrets into application configuration..."
sed -i "s|%%OTEL_EXPORTER_OTLP_ENDPOINT_PLACEHOLDER%%|${OTEL_EXPORTER_OTLP_ENDPOINT:-}|g" app/config/analytics_defaults.py
sed -i "s|%%OTEL_EXPORTER_OTLP_TOKEN_PLACEHOLDER%%|${OTEL_EXPORTER_OTLP_TOKEN:-}|g" app/config/analytics_defaults.py
sed -i "s|%%SENTRY_DSN_PLACEHOLDER%%|${SENTRY_DSN:-}|g" app/config/analytics_defaults.py
echo " → Placeholders replaced with secret values (or empty if not set)"
echo ""
# Show file after injection (redacted)
echo "📄 File content AFTER injection (secrets redacted):"
echo "──────────────────────────────────────────────────────────────────"
grep -E "(OTEL_EXPORTER_OTLP_ENDPOINT_DEFAULT|OTEL_EXPORTER_OTLP_TOKEN_DEFAULT|SENTRY_DSN_DEFAULT)" app/config/analytics_defaults.py | \
sed 's/\(phc_[a-zA-Z0-9]\{8\}\)[a-zA-Z0-9]*\([a-zA-Z0-9]\{4\}\)/\1***\2/g' | \
sed 's|\(https://[^@]*@[^/]*\)|***REDACTED***|g' || true
echo "──────────────────────────────────────────────────────────────────"
echo ""
# Verify placeholders were replaced
echo "🔍 Verifying injection was successful..."
if grep -q "%%OTEL_EXPORTER_OTLP_ENDPOINT_PLACEHOLDER%%" app/config/analytics_defaults.py; then
echo "❌ ERROR: Grafana endpoint placeholder was NOT replaced!"
exit 1
else
echo "✅ Grafana endpoint placeholder successfully replaced"
fi
if grep -q "%%OTEL_EXPORTER_OTLP_TOKEN_PLACEHOLDER%%" app/config/analytics_defaults.py; then
echo "❌ ERROR: Grafana token placeholder was NOT replaced!"
exit 1
else
echo "✅ Grafana token placeholder successfully replaced"
fi
if grep -q "%%SENTRY_DSN_PLACEHOLDER%%" app/config/analytics_defaults.py; then
echo "❌ ERROR: Sentry DSN placeholder was NOT replaced!"
echo " The placeholder '%%SENTRY_DSN_PLACEHOLDER%%' is still present in the file."
exit 1
else
echo "✅ Sentry DSN placeholder successfully replaced"
fi
echo "✅ Grafana OTLP values injected"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ SUCCESS: Analytics credentials injected from GitHub Secret Store"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📊 Injected Credentials Summary:"
if [ -n "$OTEL_EXPORTER_OTLP_ENDPOINT" ] && [ -n "$OTEL_EXPORTER_OTLP_TOKEN" ]; then
echo " • Grafana OTLP: configured ✓"
else
echo " • Grafana OTLP: [Not configured - telemetry sink disabled] ⚠️"
fi
if [ -n "$SENTRY_DSN" ]; then
echo " • Sentry DSN: ${SENTRY_DSN:0:20}*** ✓"
else
echo " • Sentry DSN: [Not configured] ⚠️"
fi
echo ""
echo "🔒 Security Notes:"
echo " • Secrets are injected at build time from GitHub Secret Store"
echo " • Secrets are never exposed in logs or build artifacts"
echo " • Users can still opt-in/opt-out of telemetry via admin dashboard"
echo ""
- name: Inject donate-hide public key (optional)
env:
DONATE_HIDE_PUBLIC_KEY_PEM: ${{ secrets.DONATE_HIDE_PUBLIC_KEY_PEM }}
run: |
if [ -n "$DONATE_HIDE_PUBLIC_KEY_PEM" ]; then
echo "✅ DONATE_HIDE_PUBLIC_KEY_PEM secret set — writing donate_hide_public.pem for Docker build"
echo "$DONATE_HIDE_PUBLIC_KEY_PEM" > donate_hide_public.pem
echo " → File will be copied into image at /app/donate_hide_public.pem (DONATE_HIDE_PUBLIC_KEY_FILE)"
else
echo "⚠️ DONATE_HIDE_PUBLIC_KEY_PEM not set — Support visibility code verification will be disabled in image"
fi
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_VERSION=${{ needs.determine-version.outputs.version }}
cache-from: type=registry,ref=${{ steps.image-name.outputs.IMAGE_NAME_LOWER }}:latest
cache-to: type=inline
- name: Generate deployment manifests
run: |
VERSION="${{ needs.determine-version.outputs.version }}"
# Remove 'v' prefix for image tag
VERSION_NO_V="${VERSION#v}"
# Convert repository name to lowercase for image name
IMAGE_NAME_LOWER=$(echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
PRODUCTION_IMAGE="${IMAGE_NAME_LOWER}:${VERSION_NO_V}"
# Docker Compose deployment - includes all services from docker-compose.yml
cat > docker-compose.production.yml << EOF
# TimeTracker Production Deployment
# Generated: $(date -u +'%Y-%m-%d %H:%M:%S UTC')
# Version: ${VERSION}
# Image: ${PRODUCTION_IMAGE}
services:
# Certificate generator - runs once to create self-signed certs with SANs
certgen:
image: alpine:latest
container_name: timetracker-certgen
volumes:
- ./nginx/ssl:/certs
- ./scripts:/scripts:ro
command: sh /scripts/generate-certs.sh
restart: "no"
# HTTPS reverse proxy (TLS terminates here)
nginx:
image: nginx:alpine
container_name: timetracker-nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
certgen:
condition: service_completed_successfully
app:
condition: service_started
restart: unless-stopped
app:
image: ${PRODUCTION_IMAGE}
container_name: timetracker-app
environment:
- TZ=\${TZ:-Europe/Brussels}
- CURRENCY=\${CURRENCY:-EUR}
- ROUNDING_MINUTES=\${ROUNDING_MINUTES:-1}
- SINGLE_ACTIVE_TIMER=\${SINGLE_ACTIVE_TIMER:-true}
- ALLOW_SELF_REGISTER=\${ALLOW_SELF_REGISTER:-true}
- IDLE_TIMEOUT_MINUTES=\${IDLE_TIMEOUT_MINUTES:-30}
- ADMIN_USERNAMES=\${ADMIN_USERNAMES:-admin}
# IMPORTANT: Change SECRET_KEY in production! Used for sessions and CSRF tokens.
# Generate a secure key: python -c "import secrets; print(secrets.token_hex(32))"
#
# CSRF CONFIGURATION:
# - WTF_CSRF_SSL_STRICT: Set to 'false' for HTTP access (localhost or IP address)
# Set to 'true' only when using HTTPS in production
# - If accessing via IP address (e.g., 192.168.1.100), also set:
# SESSION_COOKIE_SECURE=false and CSRF_COOKIE_SECURE=false
#
# TROUBLESHOOTING: If forms fail with "CSRF token missing or invalid":
# 1. Verify SECRET_KEY is set and doesn't change between restarts
# 2. Check CSRF is enabled: WTF_CSRF_ENABLED=true
# 3. Ensure cookies are enabled in your browser
# 4. If behind a reverse proxy, ensure it forwards cookies correctly
# 5. Check the token hasn't expired (increase WTF_CSRF_TIME_LIMIT if needed)
# 6. If accessing via IP (not localhost): WTF_CSRF_SSL_STRICT=false
# For details: docs/CSRF_CONFIGURATION.md and docs/CSRF_IP_ACCESS_GUIDE.md
- SECRET_KEY=\${SECRET_KEY:-your-secret-key-change-this}
# Disable strict Referer check by default to avoid privacy/port issues
- WTF_CSRF_SSL_STRICT=\${WTF_CSRF_SSL_STRICT:-true}
- WTF_CSRF_ENABLED=\${WTF_CSRF_ENABLED:-true}
- WTF_CSRF_TIME_LIMIT=\${WTF_CSRF_TIME_LIMIT:-3600}
- SESSION_COOKIE_SECURE=\${SESSION_COOKIE_SECURE:-true}
- SESSION_COOKIE_SAMESITE=\${SESSION_COOKIE_SAMESITE:-Lax}
- REMEMBER_COOKIE_SECURE=\${REMEMBER_COOKIE_SECURE:-true}
- CSRF_COOKIE_SECURE=\${CSRF_COOKIE_SECURE:-true}
- CSRF_COOKIE_HTTPONLY=\${CSRF_COOKIE_HTTPONLY:-false}
- CSRF_COOKIE_SAMESITE=\${CSRF_COOKIE_SAMESITE:-Lax}
- CSRF_COOKIE_NAME=\${CSRF_COOKIE_NAME:-XSRF-TOKEN}
- PREFERRED_URL_SCHEME=\${PREFERRED_URL_SCHEME:-https}
- WTF_CSRF_TRUSTED_ORIGINS=\${WTF_CSRF_TRUSTED_ORIGINS:-https://localhost}
- DATABASE_URL=postgresql+psycopg2://timetracker:timetracker@db:5432/timetracker
- REDIS_URL=redis://:\${REDIS_PASSWORD:-timetracker}@redis:6379/0
- REDIS_ENABLED=\${REDIS_ENABLED:-true}
- LOG_FILE=/app/logs/timetracker.log
# Analytics & Monitoring (optional)
# See docs/analytics.md for configuration details
- SENTRY_DSN=\${SENTRY_DSN:-}
- SENTRY_TRACES_RATE=\${SENTRY_TRACES_RATE:-0.0}
- OTEL_EXPORTER_OTLP_ENDPOINT=\${OTEL_EXPORTER_OTLP_ENDPOINT:-}
- OTEL_EXPORTER_OTLP_TOKEN=\${OTEL_EXPORTER_OTLP_TOKEN:-}
- ENABLE_TELEMETRY=\${ENABLE_TELEMETRY:-false}
- TELE_SALT=\${TELE_SALT:-8f4a7b2e9c1d6f3a5e8b4c7d2a9f6e3b1c8d5a7f2e9b4c6d3a8f5e1b7c4d9a2f}
# Expose only internally; nginx publishes ports
ports: []
volumes:
- app_data:/data
- app_logs:/app/logs
- app_uploads:/app/app/static/uploads
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/_health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
image: postgres:16-alpine
container_name: timetracker-db
environment:
- POSTGRES_DB=\${POSTGRES_DB:-timetracker}
- POSTGRES_USER=\${POSTGRES_USER:-timetracker}
- POSTGRES_PASSWORD=\${POSTGRES_PASSWORD:-timetracker}
- TZ=\${TZ:-Europe/Brussels}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U \$\$POSTGRES_USER -d \$\$POSTGRES_DB"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
# Redis - Caching and session storage
redis:
image: redis:7-alpine
container_name: timetracker-redis
command: redis-server --appendonly yes --requirepass \${REDIS_PASSWORD:-timetracker}
volumes:
- redis_data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
restart: unless-stopped
# Analytics & Monitoring Services
# All services start by default for complete monitoring
# See docs/analytics.md and ANALYTICS_QUICK_START.md for details
# Prometheus - Metrics collection and storage
prometheus:
image: prom/prometheus:latest
container_name: timetracker-prometheus
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--storage.tsdb.retention.time=30d'
ports:
- "9090:9090"
restart: unless-stopped
# Grafana - Metrics visualization and dashboards
grafana:
image: grafana/grafana:latest
container_name: timetracker-grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=\${GRAFANA_ADMIN_PASSWORD:-admin}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=\${GF_SERVER_ROOT_URL:-http://localhost:3000}
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning
ports:
- "3000:3000"
depends_on:
- prometheus
restart: unless-stopped
# Loki - Log aggregation
loki:
image: grafana/loki:latest
container_name: timetracker-loki
volumes:
- ./loki/loki-config.yml:/etc/loki/local-config.yaml
- loki_data:/loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
restart: unless-stopped
# Promtail - Log shipping to Loki
promtail:
image: grafana/promtail:latest
container_name: timetracker-promtail
volumes:
- ./logs:/var/log/timetracker:ro
- ./promtail/promtail-config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
depends_on:
- loki
restart: unless-stopped
volumes:
app_data:
driver: local
app_logs:
driver: local
app_uploads:
driver: local
db_data:
driver: local
prometheus_data:
driver: local
grafana_data:
driver: local
loki_data:
driver: local
redis_data:
driver: local
EOF
# Kubernetes deployment (basic example)
cat > k8s-deployment.yml << EOF
# Kubernetes Deployment for TimeTracker ${VERSION}
apiVersion: apps/v1
kind: Deployment
metadata:
name: timetracker
labels:
app: timetracker
version: ${VERSION}
spec:
replicas: 2
selector:
matchLabels:
app: timetracker
template:
metadata:
labels:
app: timetracker
version: ${VERSION}
spec:
containers:
- name: timetracker
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}
ports:
- containerPort: 8080
name: http
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: timetracker-secrets
key: database-url
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: timetracker-secrets
key: secret-key
livenessProbe:
httpGet:
path: /_health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /_health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: timetracker
spec:
selector:
app: timetracker
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
EOF
echo "📄 Deployment manifests created"
- name: Upload deployment manifests
uses: actions/upload-artifact@v4
with:
name: deployment-manifests-${{ needs.determine-version.outputs.version }}
path: |
docker-compose.production.yml
k8s-deployment.yml
# ============================================================================
# Build Desktop Applications
# ============================================================================
build-desktop-windows:
name: Build Desktop - Windows
runs-on: windows-latest
needs: [determine-version]
continue-on-error: true
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Generate desktop icons
run: |
npm install
npm run generate:icons
- name: Install dependencies
working-directory: desktop
run: npm ci
- name: Build Windows
working-directory: desktop
run: npm run build:win
- name: Upload Windows installer
if: success()
uses: actions/upload-artifact@v4
with:
name: desktop-windows-${{ needs.determine-version.outputs.version }}
path: desktop/dist/*.exe
retention-days: 90
build-desktop-linux:
name: Build Desktop - Linux
runs-on: ubuntu-latest
needs: [determine-version]
continue-on-error: true
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Generate desktop icons
run: |
npm install
npm run generate:icons
- name: Install dependencies
working-directory: desktop
run: npm ci
- name: Build Linux
working-directory: desktop
run: npm run build:linux
- name: Upload Linux packages
if: success()
uses: actions/upload-artifact@v4
with:
name: desktop-linux-${{ needs.determine-version.outputs.version }}
path: |
desktop/dist/*.AppImage
desktop/dist/*.deb
retention-days: 90
build-desktop-macos:
name: Build Desktop - macOS
runs-on: macos-latest
needs: [determine-version]
continue-on-error: true
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Generate desktop icons
run: |
npm install
npm run generate:icons
chmod +x scripts/generate-macos-icon.sh
./scripts/generate-macos-icon.sh
- name: Install dependencies
working-directory: desktop
run: npm ci
- name: Build macOS
working-directory: desktop
run: npm run build:mac
- name: Upload macOS DMG
if: success()
uses: actions/upload-artifact@v4
with:
name: desktop-macos-${{ needs.determine-version.outputs.version }}
path: desktop/dist/*.dmg
retention-days: 90
# ============================================================================
# Build Mobile Applications
# ============================================================================
build-mobile-android:
name: Build Mobile - Android
runs-on: ubuntu-latest
needs: [determine-version]
continue-on-error: true
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.35.4'
channel: 'stable'
- name: Generate app icons
run: |
pip install Pillow
python scripts/generate-mobile-icon.py
- name: Install dependencies
working-directory: mobile
run: flutter pub get
- name: Generate launcher icons
working-directory: mobile
run: dart run flutter_launcher_icons
- name: Run mobile tests
working-directory: mobile
run: flutter test
- name: Build APK
working-directory: mobile
env:
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_TOKEN: ${{ secrets.OTEL_EXPORTER_OTLP_TOKEN }}
run: |
flutter build apk --release \
--dart-define=OTEL_EXPORTER_OTLP_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-}" \
--dart-define=OTEL_EXPORTER_OTLP_TOKEN="${OTEL_EXPORTER_OTLP_TOKEN:-}"
- name: Build App Bundle
working-directory: mobile
env:
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_TOKEN: ${{ secrets.OTEL_EXPORTER_OTLP_TOKEN }}
run: |
flutter build appbundle --release \
--dart-define=OTEL_EXPORTER_OTLP_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-}" \
--dart-define=OTEL_EXPORTER_OTLP_TOKEN="${OTEL_EXPORTER_OTLP_TOKEN:-}"
continue-on-error: true
- name: Upload Android APK
if: success()
uses: actions/upload-artifact@v4
with:
name: mobile-android-apk-${{ needs.determine-version.outputs.version }}
path: mobile/build/app/outputs/flutter-apk/app-release.apk
retention-days: 90
- name: Upload Android App Bundle
if: always()
uses: actions/upload-artifact@v4
with:
name: mobile-android-aab-${{ needs.determine-version.outputs.version }}
path: mobile/build/app/outputs/bundle/release/app-release.aab
if-no-files-found: ignore
retention-days: 90
build-mobile-ios:
name: Build Mobile - iOS
runs-on: macos-latest
needs: [determine-version]
continue-on-error: true
timeout-minutes: 45
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.35.4'
channel: 'stable'
- name: Generate app icons
run: |
pip install Pillow
python scripts/generate-mobile-icon.py
- name: Install dependencies
working-directory: mobile
run: flutter pub get
- name: Generate iOS platform files
working-directory: mobile
run: flutter create --platforms=ios .
- name: Generate launcher icons
working-directory: mobile
run: |
dart run flutter_launcher_icons
dart run flutter_launcher_icons -f flutter_launcher_icons_ios.yaml
- name: Run mobile tests
working-directory: mobile
run: flutter test
# Release mode is invalid for iOS simulator; this validates compilation and produces Runner.app for the artifact zip.
- name: Build iOS (simulator)
working-directory: mobile
env:
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }}
OTEL_EXPORTER_OTLP_TOKEN: ${{ secrets.OTEL_EXPORTER_OTLP_TOKEN }}
run: |
flutter build ios --simulator \
--dart-define=OTEL_EXPORTER_OTLP_ENDPOINT="${OTEL_EXPORTER_OTLP_ENDPOINT:-}" \
--dart-define=OTEL_EXPORTER_OTLP_TOKEN="${OTEL_EXPORTER_OTLP_TOKEN:-}"
- name: Create iOS archive
if: success()
working-directory: mobile
run: |
mkdir -p dist
if [ -d "build/ios/iphonesimulator/Runner.app" ]; then
cd build/ios/iphonesimulator
zip -r ../../../dist/TimeTracker-iOS-${{ needs.determine-version.outputs.version }}.zip Runner.app
cd ../../..
echo "✅ iOS simulator archive created successfully"
ls -lh dist/
else
echo "❌ ERROR: Runner.app not found at build/ios/iphonesimulator/Runner.app"
ls -la build/ios/ || true
ls -la build/ || true
find build -name "Runner.app" -type d 2>/dev/null || true
exit 1
fi
- name: Upload iOS build
if: success()
uses: actions/upload-artifact@v4
with:
name: mobile-ios-${{ needs.determine-version.outputs.version }}
path: mobile/dist/*.zip
if-no-files-found: error
retention-days: 90
# ============================================================================
# Create GitHub Release
# ============================================================================
create-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [build-and-push, determine-version, build-desktop-windows, build-desktop-linux, build-desktop-macos, build-mobile-android, build-mobile-ios]
if: github.event_name != 'release'
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download deployment manifests
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: deployment-manifests-${{ needs.determine-version.outputs.version }}
- name: Download desktop artifacts
uses: actions/download-artifact@v4
continue-on-error: true
with:
pattern: 'desktop-*'
merge-multiple: false
- name: Download mobile artifacts
uses: actions/download-artifact@v4
continue-on-error: true
with:
pattern: 'mobile-*'
merge-multiple: false
- name: Prepare release files
run: |
VERSION="${{ needs.determine-version.outputs.version }}"
RELEASE_DIR="release-files"
mkdir -p "$RELEASE_DIR"
# Move deployment manifests to release directory
if [ -f "docker-compose.production.yml" ]; then
cp docker-compose.production.yml "$RELEASE_DIR/" || true
fi
if [ -f "k8s-deployment.yml" ]; then
cp k8s-deployment.yml "$RELEASE_DIR/" || true
fi
# Organize desktop files
DESKTOP_DIR="$RELEASE_DIR/desktop"
mkdir -p "$DESKTOP_DIR"
# Windows
if [ -d "desktop-windows-$VERSION" ]; then
mkdir -p "$DESKTOP_DIR/windows"
cp desktop-windows-$VERSION/* "$DESKTOP_DIR/windows/" 2>/dev/null || true
fi
# Linux
if [ -d "desktop-linux-$VERSION" ]; then
mkdir -p "$DESKTOP_DIR/linux"
cp desktop-linux-$VERSION/* "$DESKTOP_DIR/linux/" 2>/dev/null || true
fi
# macOS
if [ -d "desktop-macos-$VERSION" ]; then
mkdir -p "$DESKTOP_DIR/macos"
cp desktop-macos-$VERSION/* "$DESKTOP_DIR/macos/" 2>/dev/null || true
fi
# Organize mobile files
MOBILE_DIR="$RELEASE_DIR/mobile"
mkdir -p "$MOBILE_DIR"
# Android APK
if [ -d "mobile-android-apk-$VERSION" ]; then
mkdir -p "$MOBILE_DIR/android"
cp mobile-android-apk-$VERSION/* "$MOBILE_DIR/android/" 2>/dev/null || true
fi
# Android AAB
if [ -d "mobile-android-aab-$VERSION" ]; then
mkdir -p "$MOBILE_DIR/android"
cp mobile-android-aab-$VERSION/* "$MOBILE_DIR/android/" 2>/dev/null || true
fi
# iOS
if [ -d "mobile-ios-$VERSION" ]; then
mkdir -p "$MOBILE_DIR/ios"
cp mobile-ios-$VERSION/* "$MOBILE_DIR/ios/" 2>/dev/null || true
fi
# Create file list for release (for debugging)
find "$RELEASE_DIR" -type f > release-files-list.txt || true
echo "Files to attach to release:"
cat release-files-list.txt || echo "No files found"
# Count files for summary
FILE_COUNT=$(find "$RELEASE_DIR" -type f | wc -l || echo "0")
echo "Total files to attach: $FILE_COUNT"
echo "file_count=$FILE_COUNT" >> $GITHUB_OUTPUT
- name: Generate changelog
id: changelog
run: |
VERSION="${{ needs.determine-version.outputs.version }}"
# Try to get previous tag
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$PREVIOUS_TAG" ]; then
CHANGELOG=$(git log ${PREVIOUS_TAG}..HEAD --pretty=format:"- %s (%an)" --no-merges)
else
CHANGELOG="Initial release"
fi
# Write changelog to file first
echo "$CHANGELOG" > CHANGELOG.md
# Add build status to changelog
echo "" >> CHANGELOG.md
echo "" >> CHANGELOG.md
echo "## 📦 Build Status" >> CHANGELOG.md
echo "" >> CHANGELOG.md
# Desktop builds
echo "### Desktop Applications" >> CHANGELOG.md
if [ "${{ needs.build-desktop-windows.result }}" == "success" ]; then
echo "✅ Windows build: Success" >> CHANGELOG.md
else
echo "⚠️ Windows build: Failed or skipped" >> CHANGELOG.md
fi
if [ "${{ needs.build-desktop-linux.result }}" == "success" ]; then
echo "✅ Linux build: Success" >> CHANGELOG.md
else
echo "⚠️ Linux build: Failed or skipped" >> CHANGELOG.md
fi
if [ "${{ needs.build-desktop-macos.result }}" == "success" ]; then
echo "✅ macOS build: Success" >> CHANGELOG.md
else
echo "⚠️ macOS build: Failed or skipped" >> CHANGELOG.md
fi
# Mobile builds
echo "" >> CHANGELOG.md
echo "### Mobile Applications" >> CHANGELOG.md
if [ "${{ needs.build-mobile-android.result }}" == "success" ]; then
echo "✅ Android build: Success" >> CHANGELOG.md
else
echo "⚠️ Android build: Failed or skipped" >> CHANGELOG.md
fi
if [ "${{ needs.build-mobile-ios.result }}" == "success" ]; then
echo "✅ iOS build: Success" >> CHANGELOG.md
else
echo "⚠️ iOS build: Failed or skipped" >> CHANGELOG.md
fi
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.determine-version.outputs.version }}
name: Release ${{ needs.determine-version.outputs.version }}
body_path: CHANGELOG.md
draft: false
prerelease: ${{ needs.determine-version.outputs.is_prerelease }}
files: |
release-files/docker-compose.production.yml
release-files/k8s-deployment.yml
release-files/desktop/**/*
release-files/mobile/**/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: false
# ============================================================================
# Trigger Demo Site Deploy (Render)
# ============================================================================
trigger-demo-deploy:
name: Trigger Demo Deploy
runs-on: ubuntu-latest
needs: [build-and-push]
continue-on-error: true
timeout-minutes: 2
steps:
- name: Trigger Render deploy hook
env:
RENDER_DEPLOY_HOOK_URL: ${{ secrets.TimeTrackerDemoRender }}
run: |
if [ -z "$RENDER_DEPLOY_HOOK_URL" ]; then
echo "⚠️ TimeTrackerDemoRender secret not configured - skipping demo deploy"
exit 0
fi
echo "🚀 Triggering Render deploy hook for demo site..."
HTTP_CODE=$(curl -s -o /tmp/render-response.txt -w "%{http_code}" -X POST "$RENDER_DEPLOY_HOOK_URL")
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
echo "✅ Render deploy triggered successfully (HTTP $HTTP_CODE)"
else
echo "❌ Render deploy hook returned HTTP $HTTP_CODE"
cat /tmp/render-response.txt || true
exit 1
fi
# ============================================================================
# Post-Release Summary
# ============================================================================
release-summary:
name: Release Summary
runs-on: ubuntu-latest
needs: [security-audit, build-and-push, determine-version, create-release, trigger-demo-deploy, build-desktop-windows, build-desktop-linux, build-desktop-macos, build-mobile-android, build-mobile-ios]
if: always()
steps:
- name: Create release summary
run: |
echo "## 🚀 Release ${{ needs.determine-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Core Build Status" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Security: ${{ needs.security-audit.result }}" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Docker Build: ${{ needs.build-and-push.result }}" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Release: ${{ needs.create-release.result }}" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Demo Deploy: ${{ needs.trigger-demo-deploy.result }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Desktop Applications" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.build-desktop-windows.result }}" == "success" ]; then
echo "- ✅ Windows: Success" >> $GITHUB_STEP_SUMMARY
else
echo "- ⚠️ Windows: ${{ needs.build-desktop-windows.result }}" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.build-desktop-linux.result }}" == "success" ]; then
echo "- ✅ Linux: Success" >> $GITHUB_STEP_SUMMARY
else
echo "- ⚠️ Linux: ${{ needs.build-desktop-linux.result }}" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.build-desktop-macos.result }}" == "success" ]; then
echo "- ✅ macOS: Success" >> $GITHUB_STEP_SUMMARY
else
echo "- ⚠️ macOS: ${{ needs.build-desktop-macos.result }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Mobile Applications" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.build-mobile-android.result }}" == "success" ]; then
echo "- ✅ Android: Success" >> $GITHUB_STEP_SUMMARY
else
echo "- ⚠️ Android: ${{ needs.build-mobile-android.result }}" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.build-mobile-ios.result }}" == "success" ]; then
echo "- ✅ iOS: Success" >> $GITHUB_STEP_SUMMARY
else
echo "- ⚠️ iOS: ${{ needs.build-mobile-ios.result }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "ℹ️ *Full test suite already ran on PR before merge*" >> $GITHUB_STEP_SUMMARY
echo "ℹ️ *Desktop and mobile builds are optional - release continues even if they fail*" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔐 Analytics Configuration" >> $GITHUB_STEP_SUMMARY
echo "Analytics credentials were **successfully injected** from GitHub Secret Store:" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **OTLP**: Injected from \`OTEL_EXPORTER_OTLP_ENDPOINT\` + \`OTEL_EXPORTER_OTLP_TOKEN\` secrets" >> $GITHUB_STEP_SUMMARY
echo "- ✅ **Sentry DSN**: Injected from \`SENTRY_DSN\` secret" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "> 📍 **Secret Location**: Repository Settings → Secrets and variables → Actions" >> $GITHUB_STEP_SUMMARY
echo "> 🔒 **Security**: Secrets are embedded at build time and never exposed in logs" >> $GITHUB_STEP_SUMMARY
echo "> 👥 **Privacy**: Users maintain full control via opt-in/opt-out in admin dashboard" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
VERSION="${{ needs.determine-version.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
echo "### 🐳 Docker Images" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION_NO_V}" >> $GITHUB_STEP_SUMMARY
echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY
echo "${{ env.DOCKERHUB_IMAGE }}:${VERSION_NO_V}" >> $GITHUB_STEP_SUMMARY
echo "${{ env.DOCKERHUB_IMAGE }}:latest" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📦 Quick Deploy" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
echo "# Pull the image" >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION_NO_V}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "# Deploy with docker-compose" >> $GITHUB_STEP_SUMMARY
echo "docker-compose -f docker-compose.production.yml up -d" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
================================================
FILE: .github/workflows/ci-comprehensive.yml
================================================
name: Comprehensive CI Pipeline
# This workflow runs comprehensive tests on pull requests
#
# Test Strategy:
# - Smoke tests (fast, critical) run first
# - Unit, integration, security, and code quality tests run in parallel
# - Full test suite with PostgreSQL runs for PRs to main/master and RC branches
# - Docker build test ensures the image builds correctly
# - Test summary posted as PR comment
#
# All tests must pass before a PR can be merged
#
# Workflow triggers:
# - PRs to RC branches (from develop) - validates code before RC build
# - PRs to main/master (from RC) - validates code before release
on:
pull_request:
branches: [ main, master, 'rc', 'rc/**' ]
env:
PYTHON_VERSION: '3.11'
POSTGRES_VERSION: '16'
jobs:
# ============================================================================
# Smoke Tests - Fast, critical tests that run first
# ============================================================================
smoke-tests:
name: Smoke Tests (Quick)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
pip install -e .
- name: Run smoke tests
env:
PYTHONPATH: ${{ github.workspace }}
run: |
pytest -m smoke -v --tb=short --no-cov -n auto
- name: Upload smoke test results
if: always()
uses: actions/upload-artifact@v4
with:
name: smoke-test-results
path: |
.pytest_cache/
test-results/
# ============================================================================
# Unit Tests - Fast, isolated tests
# ============================================================================
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
needs: smoke-tests
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
test-group: [models, routes, api, utils]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
pip install -e .
- name: Run unit tests - ${{ matrix.test-group }}
env:
PYTHONPATH: ${{ github.workspace }}
run: |
if [ "${{ matrix.test-group }}" == "api" ]; then
pytest -m "api and integration" -v -n auto --cov=app --cov-report=xml --cov-report=html --cov-report=term-missing
elif [ "${{ matrix.test-group }}" == "routes" ]; then
pytest -m "unit and routes" -v -n 0 --cov=app --cov-report=xml --cov-report=html --cov-report=term-missing
else
pytest -m "unit and ${{ matrix.test-group }}" -v -n auto --cov=app --cov-report=xml --cov-report=html --cov-report=term-missing
fi
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
flags: unit-${{ matrix.test-group }}
name: unit-${{ matrix.test-group }}
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: unit-test-results-${{ matrix.test-group }}
path: |
htmlcov/
coverage.xml
# ============================================================================
# Integration Tests - Medium speed, component interaction tests
# ============================================================================
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: smoke-tests
timeout-minutes: 15
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test_password
POSTGRES_USER: test_user
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
pip install -e .
- name: Run integration tests
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
PYTHONPATH: ${{ github.workspace }}
INSTALLATION_CONFIG_DIR: ${{ github.workspace }}/.test_installation_config
run: |
pytest -m integration -v -n auto --cov=app --cov-report=xml --cov-report=html --cov-report=term-missing
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
flags: integration
name: integration-tests
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: integration-test-results
path: |
htmlcov/
coverage.xml
# ============================================================================
# Security Tests
# ============================================================================
security-tests:
name: Security Tests
runs-on: ubuntu-latest
needs: smoke-tests
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
pip install -e .
- name: Run security tests
env:
PYTHONPATH: ${{ github.workspace }}
run: |
pytest -m security -v --tb=short
- name: Run Safety dependency check
run: |
safety check --file requirements.txt --json > safety-report.json
- name: Upload security reports
if: always()
uses: actions/upload-artifact@v4
with:
name: security-reports
path: |
safety-report.json
# ============================================================================
# Code Quality
# ============================================================================
code-quality:
name: Code Quality Checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
pip install -e .
- name: Run flake8
run: |
flake8 app/ --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 app/ --count --max-complexity=10 --max-line-length=120 --statistics
- name: Run black (format check)
run: black --check app/
- name: Run isort (import check)
run: isort --check-only app/
- name: Run mypy
run: mypy app/ || true
# ============================================================================
# Docker Build Test
# ============================================================================
docker-build:
name: Docker Build Test
runs-on: ubuntu-latest
needs: [unit-tests, integration-tests]
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
run: |
docker build -t timetracker-test:pr-${{ github.event.pull_request.number || 'dev' }} .
- name: Test Docker container startup
run: |
# Start container
CONTAINER_ID=$(docker run -d --name test-container \
-p 8080:8080 \
-e DATABASE_URL="sqlite:////app/test.db" \
-e SECRET_KEY="test-secret-key-for-ci-only-$(openssl rand -hex 32)" \
-e FLASK_ENV="development" \
timetracker-test:pr-${{ github.event.pull_request.number || 'dev' }})
echo "🐳 Started container: $CONTAINER_ID"
# Wait for container to be ready (increased timeout for migrations)
HEALTH_CHECK_PASSED=false
for i in {1..60}; do
# Check if container is still running
if ! docker ps -q --filter "name=test-container" | grep -q .; then
echo "❌ Container exited unexpectedly!"
echo ""
echo "📋 Container logs:"
docker logs test-container
echo ""
echo "🔍 Container status:"
docker ps -a --filter "name=test-container"
exit 1
fi
# Try health check
if curl -f http://localhost:8080/_health >/dev/null 2>&1; then
echo "✅ Container health check passed (attempt $i/60)"
HEALTH_CHECK_PASSED=true
break
fi
# Show progress
if [ $((i % 10)) -eq 0 ]; then
echo "⏳ Still waiting for container... ($i/60)"
echo "📊 Last 10 log lines:"
docker logs --tail 10 test-container
else
echo "⏳ Waiting for container... ($i/60)"
fi
sleep 2
done
# Show full logs for debugging
echo ""
echo "📋 Full container logs:"
docker logs test-container
echo ""
# Check if health check passed
if [ "$HEALTH_CHECK_PASSED" = false ]; then
echo "❌ Health check never passed after 120 seconds"
echo ""
echo "🔍 Container inspect:"
docker inspect test-container
echo ""
echo "🔍 Container status:"
docker ps -a --filter "name=test-container"
exit 1
fi
# Final health check with detailed output
echo "🔍 Final health check:"
curl -v http://localhost:8080/_health || {
echo "❌ Final health check failed"
echo "📋 Latest logs:"
docker logs --tail 50 test-container
exit 1
}
echo "✅ Docker container test completed successfully"
# Cleanup
docker stop test-container
docker rm test-container
# ============================================================================
# Full Test Suite (runs on all PRs to main/master/rc)
# ============================================================================
full-test-suite:
name: Full Test Suite with PostgreSQL
runs-on: ubuntu-latest
needs: [smoke-tests, unit-tests, integration-tests]
if: github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'master' || startsWith(github.base_ref, 'rc'))
timeout-minutes: 30
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test_password
POSTGRES_USER: test_user
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-test.txt
pip install -e .
- name: Validate database migrations
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
echo "🔍 Validating database migrations..."
# Check if there are migration-related changes
if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E "(app/models/|migrations/)" > /dev/null; then
echo "📋 Migration-related changes detected"
# Initialize fresh database
flask db upgrade
# Test migration rollback
CURRENT_MIGRATION=$(flask db current)
echo "Current migration: $CURRENT_MIGRATION"
if [ -n "$CURRENT_MIGRATION" ] && [ "$CURRENT_MIGRATION" != "None" ]; then
echo "Testing migration operations..."
flask db upgrade head
echo "✅ Migration validation passed"
fi
# Test with sample data
python -c "
from app import create_app, db
from app.models.user import User
from app.models.project import Project
from app.models.client import Client
app = create_app()
with app.app_context():
user = User(username='test_user', role='user')
db.session.add(user)
db.session.commit()
client = Client(name='Test Client', description='Test client')
db.session.add(client)
db.session.commit()
project = Project(name='Test Project', client_id=client.id, description='Test project')
db.session.add(project)
db.session.commit()
print('✅ Sample data created and validated successfully')
"
else
echo "ℹ️ No migration-related changes detected"
fi
- name: Run full test suite
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
PYTHONPATH: ${{ github.workspace }}
INSTALLATION_CONFIG_DIR: ${{ github.workspace }}/.test_installation_config
run: |
pytest -v -n auto --cov=app --cov-report=xml --cov-report=html --cov-report=term-missing \
--cov-fail-under=35 --junitxml=junit.xml --maxfail=5
- name: Upload full coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
flags: full-suite
name: full-test-suite
- name: Upload full test results
if: always()
uses: actions/upload-artifact@v4
with:
name: full-test-results
path: |
htmlcov/
coverage.xml
junit.xml
- name: Publish full test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: junit.xml
check_name: Full Test Suite Results
# ============================================================================
# Test Summary and PR Comment
# ============================================================================
test-summary:
name: Test Summary
runs-on: ubuntu-latest
needs: [smoke-tests, unit-tests, integration-tests, security-tests, code-quality, docker-build, full-test-suite]
if: always() && github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Generate test summary
uses: actions/github-script@v7
with:
script: |
const jobs = [
{ name: 'Smoke Tests', result: '${{ needs.smoke-tests.result }}' },
{ name: 'Unit Tests', result: '${{ needs.unit-tests.result }}' },
{ name: 'Integration Tests', result: '${{ needs.integration-tests.result }}' },
{ name: 'Security Tests', result: '${{ needs.security-tests.result }}' },
{ name: 'Code Quality', result: '${{ needs.code-quality.result }}' },
{ name: 'Docker Build', result: '${{ needs.docker-build.result }}' },
{ name: 'Full Test Suite', result: '${{ needs.full-test-suite.result }}' }
];
const passed = jobs.filter(j => j.result === 'success').length;
const failed = jobs.filter(j => j.result === 'failure').length;
const total = jobs.length;
let emoji = failed === 0 ? '✅' : '❌';
let status = failed === 0 ? 'All tests passed!' : `${failed} test suite(s) failed`;
let commentBody = `## ${emoji} CI Test Results\n\n`;
commentBody += `**Overall Status:** ${status}\n\n`;
commentBody += `**Test Results:** ${passed}/${total} passed\n\n`;
commentBody += `### Test Suites:\n\n`;
for (const job of jobs) {
const icon = job.result === 'success' ? '✅' :
job.result === 'failure' ? '❌' :
job.result === 'skipped' ? '⏭️' : '⏸️';
commentBody += `- ${icon} ${job.name}: **${job.result}**\n`;
}
commentBody += `\n---\n`;
commentBody += `*Commit: ${context.sha.substring(0, 7)}*\n`;
commentBody += `*Workflow: [${context.runId}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*`;
// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('CI Test Results')
);
if (botComment) {
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
}
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI/CD Pipeline
# DISABLED: This workflow is disabled in favor of the Comprehensive CI Pipeline (ci-comprehensive.yml)
# Only the Comprehensive CI Pipeline should run for all CI/CD operations
on:
workflow_dispatch: # Only allows manual trigger, effectively disabling automatic runs
# pull_request:
# branches: [ main ]
# types: [ opened, synchronize, reopened, ready_for_review ]
env:
PYTHON_VERSION: '3.11'
POSTGRES_VERSION: '16'
jobs:
lint:
name: Lint and Code Quality
runs-on: ubuntu-latest
if: false # DISABLED: This workflow is disabled in favor of ci-comprehensive.yml
# if: github.event.pull_request.head.ref == 'rc' || startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 black pylint bandit safety
- name: Run Black (code formatting check)
run: black --check app tests
- name: Run Flake8 (linting)
run: flake8 app tests --max-line-length=120 --extend-ignore=E203,W503
continue-on-error: true
- name: Run Pylint
run: pylint app --disable=all --enable=errors --max-line-length=120
continue-on-error: true
- name: Run Bandit (security linting)
run: bandit -r app -f json -o bandit-report.json
continue-on-error: true
- name: Run Safety (dependency vulnerability check)
run: safety check --json
continue-on-error: true
test:
name: Test Suite
runs-on: ubuntu-latest
if: false # DISABLED: This workflow is disabled in favor of ci-comprehensive.yml
# if: github.event.pull_request.head.ref == 'rc' || startsWith(github.event.pull_request.head.ref, 'rc/')
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: timetracker
POSTGRES_PASSWORD: timetracker
POSTGRES_DB: timetracker_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install -r requirements-test.txt
- name: Run database migrations
env:
DATABASE_URL: postgresql+psycopg2://timetracker:timetracker@localhost:5432/timetracker_test
run: |
flask db upgrade
- name: Run tests with coverage
env:
DATABASE_URL: postgresql+psycopg2://timetracker:timetracker@localhost:5432/timetracker_test
FLASK_ENV: testing
SECRET_KEY: test-secret-key-for-ci
run: |
pytest -n auto --cov=app --cov-report=xml --cov-report=html --cov-report=term-missing tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
security:
name: Security Scan
runs-on: ubuntu-latest
if: false # DISABLED: This workflow is disabled in favor of ci-comprehensive.yml
# if: github.event.pull_request.head.ref == 'rc' || startsWith(github.event.pull_request.head.ref, 'rc/')
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install bandit safety semgrep
- name: Run Bandit security scan
run: bandit -r app -f json -o bandit-report.json
continue-on-error: true
- name: Run Safety dependency check
run: safety check --json
continue-on-error: true
- name: Run Semgrep security scan
run: semgrep --config=auto app/
continue-on-error: true
build:
name: Docker Build
runs-on: ubuntu-latest
needs: [lint, test]
if: false # DISABLED: This workflow is disabled in favor of ci-comprehensive.yml
# if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub (if needed)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME || '' }}
password: ${{ secrets.DOCKER_PASSWORD || '' }}
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
continue-on-error: true
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: timetracker:latest
cache-from: type=registry,ref=timetracker:latest
cache-to: type=inline
================================================
FILE: .github/workflows/crowdin-sync.yml
================================================
# Manual Crowdin sync: uploads English source .po, downloads translations, opens a PR.
# Prerequisites: repo secrets CROWDIN_PROJECT_ID and CROWDIN_PERSONAL_TOKEN (see docs/CONTRIBUTING_TRANSLATIONS.md).
name: Crowdin sync
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Crowdin push and pull
uses: crowdin/github-action@v2
with:
upload_sources: true
# Set to true once to seed Crowdin with existing translations/…/messages.po, then set back to false.
upload_translations: false
download_translations: true
localization_branch_name: i18n/crowdin
create_pull_request: true
pull_request_title: "chore(i18n): Crowdin translations"
pull_request_body: "Automated sync from Crowdin. Review placeholders and `no` vs `nb` paths before merge."
commit_message: "chore(i18n): sync Crowdin translations"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
================================================
FILE: .github/workflows/migration-check.yml
================================================
name: Database Migration Validation
on:
pull_request:
paths:
- 'app/models/**'
- 'migrations/**'
- 'requirements.txt'
jobs:
validate-migrations:
runs-on: ubuntu-latest
outputs:
migration_changes: ${{ steps.migration_check.outputs.migration_changes }}
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test_password
POSTGRES_USER: test_user
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Check for migration changes
id: migration_check
run: |
# Check if there are changes to models or migrations
if git diff --name-only HEAD~1 | grep -E "(app/models/|migrations/)" > /dev/null; then
echo "migration_changes=true" >> $GITHUB_OUTPUT
echo "📋 Migration-related changes detected"
else
echo "migration_changes=false" >> $GITHUB_OUTPUT
echo "ℹ️ No migration-related changes detected"
fi
- name: Validate migration consistency
if: steps.migration_check.outputs.migration_changes == 'true'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
echo "🔍 Validating migration consistency..."
# Show current directory and check for migration files
echo "Current directory: $(pwd)"
echo "Migration files:"
ls -la migrations/versions/ | tail -5
# Initialize fresh database with verbose output
echo "Running flask db upgrade..."
if ! flask db upgrade; then
echo ""
echo "❌ Migration failed!"
echo "Checking database state..."
flask db current || true
echo ""
echo "Checking migration history..."
flask db history | tail -10 || true
exit 1
fi
echo "✅ Migrations completed successfully"
# Generate a new migration from current models
echo "Generating test migration to check consistency..."
if ! flask db migrate -m "Test migration consistency" --rev-id test_consistency; then
echo "⚠️ Flask db migrate encountered an error"
echo "This might indicate schema drift or migration issues"
# Check if a migration file was still created despite the error
MIGRATION_FILE=$(find migrations/versions -name "*test_consistency*.py" 2>/dev/null | head -1)
if [ -f "$MIGRATION_FILE" ]; then
echo "Migration file was created: $MIGRATION_FILE"
cat "$MIGRATION_FILE"
rm "$MIGRATION_FILE"
fi
# Don't fail the workflow - this might be expected behavior
echo "Continuing validation despite migration generation warning..."
fi
# Check if the generated migration is empty (no changes needed)
MIGRATION_FILE=$(find migrations/versions -name "*test_consistency*.py" 2>/dev/null | head -1)
if [ -f "$MIGRATION_FILE" ]; then
# Check if migration has actual changes
if grep -q "op\." "$MIGRATION_FILE"; then
echo "⚠️ Migration inconsistency detected!"
echo "The database schema doesn't match the models."
echo "Generated migration file: $MIGRATION_FILE"
cat "$MIGRATION_FILE"
# For now, we'll treat this as a warning rather than a failure
# The schema drift existed before this PR and should be addressed separately
echo "📝 Note: This indicates existing schema drift that should be addressed in a separate PR."
echo "✅ Continuing with migration validation as the payment tracking changes are isolated."
# Clean up test migration
rm "$MIGRATION_FILE"
else
echo "✅ Migration consistency validated - no schema drift detected"
# Clean up test migration
rm "$MIGRATION_FILE"
fi
else
echo "✅ No migration file generated - models are in sync"
fi
- name: Test migration rollback safety
if: steps.migration_check.outputs.migration_changes == 'true'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
set -e
echo "🔄 Testing migration rollback safety..."
# Get current migration (trim whitespace and optional "(head)" suffix)
CURRENT_MIGRATION=$(flask db current | sed 's/ (head)$//' | tr -d '[:space:]')
echo "Current migration: $CURRENT_MIGRATION"
if [ -z "$CURRENT_MIGRATION" ] || [ "$CURRENT_MIGRATION" = "None" ]; then
echo "ℹ️ No migrations to test rollback on"
exit 0
fi
# Resolve parent revision via Alembic (avoids ambiguous "downgrade -1" with merge revisions)
ROLLBACK_RESULT=$(python <<'PYEOF'
import os
import sys
from app import create_app, db
from alembic.config import Config
from alembic.script import ScriptDirectory
app = create_app()
with app.app_context():
r = db.session.execute(db.text("SELECT version_num FROM alembic_version"))
rows = r.fetchall()
if not rows:
print("SKIP")
sys.exit(0)
current_rev = rows[0][0]
config = Config(os.path.join(os.getcwd(), "migrations", "alembic.ini"))
script = ScriptDirectory.from_config(config)
rev = script.get_revision(current_rev)
if rev is None:
print("SKIP")
sys.exit(0)
down = rev.down_revision
if down is None:
print("SKIP")
elif isinstance(down, tuple):
print("SKIP")
else:
print("PARENT:" + down)
PYEOF
)
if [ "$ROLLBACK_RESULT" = "SKIP" ]; then
echo "ℹ️ At base or merge revision — skipping downgrade, verifying upgrade head..."
if ! flask db upgrade head; then
echo "❌ Rollback test failed: upgrade head failed after skip"
exit 1
fi
echo "✅ Migration rollback test passed (downgrade skipped, upgrade head OK)"
exit 0
fi
PARENT_REV="${ROLLBACK_RESULT#PARENT:}"
if [ -z "$PARENT_REV" ]; then
echo "❌ Rollback test failed: could not resolve parent revision"
exit 1
fi
echo "Testing migration rollback (downgrade to $PARENT_REV, then upgrade head)..."
if ! flask db downgrade "$PARENT_REV"; then
echo "❌ Rollback test failed: downgrade to $PARENT_REV failed"
exit 1
fi
if ! flask db upgrade head; then
echo "❌ Rollback test failed: upgrade head failed after downgrade"
exit 1
fi
echo "✅ Migration rollback test passed"
- name: Test migration with sample data
if: steps.migration_check.outputs.migration_changes == 'true'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
echo "📊 Testing migration with sample data..."
# Create sample data script
python <<'EOF'
from app import create_app, db
from app.models.user import User
from app.models.project import Project
from app.models.client import Client
import datetime
app = create_app()
with app.app_context():
# Create test user
user = User(
username='test_user',
role='user'
)
db.session.add(user)
db.session.commit() # Commit to get user ID
# Create test client
client = Client(
name='Test Client',
description='Test client for migration validation'
)
db.session.add(client)
db.session.commit() # Commit to get client ID
# Create test project
project = Project(
name='Test Project',
client_id=client.id,
description='Test project for migration validation'
)
db.session.add(project)
db.session.commit()
print('✅ Sample data created successfully')
EOF
# Verify data integrity after migration
python <<'EOF'
from app import create_app, db
from app.models.user import User
from app.models.project import Project
from app.models.client import Client
app = create_app()
with app.app_context():
user_count = User.query.count()
project_count = Project.query.count()
client_count = Client.query.count()
print(f'Users: {user_count}, Projects: {project_count}, Clients: {client_count}')
if user_count > 0 and project_count > 0 and client_count > 0:
print('✅ Data integrity verified after migration')
else:
print('❌ Data integrity check failed')
exit(1)
EOF
- name: Generate migration report
if: steps.migration_check.outputs.migration_changes == 'true'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
echo "📋 Generating migration report..."
# Get migration history
echo "## Migration History" > migration_report.md
echo "" >> migration_report.md
flask db history --verbose >> migration_report.md
# Get current schema info
echo "" >> migration_report.md
echo "## Current Schema" >> migration_report.md
echo "" >> migration_report.md
python <<'EOF' >> migration_report.md
from app import create_app, db
from sqlalchemy import inspect
app = create_app()
with app.app_context():
inspector = inspect(db.engine)
tables = inspector.get_table_names()
print('### Tables:')
for table in sorted(tables):
print(f'- {table}')
columns = inspector.get_columns(table)
for column in columns:
print(f' - {column["name"]}: {column["type"]}')
EOF
cat migration_report.md
- name: Upload migration report
if: steps.migration_check.outputs.migration_changes == 'true'
uses: actions/upload-artifact@v4
with:
name: migration-report
path: migration_report.md
comment-on-pr:
runs-on: ubuntu-latest
needs: validate-migrations
if: github.event_name == 'pull_request' && always()
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Comment migration status on PR
uses: actions/github-script@v7
with:
script: |
const success = '${{ needs.validate-migrations.result }}' === 'success';
const migrationChanges = '${{ needs.validate-migrations.outputs.migration_changes }}' === 'true';
let commentBody = '## Database Migration Validation\n\n';
if (migrationChanges) {
if (success) {
commentBody += ':white_check_mark: **Migration validation passed!**\n\n';
commentBody += '**Completed checks:**\n';
commentBody += '- :white_check_mark: Migration consistency validation (with schema drift warnings)\n';
commentBody += '- :white_check_mark: Rollback safety test\n';
commentBody += '- :white_check_mark: Data integrity verification\n\n';
commentBody += '**The database migrations are safe to apply.** :rocket:\n\n';
commentBody += ':memo: **Note:** Schema drift warnings indicate existing model/migration mismatches that existed before this PR. These should be addressed in a separate schema alignment PR.\n';
} else {
commentBody += ':x: **Migration validation failed!**\n\n';
commentBody += '**Issues detected:**\n';
commentBody += '- Migration consistency problems\n';
commentBody += '- Rollback safety issues\n';
commentBody += '- Data integrity concerns\n\n';
commentBody += '**Please review the migration files and fix the issues before merging.** :warning:\n';
}
} else {
commentBody += ':information_source: **No migration-related changes detected.**\n\n';
commentBody += 'This PR does not modify database models or migrations.\n';
}
commentBody += '\n---\n*This comment was automatically generated by the Migration Validation workflow.*';
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Database Migration Validation')
);
if (botComment) {
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
}
================================================
FILE: .github/workflows/static.yml
================================================
name: Deploy to GitHub Pages
on:
release:
types: [published]
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: '.'
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows-archive/ci.yml.backup
================================================
name: Continuous Integration
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
env:
PYTHON_VERSION: '3.11'
jobs:
test-database-migrations:
runs-on: ubuntu-latest
strategy:
matrix:
db_type: [postgresql, sqlite]
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: test_password
POSTGRES_USER: test_user
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
run: pip install -r requirements.txt
- name: Test PostgreSQL migrations
if: matrix.db_type == 'postgresql'
env:
DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
echo "Testing PostgreSQL migrations..."
flask db upgrade
python -c "from app import create_app, db; app = create_app(); app.app_context().push(); print('PostgreSQL migration successful')"
flask db downgrade base
flask db upgrade
echo "PostgreSQL migration rollback/upgrade test passed"
- name: Test SQLite migrations
if: matrix.db_type == 'sqlite'
env:
DATABASE_URL: sqlite:///test.db
FLASK_APP: app.py
FLASK_ENV: testing
run: |
echo "Testing SQLite migrations..."
flask db upgrade
python -c "from app import create_app, db; app = create_app(); app.app_context().push(); print('SQLite migration successful')"
flask db downgrade base
flask db upgrade
echo "SQLite migration rollback/upgrade test passed"
test-docker-build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Test Docker build
run: |
docker build -t timetracker-test:latest .
echo "Docker build successful"
- name: Test Docker container startup
run: |
# Start container in background
docker run -d --name test-container -p 8080:8080 \
-e DATABASE_URL="sqlite:///test.db" \
timetracker-test:latest
# Wait for container to be ready
for i in {1..30}; do
if curl -f http://localhost:8080/_health >/dev/null 2>&1; then
echo "Container health check passed"
break
fi
echo "Waiting for container to be ready... ($i/30)"
sleep 2
done
# Show container logs for debugging
docker logs test-container
# Stop container
docker stop test-container
docker rm test-container
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install security tools
run: |
pip install safety bandit
- name: Run safety (dependency vulnerability scan)
run: safety check --file requirements.txt
- name: Run bandit (security linting)
run: bandit -r app/ -f json -o bandit-report.json || true
- name: Upload security report
uses: actions/upload-artifact@v4
if: always()
with:
name: security-report
path: bandit-report.json
create-pr-preview:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
needs: [test-database-migrations, test-docker-build]
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('CI Pipeline Status')
);
const commentBody = [
'## CI Pipeline Status',
'',
'**All checks passed!** :white_check_mark:',
'',
'**Completed Checks:**',
'- :white_check_mark: Database migration tests (PostgreSQL & SQLite)',
'- :white_check_mark: Docker build and startup test',
'- :white_check_mark: Security vulnerability scan',
'',
'**Ready for review and merge** :rocket:',
'',
'---',
'*This comment was automatically generated by the CI pipeline.*'
].join('\n');
if (botComment) {
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
}
================================================
FILE: .github/workflows-archive/docker-publish.yml.backup
================================================
name: Build and Publish TimeTracker Docker Image
on:
push:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
branches: [ main ]
release:
types: [ published ]
workflow_dispatch:
inputs:
version:
description: 'Custom version tag (e.g., v1.2.3, build-123)'
required: false
default: ''
env:
REGISTRY: ghcr.io
IMAGE_NAME: drytrix/timetracker
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
include:
- name: amd64
platform: linux/amd64
- name: arm64
platform: linux/arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for better versioning
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Determine version
id: version
run: |
# Version determination per requirement:
# - If a version tag is available, use that (release tag or git tag)
# - Otherwise use dev-{buildnumber}
if [[ "${{ github.event.inputs.version }}" != "" ]]; then
VERSION="${{ github.event.inputs.version }}"
VERSION_SOURCE="manual"
elif [[ "${{ github.event_name }}" == "release" && "${{ github.event.release.tag_name }}" != "" ]]; then
VERSION="${{ github.event.release.tag_name }}"
VERSION_SOURCE="release"
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
VERSION=${GITHUB_REF#refs/tags/}
VERSION_SOURCE="git_tag"
else
BUILD_NUMBER=${{ github.run_number }}
VERSION="dev-${BUILD_NUMBER}"
VERSION_SOURCE="dev_build"
fi
# Clean version string (replace invalid characters)
VERSION=$(echo "$VERSION" | sed 's/[^a-zA-Z0-9._-]/-/g')
# Set outputs
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "version_source=$VERSION_SOURCE" >> $GITHUB_OUTPUT
echo "branch_name=${BRANCH_NAME:-}" >> $GITHUB_OUTPUT
echo "build_number=${BUILD_NUMBER:-}" >> $GITHUB_OUTPUT
echo "=== Version Information ==="
echo "Version: $VERSION"
echo "Source: $VERSION_SOURCE"
echo "Branch: ${BRANCH_NAME:-N/A}"
echo "Build Number: ${BUILD_NUMBER:-N/A}"
echo "Commit SHA: ${GITHUB_SHA::8}"
echo "=========================="
- name: Check files and create combined Dockerfile
run: |
echo "--- Checking available files ---"
pwd
ls -la
echo "--- Checking if requirements.txt exists ---"
if [ -f requirements.txt ]; then
echo "requirements.txt found:"
cat requirements.txt
else
echo "requirements.txt NOT found!"
echo "Available .txt files:"
find . -name "*.txt" -type f
fi
echo "--- Creating combined Dockerfile ---"
cp Dockerfile Dockerfile.final
# Ensure port 8080 is exposed in the final Dockerfile
if ! grep -q "^EXPOSE 8080" Dockerfile.final; then
echo "\n# Ensure required port is exposed" >> Dockerfile.final
echo "EXPOSE 8080" >> Dockerfile.final
fi
echo "Combined Dockerfile created successfully"
- name: Build Docker image
run: |
IMAGE_ID=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
VERSION="${{ steps.version.outputs.version }}"
echo "Building Docker image..."
echo "Image ID: $IMAGE_ID"
echo "Version: $VERSION"
# Build the Docker image with version label
docker build \
-f Dockerfile.final \
--label "org.opencontainers.image.version=$VERSION" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
--label "org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
--label "org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}" \
--label "org.opencontainers.image.exposedPorts=8080" \
--build-arg APP_VERSION="$VERSION" \
-t $IMAGE_ID:$VERSION \
.
# Determine publish tags
if [[ "${{ github.event_name }}" == "release" ]]; then
# Release: publish version and latest
docker tag $IMAGE_ID:$VERSION $IMAGE_ID:latest
echo "Release build: will push tags [$VERSION, latest]"
else
# Non-release: publish development
docker tag $IMAGE_ID:$VERSION $IMAGE_ID:development
echo "Non-release build: will push tag [development]"
fi
- name: Push Docker image
if: github.event_name != 'pull_request'
run: |
IMAGE_ID=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
VERSION="${{ steps.version.outputs.version }}"
echo "Pushing Docker image..."
echo "Image ID: $IMAGE_ID"
echo "Version: $VERSION"
if [[ "${{ github.event_name }}" == "release" ]]; then
# Push version and latest
docker push $IMAGE_ID:$VERSION
docker push $IMAGE_ID:latest
else
# Push only development
docker push $IMAGE_ID:development
fi
- name: Generate build summary
run: |
echo "=== Build Summary ==="
echo "Repository: ${{ github.repository }}"
echo "Event: ${{ github.event_name }}"
echo "Version: ${{ steps.version.outputs.version }}"
echo "Version Source: ${{ steps.version.outputs.version_source }}"
echo "Branch: ${{ steps.version.outputs.branch_name }}"
echo "Build Number: ${{ steps.version.outputs.build_number }}"
echo "Commit: ${{ github.sha }}"
echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}"
echo "===================="
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.find(comment => comment.user.type === 'Bot' && comment.body.includes('Docker build completed'));
const commentBody = `## 🐳 Docker Build Completed
**Build Information:**
- **Version:** \`${{ steps.version.outputs.version }}\`
- **Source:** ${{ steps.version.outputs.version_source }}
- **Branch:** ${{ steps.version.outputs.branch_name }}
- **Build Number:** ${{ steps.version.outputs.build_number }}
- **Commit:** \`${{ github.sha }}\`
**Image:** \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}\`
> This is a preview build. The image will be pushed when merged to main.
---
*This comment was automatically generated by the Docker build workflow.*`;
if (botComment) {
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: commentBody,
});
}
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
!mobile/lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Application specific
# gettext template produced by `pybabel extract` (regenerate as needed; do not commit)
messages.pot
data/
# Flutter app source lives under mobile/lib/data/ (do not treat as runtime data dir)
!mobile/lib/data/
!mobile/lib/data/**
logs/
backups/
*.db
*.sqlite
*.sqlite3
# Docker
.dockerignore
# IDE
.vscode/
.idea/
.cursor/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Temporary files
*.tmp
*.temp
# Test artifacts
test.db
test_*.db
junit.xml
*.xml
!mobile/android/app/src/main/AndroidManifest.xml
test-results/
.pytest_tmp*/
bandit-report.json
safety-report.json
migration_report.md
# Test output
.testmondata
# Install / build logs
install_log.txt
# Benchmark output
.benchmarks/
# SSL Certificates (generated by mkcert)
nginx/ssl/*.pem
nginx/ssl/*.key
nginx/ssl/*.crt
# Code Signing Certificates (NEVER commit these!)
*.pfx
*.p12
*.spc
*.pvk
desktop/certs/
desktop/*.pfx
desktop/assets/icon.iconset/
desktop/*.p12
desktop/*.spc
desktop/*.pvk
# Docker Compose overrides (do not ignore auto files, only manually generated)
# docker-compose.https.yml is now tracked
# Environment backups
.env.backup
# Node.js / Frontend build
node_modules/
package-lock.json
!desktop/package-lock.json
# Tailwind CSS build output (keep source in git)
app/static/dist/
/logs
logs/app.jsonl
# Internal / code-generation only (do not commit to public repo)
docs/internal/
scripts/generate_donate_hide_code.py
# Mobile
mobile/android/app/build/
mobile/android/.gradle/
mobile/ios/build/
mobile/ios/Pods/
mobile/ios/Podfile.lock
mobile/ios/Podfile.lock.lock
mobile/ios/Podfile.lock.lock.lock
mobile/ios/Podfile.lock.lock.lock.lock
mobile/ios/Podfile.lock.lock.lock.lock.lock
mobile/ios/Podfile.lock.lock.lock.lock.lock.lock
mobile/.dart_tool/
mobile/.android/
mobile/.ios/
mobile/.flutter-plugins-dependencies
mobile/.flutter-plugins
mobile/.flutter-plugins-dependencies
mobile/.flutter-plugins-dependencies.lock
mobile/.flutter-plugins-dependencies.lock.lock
mobile/.flutter-plugins-dependencies.lock.lock.lock
mobile/.flutter-plugins-dependencies.lock.lock.lock.lock
mobile/.flutter-plugins-dependencies.lock.lock.lock.lock.lock
mobile/.flutter-plugins-dependencies.lock.lock.lock.lock.lock.lock
mobile/.flutter-plugins-dependencies.lock.lock.lock.lock.lock.lock.lock
mobile/.flutter-plugins-dependencies.lock.lock.lock.lock.lock.lock.lock.lock
mobile/.flutter-plugins-dependencies.lock.lock.lock.lock.lock.lock.lock.lock.lock
mobile/.flutter-plugins-dependencies.lock.lock.lock.lock.lock.lock.lock.lock.lock.lock
mobile/.flutter-plugins-dependencies.lock.lock.lock.lock.lock.lock.lock.lock.lock.lock.lock
## License Files
donate_hide_private.pem
================================================
FILE: .pre-commit-config.yaml
================================================
# Pre-commit hooks configuration for TimeTracker
# Install with: pre-commit install
# Run manually: pre-commit run --all-files
repos:
# General file checks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- id: check-added-large-files
args: ['--maxkb=1000']
- id: check-merge-conflict
- id: check-case-conflict
- id: detect-private-key
- id: mixed-line-ending
args: ['--fix=lf']
# Python code formatting
- repo: https://github.com/psf/black
rev: 24.8.0
hooks:
- id: black
language_version: python3.11
args: [--line-length=120]
# Python import sorting
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
args: [--profile=black, --line-length=120]
# Python linting
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
args: [
--max-line-length=120,
--extend-ignore=E203,E501,W503,
--exclude=migrations,
]
additional_dependencies: [
flake8-docstrings,
flake8-bugbear,
]
# Security checks
- repo: https://github.com/PyCQA/bandit
rev: 1.7.6
hooks:
- id: bandit
args: [-r, app/, -ll]
pass_filenames: false
# Markdown linting
- repo: https://github.com/markdownlint/markdownlint
rev: v0.12.0
hooks:
- id: markdownlint
args: [--ignore, node_modules]
# YAML formatting
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
rev: v2.11.0
hooks:
- id: pretty-format-yaml
args: [--autofix, --indent, '2']
# Optionally, add local hooks for custom checks
- repo: local
hooks:
- id: smoke-tests
name: Run smoke tests
entry: pytest
args: [-m, smoke, --tb=short, -q]
language: system
pass_filenames: false
stages: [commit]
always_run: false # Set to true to always run smoke tests on commit
# Configuration
default_language_version:
python: python3.11
# Skip certain files/directories
exclude: |
(?x)^(
migrations/|
docs/|
node_modules/|
.venv/|
venv/|
\.git/|
\.pytest_cache/|
__pycache__/
)$
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to TimeTracker will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [5.5.2] - 2026-04-30
### Fixed
- **Quote edit redirect for delegated editors** — Users with `edit_quotes` permission could save changes on draft quotes they did not create but were redirected to an empty/“not found” flow because quote detail/list visibility was still filtered by `created_by`. Quote list/detail scope now matches edit capability for users with `edit_quotes` across web and API quote reads. Added a regression test for edit-then-redirect view loading and updated quote comment edit context links.
## [5.5.0] - 2026-04-27
### Added
- **LDAP authentication** — Optional directory login via `AUTH_METHOD=ldap` or combined `AUTH_METHOD=all` (with local + OIDC). New `LDAP_*` settings in `app/config.py`, `LDAPService` (`app/services/ldap_service.py`), login and password-reset behaviour keyed off `users.auth_provider` (`local` | `oidc` | `ldap`), admin **System Settings** LDAP panel and `POST /admin/ldap/test`, production env validation for required LDAP variables, Alembic `153_add_user_auth_provider`, and tests in `tests/test_ldap_auth.py`. Dependency: `ldap3`. Documentation: [docs/admin/configuration/LDAP_SETUP.md](docs/admin/configuration/LDAP_SETUP.md); OIDC and getting-started guides updated for `ldap` / `all`.
### Fixed
- **Admin “Allow only one active timer per user” ignored at runtime** — Timer start and related flows always blocked a second running entry and never read `Settings.single_active_timer` from the database. Enforcement now uses `Settings.get_settings()` via `TimeTrackingService.can_start_timer` (web timer routes, REST v1, kiosk start, legacy session `POST /api/timer/resume`). `POST /api/v1/timer/start` returns **409** with `error_code: timer_already_running` when the setting is on and a timer is already running. `SINGLE_ACTIVE_TIMER` still seeds new installs only. Tests: `tests/test_single_active_timer_setting.py`.
- **API integration test for project tasks** — `tests/test_api_comprehensive.py` now matches `GET /api/projects/<id>/tasks`, which returns **all** tasks (including done and cancelled) for the time-entry UI.
- **Quote create returned HTTP 500 after save (#583)** — The quote was saved, but the redirect to the quote detail page crashed when **Valid until** was set: the template compared `valid_until` to `now()`, and `now` was never defined in the Jinja context. The expired badge now uses `Quote.is_expired` (same rule, app timezone). Regression coverage in `tests/test_routes/test_quotes_web.py` posts `valid_until` so the view path is exercised.
- **Desktop app navigation guard** — `will-navigate` no longer mis-classifies `file:` loads (opaque `"null"` origin) as external navigation. Allowed in-app protocols include `file:`, `about:`, and `devtools:`; `http:` / `https:` are still blocked from the embedded window.
- **Desktop offline UI (bundle)** — Shared helpers load before dependent modules; timesheet period and time-off request lists expose **Delete** where allowed (with `currentUserProfile.id` for ownership); approve/reject controls read approval state from `state.currentUserProfile`; API client includes `deleteTimesheetPeriod` and `deleteTimeOffRequest`.
### Added
- **Mobile bottom navigation (web)** — On viewports below the `md` breakpoint (768px), signed-in users get a fixed bottom bar with tabs for Dashboard, Timer, Time entries, Projects, and **More**. **More** opens a slide-up drawer (backdrop, close control, Escape) linking to Invoices, Clients, Reports, and **My Settings** (`user.settings`), respecting module enablement where applicable. Implementation: [`app/templates/partials/_bottom_nav.html`](app/templates/partials/_bottom_nav.html) included from [`app/templates/base.html`](app/templates/base.html); [`app/static/mobile.js`](app/static/mobile.js) drives the drawer. **Safe area:** `pb-safe` utility in [`app/static/src/input.css`](app/static/src/input.css) and safelist in [`tailwind.config.js`](tailwind.config.js). Main content uses `pb-16` on small screens so it is not covered by the bar. Layout breakpoint for sidebar visibility, main margin, mobile menu, and RTL `#mainContent` margin is aligned to `md` (768px).
- **Smart in-app notifications** — Opt-in under **Settings → Notifications → In-app reminders**: nudge when no time is logged today (configurable hour window, user timezone), alert when an active timer exceeds a configurable duration, and end-of-day summary of hours logged. Server-driven via `GET /api/notifications` and `POST /api/notifications/dismiss`; per-day dismissals stored in `user_smart_notification_dismissals`. Environment defaults: `SMART_NOTIFY_MAX_PER_DAY`, `SMART_NOTIFY_NO_TRACKING_AFTER`, `SMART_NOTIFY_SUMMARY_AT`, `SMART_NOTIFY_LONG_TIMER_HOURS`, `SMART_NOTIFY_SCHEDULER_SLOT_MINUTES` (see `app/config.py` and [docs/features/SMART_NOTIFICATIONS.md](docs/features/SMART_NOTIFICATIONS.md)). Migration `150_add_smart_notifications`. The dashboard client polls the API and shows toasts (optional browser notifications when enabled and permission granted). `toastManager.show` supports an optional `onDismiss` callback.
- **Value dashboard widget** — Dashboard productivity block backed by `StatsService` and `GET /api/stats/value-dashboard` (short-TTL Redis cache when available). Wired from `dashboard-enhancements.js` with the existing real-time dashboard refresh.
- **Quote line item reorder (Issue #584)** — Non-null `quote_items.position` (migration `146_add_quote_item_position`); `Quote.items` is ordered by `position`, then `id`. Create, edit, duplicate, bulk duplicate, API item payloads, and quote-template apply assign positions from the submitted row order. **Create quote** and **edit quote** forms include per-row **Move up** / **Move down** controls on **Quote line items**, **Costs**, and **Extra goods** so rows can be reordered without deleting and re-entering data; PDFs and detail views follow the saved order. New translatable UI strings: **Order**, **Move up**, **Move down** (run `pybabel extract` / `update` per [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md)).
- **Offline queue replay** — Queued requests now store method, headers, and body in a replay-safe form (serializable for localStorage). POST/PUT requests replayed when back online send the same body and method. Legacy queue items (with `options` only) are still replayed via fallback.
- **Inventory API scopes** — New scopes `read:inventory` and `write:inventory` for inventory-only API access. Existing `read:projects` and `write:projects` still grant the same inventory access for backward compatibility.
- **Client portal reports: date range and CSV export** — Reports support optional `days` query param (1–365, default 30). Add `?format=csv` to download a CSV of the same report (summary, hours by project, time by date). Export uses the same access control as the reports page.
- **Jira webhook verification** — When a webhook secret is configured in the Jira integration (Connection Settings → Webhook Secret), incoming webhooks are verified using HMAC-SHA256 of the request body. Supported headers: `X-Hub-Signature-256`, `X-Atlassian-Webhook-Signature`, `X-Hub-Signature`. Requests with missing or invalid signature are rejected. If no secret is set, behavior is unchanged (all webhooks accepted).
- **Crowdin integration (maintainers)** — Root [`crowdin.yml`](crowdin.yml) maps `translations/en/LC_MESSAGES/messages.po` to per-locale `messages.po` paths (with `nb` → `no` for Norwegian). Manual [`.github/workflows/crowdin-sync.yml`](.github/workflows/crowdin-sync.yml) uploads sources and downloads translations when `CROWDIN_PROJECT_ID` and `CROWDIN_PERSONAL_TOKEN` are set. [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md) includes a Crowdin setup section; [docs/TRANSLATION_SYSTEM.md](docs/TRANSLATION_SYSTEM.md) and contributor docs cross-link it.
### Changed
- **Documentation (API)** — Documented session-auth `GET /api/stats/value-dashboard` (response fields, Redis TTL, rate resolution) in [`docs/api/REST_API.md`](docs/api/REST_API.md) and linked dashboard session JSON from [`docs/API.md`](docs/API.md).
- **API v1 search scoping** — Project, task, and client branches of token search use shared `apply_project_scope` and `apply_client_scope` query helpers in [`app/utils/scope_filter.py`](app/utils/scope_filter.py) for consistent subcontractor restrictions.
- **Documentation (translations)** — Added [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md) for contributors without Git (issue template, optional spreadsheet or hosted platform, maintainer workflow). Root [CONTRIBUTING.md](CONTRIBUTING.md) links to it; [docs/TRANSLATION_SYSTEM.md](docs/TRANSLATION_SYSTEM.md) defers the enabled locale list to `app/config.py` (`LANGUAGES`) and points translators at the new guide.
- **Factur-X / PDF/A-3 invoice PDFs (export and email)** — Download and email attachments use the same embed-and-normalize path. Embedded CII uses Associated File relationship **Data** and MIME **text/xml**. PDF/A-3 normalization embeds sRGB via `app/resources/icc/` (override with `INVOICE_SRGB_ICC_PATH`). Added `app/utils/invoice_pdf_postprocess.py` and tests; [PEPPOL e-Invoicing](docs/admin/configuration/PEPPOL_EINVOICING.md) updated (veraPDF note, pytest command).
- **Documentation sync** — CODEBASE_AUDIT.md: marked gaps 2.3–2.7 and 2.9 as fixed; added “Implemented 2026-03-16” summary. CLIENT_FEATURES_IMPLEMENTATION_STATUS: report date range and CSV export noted as implemented. INCOMPLETE_IMPLEMENTATIONS_ANALYSIS: added “Verified 2026-03-16” for webhook verification, issues permissions, search API, offline queue.
- **Activity feed API date params** — `/api/activity` now returns 400 with a clear message when `start_date` or `end_date` are invalid (e.g. not ISO 8601). Invalid dates on the web route `/activity` are logged and the filter is skipped (no 500).
- **Invoice PEPPOL compliance check** — Exceptions in the PEPPOL compliance block are no longer silently ignored: specific and generic exceptions are caught, logged, and a generic warning (“Could not verify PEPPOL compliance; check configuration.”) is shown to the user so the view still renders.
- **Documentation and i18n audit** — Updated docs and translations to match current implementation: removed stale "coming soon" claims; marked INCOMPLETE_IMPLEMENTATIONS_ANALYSIS as historical and added still-relevant summary; rewrote INVENTORY_MISSING_FEATURES as "Remaining Gaps" (transfers, adjustments, reports, PO management, API are implemented); updated GETTING_STARTED (PDF export, project permissions, REST API); REST_API (webhooks supported); KEYBOARD_SHORTCUTS_SUMMARY (customization implemented); BULK_TASK_OPERATIONS (bulk due date/priority implemented); INVENTORY_IMPLEMENTATION_STATUS (report templates done); activity_feed (invoices/clients/comments status clarified). Removed orphaned translation strings "Bulk due date update feature coming soon!" and "Bulk priority update feature coming soon!" from 10 locale `.po` files.
### Added
- **Mileage and Per Diem export and filter (Issue #564)** — Mileage and Per Diem now support CSV and PDF export using the same filter set as the list view, matching Time Entries behavior. **Mileage**: Export CSV and Export PDF buttons in the filter card; exports use current filters (search, status, project, client, date range). Routes: `GET /mileage/export/csv`, `GET /mileage/export/pdf`. PDF report via [app/utils/mileage_pdf.py](app/utils/mileage_pdf.py) (ReportLab, landscape A4, totals row). **Per diem**: Client filter added to the list form (with client-lock/single-client handling); Export CSV and Export PDF buttons; routes `GET /per-diem/export/csv`, `GET /per-diem/export/pdf`. PDF via [app/utils/per_diem_pdf.py](app/utils/per_diem_pdf.py). Export links are built from the current filter form (JS), so applied filters apply to both the list and the downloaded file.
- **Break time for timers and manual time entries (Issue #561)** — Pause/resume running timers so time while paused counts as break; on stop, stored duration = (end − start) − break (with rounding). Manual time entries and edit form have an optional **Break** field (HH:MM); effective duration is (end − start) − break. Optional default break rules in Settings (e.g. >6 h → 30 min, >9 h → 45 min) power a **Suggest** button on the manual entry form; users can override. New columns: `time_entries.break_seconds`, `time_entries.paused_at`; Settings: `break_after_hours_1`, `break_minutes_1`, `break_after_hours_2`, `break_minutes_2`. API: `POST /api/v1/timer/pause`, `POST /api/v1/timer/resume`; timer status and time entry create/update accept and return `break_seconds`. See [docs/BREAK_TIME_FEATURE.md](docs/BREAK_TIME_FEATURE.md).
- **Architecture refactor** — API v1 split into per-resource sub-blueprints (projects, tasks, clients, invoices, expenses, payments, mileage, deals, leads, contacts) under `app/routes/api_v1_*.py`; bootstrap slimmed by moving `setup_logging` to `app/utils/setup_logging.py` and legacy migrations to `app/utils/legacy_migrations.py`. Dashboard aggregations (top projects, time-by-project chart) moved into `AnalyticsService` (`get_dashboard_top_projects`, `get_time_by_project_chart`); dashboard route simplified to call services only. ARCHITECTURE.md updated with module table, API structure, and data flow; DEVELOPMENT.md with development workflow and build steps.
### Fixed
- **Xero integration for apps created after March 2026 (Issue #567)** — OAuth no longer fails with "Invalid scope for client" for Xero Developer apps created on or after March 2, 2026. Replaced deprecated `accounting.transactions` scope with granular `accounting.invoices` and `accounting.payments`. Expense sync now uses the correct `/api.xro/2.0/ExpenseClaims` endpoint (replacing the non-existent `/api.xro/2.0/Expenses`) and reads `ExpenseClaimID` from the response. `_api_request` now accepts an optional request body so invoice and expense payloads are sent to the Xero API. See [docs/integrations/XERO.md](docs/integrations/XERO.md).
- **Time Entries date filter and export (Issue #555)** — Start/End date filters were hard to discover and exports ignored them. The Time Entries overview now has a visible **Apply filters** button in the filter header (next to Clear Filters and Export) so users can apply date and other filters without scrolling. CSV and PDF export links always use the current filter parameters: export href is set from the page URL on load and updated whenever filter form values change, so left-click export, right-click "Open in new tab", and "Save link as" all produce filtered exports. The in-form Apply filters button and the header button both trigger the same filter logic; clicking the header button expands the filter panel if it is collapsed.
- **Log Time / Edit Time Entry on mobile (Issue #557)** — Opening the manual time entry ("Log Time") or edit time entry page on mobile could freeze or crash the browser. The Toast UI Editor (WYSIWYG markdown editor) for the notes field is heavy and causes freezes on mobile Safari/Chrome. On viewports ≤767px we now skip loading the editor and show a plain textarea for notes instead; desktop behavior is unchanged. Manual entry and edit timer templates load Toast UI only when not in mobile view.
- **Stop & Save error (Issue #563)** — Fixed error after clicking "Stop & Save" on the dashboard. The post-timer toast was building the "View time entries" URL with the wrong route name (`timer.time_entries`); the correct endpoint is `timer.time_entries_overview`. Time entries were already saved; the error occurred when rendering the dashboard redirect.
- **Dashboard cache (Issue #549)** — Removed dashboard caching that caused "Instance not bound to a Session" and "Database Error" on second visit. Cached template data contained ORM objects (active_timer, recent_entries, top_projects, templates, etc.) that become detached when served in a different request.
- **Task description field (Issue #535)** — When creating or editing a task, the description field could appear missing or broken if the Toast UI Editor (loaded from CDN) failed to load (e.g. reverse proxy, CSP, Firefox, or offline). A fallback now shows a plain textarea so users can always enter a description; Markdown is still supported when the rich editor loads.
- **ZUGFeRD / PDF/A-3 and PEPPOL (Discussion #433)** — ZUGFeRD embedding no longer silently succeeds without XML when the embed step fails; export is aborted with an actionable error. XMP metadata is created when missing so validators recognize the document. Optional PDF/A-3 normalization (XMP identification and output intent) and optional veraPDF validation gate added. Native PEPPOL transport (SML/SMP + AS4) and strict sender/recipient identifier validation added.
### Added
- **Dashboard time-by-project chart** — "Time by project (last 7 days)" horizontal bar chart on the dashboard (Chart.js); link to Summary report.
- **Summary report charts** — Time-by-project (last 30 days) bar chart and daily trend (last 14 days) line chart on the Summary report page.
- **Summary report PDF export** — New route `/reports/summary/export/pdf`; one-page PDF with today/week/month hours and top projects table ([app/utils/summary_report_pdf.py](app/utils/summary_report_pdf.py)).
- **Post-timer toast** — After stopping the timer, a success toast shows "Logged Xh on [Project]" with an action link "View time entries"; toast manager supports optional `actionLink` and `actionLabel`.
- **Remind to log** — User setting "Remind me to log time at end of day" with time picker (Settings); scheduled task runs hourly and sends one email per day to users who have the reminder enabled and have logged < 0.5h that day (in their timezone). Migration `135_add_remind_to_log_settings` adds `notification_remind_to_log` and `reminder_to_log_time` to users.
- **Migration merge 133** — Merge heads 132 (timesheet governance) and 129 (task tags) so `flask db upgrade` runs without conflicts.
- **PEPPOL native transport** — Transport mode can be set to **Native** (SML/SMP participant discovery + AS4 send) in addition to **Generic** (HTTP JSON access point). Sender and recipient identifiers are validated before send. New settings: `peppol_transport_mode`, `peppol_sml_url`, `peppol_native_cert_path`, `peppol_native_key_path` (Admin → Peppol e-Invoicing).
- **PDF/A-3 and validation** — Option **Normalize ZUGFeRD PDFs to PDF/A-3** and optional **Run veraPDF after export** with configurable path. Migration `130_add_peppol_transport_mode_and_native` adds the new columns.
- **Dashboard timer widget** — Pause and Stop buttons while a timer is running (Pause saves the segment so you can resume later). When no timer is active, a prominent "Resume (project name)" button restarts tracking with the same project/task/notes as your last entry. Quick time adjustment buttons (−15 / −5 / +5 / +15 minutes) let you correct the current session without leaving the dashboard. New route `POST /timer/adjust` for start-time adjustment.
### Changed
- **UI/UX redesign** — Consolidated component system: single `page_header`, `empty_state` / `empty_state_compact`, and `loading_overlay` in `components/ui.html`; migrated overdue tasks page from Bootstrap to Tailwind; added form error and disabled states in design tokens. Base layout: main content max-width (1280px) and centered; first-class **Timer** and **Time entries** in sidebar; reduced nav label weight. Timer flow: single adjust-time form with one submit; dashboard hero is the Timer card (start/stop, quick start, repeat last); post-stop toast with “View time entries” unchanged. Dashboard: Timer as hero block first, then Today/Week/Month stats, then Recent entries (last 5, columns Project/Duration/Date/Actions) with “View all” link to Time entries overview. Empty and loading states use shared macros; toasts used for errors and success. New [UI Guidelines](docs/UI_GUIDELINES.md); README and ARCHITECTURE updated with UI overview and UI layer section.
- **Dashboard** — Weekly goal widget already showed progress bar; added time-by-project (7d) chart and chart data from main route.
- **Summary report** — Added Chart.js time-by-project and daily-trend charts; added Export PDF button; backend passes chart and trend data from AnalyticsService.
- **Toast notifications** — Optional `actionLink` and `actionLabel` in toast manager for action links in toasts.
- **Documentation** — README updated with new features (dashboard chart, summary charts/PDF, post-timer toast, remind to log); daily workflow note in Screenshots section.
- **Log Time Manually page** — Redesigned for a more professional layout: form grouped into sections (Project & task, Date & time, Details) with clear headings and icons; main card uses rounded-xl and shadow-lg; unified label and helper text styling; primary "Log Time" and secondary "Clear" buttons aligned with dashboard button styles; duplicate-entry banner uses rounded-xl.
## [4.20.6] - 2025-02-20
### Changed
- **Version Update** — Updated to version 4.20.6.
## [4.20.5] - 2025-02-17
### Changed
- **Version Update** — Updated to version 4.20.5.
## [4.20.0] - 2025-02-16
### Fixed
- **PDF layout: decorative image persistence and PDF preview (Issue #432)** — Decorative images now survive save/load: image URLs are synced onto groups before generating the template, injected into the saved design JSON using position-based matching, and restored from the saved JSON onto the canvas on load. Empty decorative image elements are no longer added to the ReportLab template, and the PDF generator skips empty or invalid image sources and validates base64 data URIs, preventing a mostly-black or broken PDF preview.
- **Header Start Timer button** — Fixed manual entry URL (`/timer/manual_entry` → `/timer/manual`); timer now correctly opens manual entry when starting from the header button.
### Added
- **Header quick access buttons** — Chat, Timer, and Help are grouped in the header as round icon buttons, vertically aligned and evenly spaced. One-click timer start/stop from any page; Help links to documentation; Chat opens team chat when enabled.
- **ZugFerd / Factur-X support for invoice PDFs** — When enabled in Admin → Settings → Peppol e-Invoicing, exported invoice PDFs embed EN 16931 UBL XML as `ZUGFeRD-invoice.xml`, producing hybrid human- and machine-readable invoices. Uses the same UBL as Peppol; these PDFs can be sent via Peppol or email. New setting `invoices_zugferd_pdf`, migration `128_add_invoices_zugferd_pdf`, dependency `pikepdf`, and [docs/admin/configuration/PEPPOL_EINVOICING.md](docs/admin/configuration/PEPPOL_EINVOICING.md) updated for both Peppol and ZugFerd.
- **Subcontractor role and assigned clients** — Users with the Subcontractor role can be restricted to specific clients and their projects. Admins assign clients in Admin → Users → Edit user (section "Assigned Clients (Subcontractor)"). Scope is applied to clients, projects, time entries, reports, invoices, timer, and API v1; direct access to other clients/projects returns 403. New table `user_clients`, migration `127_add_user_clients_table`, and docs in [docs/SUBCONTRACTOR_ROLE.md](docs/SUBCONTRACTOR_ROLE.md).
### Changed
- **Version Update** — Updated to version 4.20.0.
## [4.19.0] - 2025-02-13
### Added
- **REST API v1** - CRM and time approvals: `/api/v1/deals`, `/api/v1/leads`, `/api/v1/clients/<id>/contacts`, `/api/v1/contacts/<id>`, `/api/v1/time-entry-approvals` (list, get, approve, reject, cancel, request-approval, bulk-approve). New API token scopes: `read:deals`, `write:deals`, `read:leads`, `write:leads`, `read:contacts`, `write:contacts`, `read:time_approvals`, `write:time_approvals`.
- **Documentation** - Service layer and BaseCRUD pattern ([docs/development/SERVICE_LAYER_AND_BASE_CRUD.md](docs/development/SERVICE_LAYER_AND_BASE_CRUD.md)); RBAC permission model ([docs/development/RBAC_PERMISSION_MODEL.md](docs/development/RBAC_PERMISSION_MODEL.md)).
### Changed
- **API responses** - Projects and new CRM/approvals API v1 routes use standardized `error_response` / `forbidden_response` / `not_found_response` from `app.utils.api_responses`.
- **Templates** - All templates consolidated under `app/templates/`; root `templates/` removed and extra Jinja loader removed.
- **Version** - README, FEATURES_COMPLETE.md, and docs reference `setup.py` as single source of truth for version (4.19.0).
- **Refactored examples** - `projects_refactored_example.py`, `timer_refactored.py`, `invoices_refactored.py` marked as reference-only in module docstrings.
## [4.14.0] - 2025-01-27
### Changed
- **Version Update** - Updated to version 4.14.0
- **Documentation** - Comprehensive README and documentation updates for clarity and completeness
- **Technology Stack** - Added complete technology stack overview to README
- **Quick Start** - Enhanced with prerequisites, clearer instructions, and troubleshooting links
- **System Requirements** - Added detailed system requirements section
- **Documentation Organization** - Improved organization by use case and user type
### Fixed
- **Version Consistency** - Fixed version inconsistencies across all documentation files
- **Documentation Links** - Fixed broken links and improved navigation
- **Feature Documentation** - Added comprehensive links to feature guides throughout README
## [4.13.2] - 2025-01-27
### Changed
- **Version Update** - Updated to version 4.13.2
- **Documentation** - Comprehensive README and documentation updates for clarity and completeness
### Fixed
- **Version Consistency** - Fixed version inconsistencies across all documentation files
## [4.8.8] - 2025-01-27
### Changed
- **Version Update** - Updated to version 4.8.8
- **Documentation** - Comprehensive project analysis and documentation updates
### Fixed
- **Version Consistency** - Fixed version inconsistencies across documentation files
## [4.6.0] - 2025-12-14
### Added
- **Comprehensive Issue/Bug Tracking System** - Complete issue and bug tracking functionality with full lifecycle management
## [4.5.1] - 2025-12-13
### Changed
- **Performance Optimization** - Optimized task listing queries and improved version management
- **Version Management** - Enhanced version management system
## [4.5.0] - 2025-12-12
### Added
- **Advanced Report Builder** - Iterative report generation with email distribution capabilities
- **Quick Task Creation** - Create tasks directly from the Start Timer modal for faster workflow
- **Kanban Board Enhancements** - Added user filter and flexible column layout options
- **PWA Install UI** - Improved Progressive Web App installation user interface
### Fixed
- **Permission and Role Management** - Fixed bugs in permission and role management system
### Changed
- **Error Handling** - Improved error handling throughout the application
- **Performance Logging** - Enhanced performance logging and monitoring
## [4.4.1] - 2025-12-08
### Added
- **Custom Reports Enhancement** - Enhanced custom reports and scheduled reports functionality
### Fixed
- **Dashboard Cache Invalidation** - Fixed dashboard cache invalidation when editing timer entries (#342)
- **Custom Field Definitions** - Fixed graceful handling of missing custom_field_definitions table (#344)
## [4.4.0] - 2025-12-03
### Added
- **Project Custom Fields** - Add custom fields to projects for enhanced project tracking
- **File Attachments** - File attachment support for projects and clients
- **Salesman-Based Report Splitting** - Report splitting and email distribution based on salesperson assignments
### Changed
- **Performance Optimization** - Optimized task queries and fixed N+1 performance issues
- **Version Update** - Updated setup.py version to 4.4.0
## [4.3.2] - 2025-12-02
### Added
- **Custom Field Filtering** - Custom field filtering and display for clients, projects, and time entries
- **Client Count Tracking** - Client count tracking and cleanup for custom field definitions
- **Unpaid Hours Report** - New unpaid hours report with Ajax filtering and Excel export
- **Time Entries Overview** - New time entries overview page with AJAX filters and bulk mark as paid
- **Configurable Duplicate Detection** - Configurable duplicate detection fields for CSV client import
- **Enhanced Audit Logging** - Improved error handling and diagnostic tools for audit logging
### Changed
- **Offline Sync** - Enhanced offline sync functionality and performance improvements
- **Error Handling** - Improved error handling throughout the application
- **Docker Healthchecks** - Enhanced Docker healthcheck functionality
## [4.3.1] - 2025-12-01
### Changed
- **Offline Sync** - Enhanced offline sync functionality and performance improvements
## [4.3.0] - 2025-12-01
### Added
- **Custom Field Filtering** - Custom field filtering and display for clients, projects, and time entries
- **Client Count Tracking** - Client count tracking and cleanup for custom field definitions
- **Unpaid Hours Report** - New unpaid hours report with Ajax filtering and Excel export
- **Time Entries Overview** - New time entries overview page with AJAX filters and bulk mark as paid
- **Configurable Duplicate Detection** - Configurable duplicate detection fields for CSV client import
- **Enhanced Audit Logging** - Improved error handling and diagnostic tools for audit logging
### Changed
- **Error Handling** - Improved error handling throughout the application
- **Docker Healthchecks** - Enhanced Docker healthcheck functionality
- **Offline Sync** - Enhanced offline sync functionality
## [4.2.1] - 2025-12-01
### Fixed
- **AUTH_METHOD=none** - Fixed authentication method when set to none
- **Schema Verification** - Added comprehensive schema verification
## [4.2.0] - 2025-11-30
### Added
- **CSV Import/Export** - CSV import/export for clients with custom fields and contacts
- **Global Custom Field Definitions** - Global custom field definitions with link template support
- **Paid Status Tracking** - Paid status tracking for time entries with invoice reference
- **OAuth Credentials Dropdown** - Converted OAuth credentials section to dropdown in System Settings
---
## Release notes format
This changelog follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Section headings used:
- **Added** — New features
- **Changed** — Changes in existing functionality
- **Deprecated** — Soon-to-be removed features
- **Removed** — Removed features
- **Fixed** — Bug fixes
- **Security** — Security-related changes
For release artifacts and tags, see [GitHub Releases](https://github.com/drytrix/TimeTracker/releases).
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to TimeTracker
Thank you for your interest in contributing to TimeTracker. This page gives you a quick overview; full guidelines are in the developer documentation.
## How to Contribute
- **Report bugs** — Use the [GitHub issue tracker](https://github.com/drytrix/TimeTracker/issues). Include steps to reproduce, expected vs actual behavior, and your environment (OS, deployment method, version).
- **Improve translations (no Git)** — Use the **Translation improvement** issue template, translate on **[Crowdin (Drytrix TimeTracker)](https://crowdin.com/project/drytrix-timetracker)**, or read [docs/CONTRIBUTING_TRANSLATIONS.md](docs/CONTRIBUTING_TRANSLATIONS.md) for spreadsheet, maintainer workflow, and Crowdin setup ([`crowdin.yml`](crowdin.yml), **Actions → Crowdin sync**).
- **Suggest features** — Open a [feature request](https://github.com/drytrix/TimeTracker/issues/new?template=feature_request.md) with a clear description and use case.
- **Submit code** — Fork the repo, create a branch, make your changes, add tests, and open a pull request. Follow the [full contributing guidelines](docs/development/CONTRIBUTING.md) for setup, coding standards, and PR process.
## Full Guidelines
For development setup, coding standards, testing, pull request process, and commit conventions, see:
- **[Contributor Guide](docs/development/CONTRIBUTOR_GUIDE.md)** — Architecture, local dev, testing, how to add routes/services/templates, versioning
- **[Contributing guidelines (full)](docs/development/CONTRIBUTING.md)** — Development setup, coding standards, testing, PR process
- **[Code of Conduct](docs/development/CODE_OF_CONDUCT.md)** — Community standards and expected behavior
- **[CHANGELOG.md](CHANGELOG.md)** — How we track changes; update the *Unreleased* section for user-facing changes
## License
By contributing, you agree that your contributions will be licensed under the [GNU General Public License v3.0](LICENSE) used by this project.
================================================
FILE: Dockerfile
================================================
# syntax=docker/dockerfile:1.4
# --- Stage 1: Frontend Build ---
FROM node:18-slim as frontend
WORKDIR /app
COPY package*.json ./
RUN npm install
# Copy files needed for Tailwind build
COPY tailwind.config.js ./
COPY postcss.config.js ./
COPY app/static/src ./app/static/src
COPY app/templates ./app/templates
# Create dist directory for output
RUN mkdir -p app/static/dist
# Run the build (creates app/static/dist/output.css)
RUN npm run build:docker
# --- Stage 2: Python Application ---
FROM python:3.11-slim-bullseye
# Build-time version argument with safe default
ARG APP_VERSION=dev-0
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV FLASK_APP=app
ENV FLASK_ENV=production
ENV APP_VERSION=${APP_VERSION}
ENV TZ=Europe/Rome
# Support visibility: if donate_hide_public.pem is in project root it is copied to /app; set path so app finds it (override in compose if needed)
ENV DONATE_HIDE_PUBLIC_KEY_FILE=/app/donate_hide_public.pem
LABEL org.opencontainers.image.description="Self-hosted time tracking web application for projects, clients, and reports."
# Install all system dependencies in a single layer
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends \
# Core utilities
curl \
tzdata \
bash \
dos2unix \
gosu \
# Network tools for debugging
iproute2 \
net-tools \
iputils-ping \
dnsutils \
# WeasyPrint dependencies
libgdk-pixbuf2.0-0 \
libpango-1.0-0 \
libcairo2 \
libpangocairo-1.0-0 \
libffi-dev \
shared-mime-info \
# Fonts
fonts-liberation \
fonts-dejavu-core \
# PostgreSQL client dependencies
gnupg \
wget \
lsb-release \
&& sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' \
&& wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
&& apt-get update \
&& apt-get install -y --no-install-recommends postgresql-client-16 \
&& rm -rf /var/lib/apt/lists/*
# Set work directory
WORKDIR /app
# Install Python dependencies with cache mount for pip
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
# Create non-root user early (before copying files)
RUN useradd -m -u 1000 timetracker
# Create all directories before copying files to ensure proper structure
RUN mkdir -p \
/app/translations \
/data \
/data/uploads \
/app/logs \
/app/instance \
/app/app/static/uploads/logos \
/app/static/uploads/logos \
/app/app/static/dist
# Copy project files with correct ownership (includes optional donate_hide_public.pem from root when present)
COPY --chown=timetracker:timetracker . .
# Also install certificate generation script to a stable path used by docs/compose
COPY --chown=timetracker:timetracker scripts/generate-certs.sh /scripts/generate-certs.sh
# Set permissions on directories and ensure static files are readable
RUN chmod -R 775 /app/translations \
&& chmod 755 /data /data/uploads /app/logs /app/instance \
&& chmod -R 755 /app/app/static/uploads /app/static/uploads \
&& chmod 755 /app/app/static/dist \
&& chmod -R 755 /app/app/static
# Copy compiled assets from frontend stage (after general COPY to ensure it overwrites any local version)
COPY --chown=timetracker:timetracker --from=frontend /app/app/static/dist/output.css /app/app/static/dist/output.css
# Ensure the CSS file has correct permissions
RUN chmod 644 /app/app/static/dist/output.css
# Copy the startup script
COPY --chown=timetracker:timetracker docker/start-fixed.py /app/start.py
# Precompile translations at build time for faster startup (no runtime pybabel calls).
# If Babel isn't available for some reason, don't fail the image build.
RUN pybabel compile -d /app/translations >/dev/null 2>&1 || true
# Fix line endings and set permissions in a single layer
RUN find /app/docker -name "*.sh" -o -name "*.py" | xargs dos2unix 2>/dev/null || true \
&& dos2unix /app/start.py /scripts/generate-certs.sh 2>/dev/null || true \
&& chmod +x \
/app/start.py \
/app/docker/init-database.py \
/app/docker/init-database-sql.py \
/app/docker/init-database-enhanced.py \
/app/docker/verify-database.py \
/app/docker/test-db.py \
/app/docker/test-routing.py \
/app/docker/entrypoint.sh \
/app/docker/entrypoint_fixed.sh \
/app/docker/entrypoint_simple.sh \
/app/docker/entrypoint-local-test.sh \
/app/docker/entrypoint-local-test-simple.sh \
/app/docker/entrypoint.py \
/app/docker/startup_with_migration.py \
/app/docker/test_db_connection.py \
/app/docker/debug_startup.sh \
/app/docker/simple_test.sh \
/app/docker/seed-dev-data.sh \
/scripts/generate-certs.sh
# Set ownership only for directories that need write access
# (Most files already have correct ownership from COPY --chown)
RUN chown -R timetracker:timetracker \
/data \
/app/logs \
/app/instance \
/app/app/static/uploads \
/app/static/uploads \
/app/translations \
/scripts
USER timetracker
# Expose port
EXPOSE 8080
# Note: Health check is configured in docker-compose.yml
# This allows different healthcheck settings per environment
# Set the entrypoint
ENTRYPOINT ["/app/docker/entrypoint_fixed.sh"]
# Run the application
CMD ["python", "/app/start.py"]
================================================
FILE: INSTALLATION.md
================================================
# TimeTracker Installation
This guide walks you through installing and running TimeTracker. For a quick overview, see the [README Quick Start](README.md#-quick-start).
## Prerequisites
- **Docker** 20.10+ and **Docker Compose** 2.0+
- **Git**
- **2GB+ RAM** for Docker containers
- **Ports:** 80/443 (HTTPS) or 8080 (HTTP)
Install Docker for your platform: [Docker Installation Guide](https://docs.docker.com/get-docker/).
## Quick Install (Docker with HTTPS)
1. Clone the repository:
```bash
git clone https://github.com/drytrix/TimeTracker.git
cd TimeTracker
```
2. Create your environment file from the template:
```bash
cp env.example .env
```
3. Edit `.env` and set at least:
- **SECRET_KEY** — Required for sessions and CSRF. Generate one:
```bash
python -c "import secrets; print(secrets.token_hex(32))"
```
- **SETTINGS_ENCRYPTION_KEY** — Recommended to encrypt stored secrets (SMTP password, OAuth client secrets, Peppol token, AI key, and 2FA secrets). Generate one:
```bash
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
```
- **TZ** — Your timezone (e.g. `America/New_York`, `Europe/Brussels`).
- **CURRENCY** — Default currency (e.g. `USD`, `EUR`).
4. Start the stack:
```bash
docker compose up -d
```
5. Open **https://localhost** in your browser. The first run may show a self-signed certificate warning; proceed to continue.
The **first user who logs in** is created as an admin (or use `ADMIN_USERNAMES` in `.env` to predefine admin usernames).
## First Login and Minimal Config
- Log in with the username you configured (e.g. from `ADMIN_USERNAMES`) or the first account you create.
- In **Admin → Settings** you can adjust timezone, currency, and other options.
- See [Getting Started](docs/GETTING_STARTED.md) for initial setup and core workflows.
## Alternative: SQLite Quick Test
To try TimeTracker without PostgreSQL:
```bash
git clone https://github.com/drytrix/TimeTracker.git
cd TimeTracker
docker-compose -f docker/docker-compose.local-test.yml up --build
```
Then open **http://localhost:8080**. No `.env` is required for this compose file. SQLite is for evaluation only; use PostgreSQL for production.
## Production Deployment
For production:
- Use a strong **SECRET_KEY** and keep `.env` out of version control.
- Prefer **PostgreSQL** (included in the default Docker Compose setup).
- Put the app behind HTTPS (reverse proxy or Docker with HTTPS compose).
> Note: The default `docker-compose.yml` requires `SECRET_KEY` to be set (32+ chars). If it is missing, `docker compose` will error during interpolation.
Detailed steps and options:
- [Docker Compose Setup](docs/admin/configuration/DOCKER_COMPOSE_SETUP.md) — Full configuration and env reference
- [Docker Public Setup](docs/admin/configuration/DOCKER_PUBLIC_SETUP.md) — Production deployment with published images
## Troubleshooting
| Problem | Documentation |
|--------|----------------|
| Docker won’t start | [Docker Startup Troubleshooting](docs/admin/configuration/DOCKER_STARTUP_TROUBLESHOOTING.md) |
| Database connection errors | [Database Connection Troubleshooting](docker/TROUBLESHOOTING_DB_CONNECTION.md) |
| CSRF or session errors | [CSRF Troubleshooting](docs/admin/security/CSRF_TROUBLESHOOTING.md) |
| Port already in use | Change ports in your `docker-compose` file or stop the conflicting service |
For more help, see the [Documentation Index](docs/README.md) and [Support](README.md#-support).
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers who use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a way requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a "work based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work for
making modifications to it. "Object code" means any non-source form of a
work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is designed to require, such as by
intimate data communication or control flow between those subprograms
and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of
gitextract__fbo_k07/
├── .bandit
├── .coveragerc
├── .editorconfig
├── .flake8
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ └── translation_fix.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── workflows/
│ │ ├── build-desktop.yml
│ │ ├── build-mobile.yml
│ │ ├── cd-development.yml
│ │ ├── cd-release.yml
│ │ ├── ci-comprehensive.yml
│ │ ├── ci.yml
│ │ ├── crowdin-sync.yml
│ │ ├── migration-check.yml
│ │ └── static.yml
│ └── workflows-archive/
│ ├── ci.yml.backup
│ └── docker-publish.yml.backup
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── INSTALLATION.md
├── LICENSE
├── Makefile
├── README.md
├── app/
│ ├── __init__.py
│ ├── blueprint_registry.py
│ ├── config/
│ │ ├── __init__.py
│ │ ├── analytics_defaults.py
│ │ └── support_ui.py
│ ├── config.py
│ ├── constants.py
│ ├── integrations/
│ │ ├── __init__.py
│ │ ├── activitywatch.py
│ │ ├── asana.py
│ │ ├── base.py
│ │ ├── caldav_calendar.py
│ │ ├── github.py
│ │ ├── gitlab.py
│ │ ├── google_calendar.py
│ │ ├── jira.py
│ │ ├── linear.py
│ │ ├── microsoft_teams.py
│ │ ├── outlook_calendar.py
│ │ ├── peppol.py
│ │ ├── peppol_as4.py
│ │ ├── peppol_identifiers.py
│ │ ├── peppol_smp.py
│ │ ├── peppol_transport.py
│ │ ├── quickbooks.py
│ │ ├── registry.py
│ │ ├── slack.py
│ │ ├── trello.py
│ │ └── xero.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── activity.py
│ │ ├── api_idempotency_key.py
│ │ ├── api_token.py
│ │ ├── audit_log.py
│ │ ├── budget_alert.py
│ │ ├── calendar_event.py
│ │ ├── calendar_integration.py
│ │ ├── client.py
│ │ ├── client_attachment.py
│ │ ├── client_note.py
│ │ ├── client_notification.py
│ │ ├── client_portal_customization.py
│ │ ├── client_portal_dashboard_preference.py
│ │ ├── client_prepaid_consumption.py
│ │ ├── client_time_approval.py
│ │ ├── comment.py
│ │ ├── comment_attachment.py
│ │ ├── contact.py
│ │ ├── contact_communication.py
│ │ ├── currency.py
│ │ ├── custom_field_definition.py
│ │ ├── custom_report.py
│ │ ├── deal.py
│ │ ├── deal_activity.py
│ │ ├── donation_interaction.py
│ │ ├── expense.py
│ │ ├── expense_category.py
│ │ ├── expense_gps.py
│ │ ├── extra_good.py
│ │ ├── focus_session.py
│ │ ├── gamification.py
│ │ ├── import_export.py
│ │ ├── integration.py
│ │ ├── integration_external_event_link.py
│ │ ├── invoice.py
│ │ ├── invoice_approval.py
│ │ ├── invoice_email.py
│ │ ├── invoice_image.py
│ │ ├── invoice_pdf_template.py
│ │ ├── invoice_peppol.py
│ │ ├── invoice_template.py
│ │ ├── issue.py
│ │ ├── kanban_column.py
│ │ ├── lead.py
│ │ ├── lead_activity.py
│ │ ├── link_template.py
│ │ ├── mileage.py
│ │ ├── payment_gateway.py
│ │ ├── payments.py
│ │ ├── per_diem.py
│ │ ├── permission.py
│ │ ├── project.py
│ │ ├── project_attachment.py
│ │ ├── project_cost.py
│ │ ├── project_stock_allocation.py
│ │ ├── project_template.py
│ │ ├── purchase_order.py
│ │ ├── push_subscription.py
│ │ ├── quote.py
│ │ ├── quote_attachment.py
│ │ ├── quote_image.py
│ │ ├── quote_template.py
│ │ ├── quote_version.py
│ │ ├── rate_override.py
│ │ ├── recurring_block.py
│ │ ├── recurring_invoice.py
│ │ ├── recurring_task.py
│ │ ├── reporting.py
│ │ ├── salesman_email_mapping.py
│ │ ├── saved_filter.py
│ │ ├── settings.py
│ │ ├── stock_item.py
│ │ ├── stock_lot.py
│ │ ├── stock_movement.py
│ │ ├── stock_reservation.py
│ │ ├── supplier.py
│ │ ├── supplier_stock_item.py
│ │ ├── task.py
│ │ ├── task_activity.py
│ │ ├── tax_rule.py
│ │ ├── team_chat.py
│ │ ├── time_entry.py
│ │ ├── time_entry_approval.py
│ │ ├── time_entry_template.py
│ │ ├── time_off.py
│ │ ├── timesheet_period.py
│ │ ├── timesheet_policy.py
│ │ ├── user.py
│ │ ├── user_client.py
│ │ ├── user_favorite_project.py
│ │ ├── user_smart_notification_dismissal.py
│ │ ├── warehouse.py
│ │ ├── warehouse_stock.py
│ │ ├── webhook.py
│ │ ├── weekly_time_goal.py
│ │ └── workflow.py
│ ├── repositories/
│ │ ├── __init__.py
│ │ ├── base_repository.py
│ │ ├── client_repository.py
│ │ ├── comment_repository.py
│ │ ├── expense_repository.py
│ │ ├── invoice_repository.py
│ │ ├── payment_repository.py
│ │ ├── project_repository.py
│ │ ├── recurring_invoice_repository.py
│ │ ├── task_repository.py
│ │ ├── time_entry_repository.py
│ │ └── user_repository.py
│ ├── resources/
│ │ └── icc/
│ │ ├── LICENSE.txt
│ │ └── sRGB-v2-nano.icc
│ ├── routes/
│ │ ├── activity_feed.py
│ │ ├── admin.py
│ │ ├── analytics.py
│ │ ├── api/
│ │ │ ├── __init__.py
│ │ │ └── v1/
│ │ │ └── __init__.py
│ │ ├── api.py
│ │ ├── api_docs.py
│ │ ├── api_v1.py
│ │ ├── api_v1_ai.py
│ │ ├── api_v1_clients.py
│ │ ├── api_v1_common.py
│ │ ├── api_v1_contacts.py
│ │ ├── api_v1_deals.py
│ │ ├── api_v1_expenses.py
│ │ ├── api_v1_invoices.py
│ │ ├── api_v1_leads.py
│ │ ├── api_v1_mileage.py
│ │ ├── api_v1_payments.py
│ │ ├── api_v1_projects.py
│ │ ├── api_v1_tasks.py
│ │ ├── api_v1_time_entries.py
│ │ ├── audit_logs.py
│ │ ├── auth.py
│ │ ├── budget_alerts.py
│ │ ├── calendar.py
│ │ ├── client_notes.py
│ │ ├── client_portal.py
│ │ ├── client_portal_customization.py
│ │ ├── clients.py
│ │ ├── comments.py
│ │ ├── contacts.py
│ │ ├── custom_field_definitions.py
│ │ ├── custom_reports.py
│ │ ├── deals.py
│ │ ├── expense_categories.py
│ │ ├── expenses.py
│ │ ├── gantt.py
│ │ ├── import_export.py
│ │ ├── integrations.py
│ │ ├── inventory.py
│ │ ├── invoice_approvals.py
│ │ ├── invoices.py
│ │ ├── invoices_refactored.py
│ │ ├── issues.py
│ │ ├── kanban.py
│ │ ├── kiosk.py
│ │ ├── leads.py
│ │ ├── link_templates.py
│ │ ├── main.py
│ │ ├── mileage.py
│ │ ├── offers.py
│ │ ├── payment_gateways.py
│ │ ├── payments.py
│ │ ├── per_diem.py
│ │ ├── permissions.py
│ │ ├── project_templates.py
│ │ ├── projects.py
│ │ ├── projects_refactored_example.py
│ │ ├── push_notifications.py
│ │ ├── quotes.py
│ │ ├── recurring_invoices.py
│ │ ├── recurring_tasks.py
│ │ ├── reports.py
│ │ ├── salesman_reports.py
│ │ ├── saved_filters.py
│ │ ├── scheduled_reports.py
│ │ ├── settings.py
│ │ ├── setup.py
│ │ ├── tasks.py
│ │ ├── team_chat.py
│ │ ├── time_approvals.py
│ │ ├── time_entry_templates.py
│ │ ├── timer.py
│ │ ├── timer_refactored.py
│ │ ├── user.py
│ │ ├── webhooks.py
│ │ ├── weekly_goals.py
│ │ ├── workflows.py
│ │ └── workforce.py
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── client_schema.py
│ │ ├── comment_schema.py
│ │ ├── expense_schema.py
│ │ ├── invoice_schema.py
│ │ ├── payment_schema.py
│ │ ├── project_schema.py
│ │ ├── task_schema.py
│ │ ├── time_entry_schema.py
│ │ ├── user_schema.py
│ │ └── version_check.py
│ ├── services/
│ │ ├── __init__.py
│ │ ├── ai_categorization_service.py
│ │ ├── ai_suggestion_service.py
│ │ ├── analytics_service.py
│ │ ├── api_token_service.py
│ │ ├── backup_service.py
│ │ ├── base_crud_service.py
│ │ ├── calendar_integration_service.py
│ │ ├── client_activity_feed_service.py
│ │ ├── client_approval_service.py
│ │ ├── client_notification_service.py
│ │ ├── client_report_service.py
│ │ ├── client_service.py
│ │ ├── comment_service.py
│ │ ├── currency_service.py
│ │ ├── custom_report_service.py
│ │ ├── email_service.py
│ │ ├── enhanced_ocr_service.py
│ │ ├── expense_service.py
│ │ ├── export_service.py
│ │ ├── gamification_service.py
│ │ ├── gantt_service.py
│ │ ├── global_search_service.py
│ │ ├── gps_tracking_service.py
│ │ ├── health_service.py
│ │ ├── import_service.py
│ │ ├── integration_service.py
│ │ ├── inventory_report_service.py
│ │ ├── invoice_approval_service.py
│ │ ├── invoice_service.py
│ │ ├── ldap_service.py
│ │ ├── llm_service.py
│ │ ├── notification_service.py
│ │ ├── payment_gateway_service.py
│ │ ├── payment_service.py
│ │ ├── peppol_service.py
│ │ ├── permission_service.py
│ │ ├── pomodoro_service.py
│ │ ├── project_service.py
│ │ ├── project_template_service.py
│ │ ├── quote_service.py
│ │ ├── recurring_invoice_service.py
│ │ ├── reporting_service.py
│ │ ├── scheduled_report_service.py
│ │ ├── stats_service.py
│ │ ├── support_prompt_service.py
│ │ ├── task_service.py
│ │ ├── time_approval_service.py
│ │ ├── time_entry_bulk_service.py
│ │ ├── time_entry_csv_import_service.py
│ │ ├── time_tracking_service.py
│ │ ├── unpaid_hours_service.py
│ │ ├── usage_stats_service.py
│ │ ├── user_service.py
│ │ ├── version_service.py
│ │ ├── workflow_engine.py
│ │ └── workforce_governance_service.py
│ ├── static/
│ │ ├── activity-feed.js
│ │ ├── admin-version-update.js
│ │ ├── ai-helper.js
│ │ ├── base-init.js
│ │ ├── calendar.css
│ │ ├── calendar.js
│ │ ├── charts.js
│ │ ├── commands.js
│ │ ├── css/
│ │ │ ├── brand-colors.css
│ │ │ ├── gantt-chart.css
│ │ │ └── rtl-support.css
│ │ ├── dashboard-enhancements.js
│ │ ├── dashboard-widgets.js
│ │ ├── data-tables-enhanced.css
│ │ ├── data-tables-enhanced.js
│ │ ├── date-picker-init.js
│ │ ├── enhanced-search.js
│ │ ├── enhanced-tables.js
│ │ ├── enhanced-ui.css
│ │ ├── enhanced-ui.js
│ │ ├── error-handling-enhanced.js
│ │ ├── floating-actions.js
│ │ ├── floating-timer-bar.js
│ │ ├── form-bridge.css
│ │ ├── form-validation.css
│ │ ├── form-validation.js
│ │ ├── global-fab.js
│ │ ├── idle.js
│ │ ├── images/
│ │ │ └── og-image-placeholder.md
│ │ ├── interactions.js
│ │ ├── js/
│ │ │ ├── command-palette.js
│ │ │ ├── gantt-color-picker.js
│ │ │ ├── integration_wizard.js
│ │ │ ├── ldap_wizard.js
│ │ │ ├── oidc_wizard.js
│ │ │ ├── setup-wizard.js
│ │ │ └── sw.js
│ │ ├── keyboard-shortcuts-advanced.js
│ │ ├── keyboard-shortcuts-enhanced.js
│ │ ├── keyboard-shortcuts.css
│ │ ├── keyboard-shortcuts.js
│ │ ├── kiosk-barcode.js
│ │ ├── kiosk-mode.css
│ │ ├── kiosk-mode.js
│ │ ├── kiosk-timer.js
│ │ ├── manifest.json
│ │ ├── mentions.js
│ │ ├── mobile.js
│ │ ├── offline-sync.js
│ │ ├── onboarding-enhanced.js
│ │ ├── onboarding.js
│ │ ├── pwa-enhancements.js
│ │ ├── quick-actions.js
│ │ ├── reports-enhanced.js
│ │ ├── smart-notifications.js
│ │ ├── src/
│ │ │ └── input.css
│ │ ├── support-ui.js
│ │ ├── test.txt
│ │ ├── time-entries-inline-edit.js
│ │ ├── toast-notifications.css
│ │ ├── toast-notifications.js
│ │ ├── typing-utils.js
│ │ ├── ui-enhancements.css
│ │ ├── ui-enhancements.js
│ │ └── uploads/
│ │ └── logos/
│ │ └── .gitkeep
│ ├── telemetry/
│ │ ├── __init__.py
│ │ ├── otel_setup.py
│ │ └── service.py
│ ├── templates/
│ │ ├── _components.html
│ │ ├── admin/
│ │ │ ├── api_tokens.html
│ │ │ ├── backups.html
│ │ │ ├── clear_cache.html
│ │ │ ├── custom_field_definitions/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ ├── dashboard.html
│ │ │ ├── email_support.html
│ │ │ ├── email_templates/
│ │ │ │ ├── create.html
│ │ │ │ ├── edit.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── integrations/
│ │ │ │ ├── list.html
│ │ │ │ └── setup.html
│ │ │ ├── ldap_setup_wizard.html
│ │ │ ├── link_templates/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ ├── modules.html
│ │ │ ├── oidc_debug.html
│ │ │ ├── oidc_setup_wizard.html
│ │ │ ├── oidc_user_detail.html
│ │ │ ├── pdf_layout.html
│ │ │ ├── permissions/
│ │ │ │ └── list.html
│ │ │ ├── quote_pdf_layout.html
│ │ │ ├── restore.html
│ │ │ ├── roles/
│ │ │ │ ├── form.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── salesman_email_mappings.html
│ │ │ ├── settings.html
│ │ │ ├── system_info.html
│ │ │ ├── telemetry.html
│ │ │ ├── user_form.html
│ │ │ ├── users/
│ │ │ │ └── roles.html
│ │ │ ├── users.html
│ │ │ └── webhooks/
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── analytics/
│ │ │ ├── dashboard.html
│ │ │ ├── dashboard_improved.html
│ │ │ └── mobile_dashboard.html
│ │ ├── approvals/
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── audit_logs/
│ │ │ ├── entity_history.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── auth/
│ │ │ ├── change_password.html
│ │ │ ├── edit_profile.html
│ │ │ ├── emails/
│ │ │ │ └── password_reset.html
│ │ │ ├── forgot_password.html
│ │ │ ├── login.html
│ │ │ ├── profile.html
│ │ │ ├── reset_password.html
│ │ │ ├── two_factor.html
│ │ │ └── two_factor_setup.html
│ │ ├── base.html
│ │ ├── budget/
│ │ │ ├── dashboard.html
│ │ │ └── project_detail.html
│ │ ├── calendar/
│ │ │ ├── event_detail.html
│ │ │ ├── event_form.html
│ │ │ ├── integrations.html
│ │ │ └── view.html
│ │ ├── chat/
│ │ │ ├── channel.html
│ │ │ └── index.html
│ │ ├── client_notes/
│ │ │ └── edit.html
│ │ ├── client_portal/
│ │ │ ├── activity_feed.html
│ │ │ ├── approval_detail.html
│ │ │ ├── approvals.html
│ │ │ ├── base.html
│ │ │ ├── dashboard.html
│ │ │ ├── documents.html
│ │ │ ├── error.html
│ │ │ ├── invoice_detail.html
│ │ │ ├── invoices.html
│ │ │ ├── issue_detail.html
│ │ │ ├── issues.html
│ │ │ ├── login.html
│ │ │ ├── new_issue.html
│ │ │ ├── notifications.html
│ │ │ ├── project_comments.html
│ │ │ ├── projects.html
│ │ │ ├── quote_detail.html
│ │ │ ├── quotes.html
│ │ │ ├── reports.html
│ │ │ ├── set_password.html
│ │ │ ├── time_entries.html
│ │ │ └── widgets/
│ │ │ ├── invoices.html
│ │ │ ├── pending_actions.html
│ │ │ ├── projects.html
│ │ │ ├── stats.html
│ │ │ └── time_entries.html
│ │ ├── clients/
│ │ │ ├── _clients_list.html
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── comments/
│ │ │ ├── _comment.html
│ │ │ ├── _comments_section.html
│ │ │ └── edit.html
│ │ ├── components/
│ │ │ ├── activity_feed_widget.html
│ │ │ ├── bulk_actions_widget.html
│ │ │ ├── cards.html
│ │ │ ├── chat_user_selector.html
│ │ │ ├── client_select.html
│ │ │ ├── keyboard_shortcuts_help.html
│ │ │ ├── multi_select.html
│ │ │ ├── offline_indicator.html
│ │ │ ├── persistent_chat_widget.html
│ │ │ ├── save_filter_widget.html
│ │ │ ├── support_modal.html
│ │ │ └── ui.html
│ │ ├── contacts/
│ │ │ ├── communication_form.html
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── deals/
│ │ │ ├── activity_form.html
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ ├── pipeline.html
│ │ │ └── view.html
│ │ ├── email/
│ │ │ ├── client_notification.html
│ │ │ ├── client_portal_password_setup.html
│ │ │ ├── comment_mention.html
│ │ │ ├── invoice.html
│ │ │ ├── overdue_invoice.html
│ │ │ ├── quote.html
│ │ │ ├── quote_accepted.html
│ │ │ ├── quote_approval_rejected.html
│ │ │ ├── quote_approval_request.html
│ │ │ ├── quote_approved.html
│ │ │ ├── quote_expired.html
│ │ │ ├── quote_expiring.html
│ │ │ ├── quote_rejected.html
│ │ │ ├── quote_sent.html
│ │ │ ├── scheduled_report.html
│ │ │ ├── task_assigned.html
│ │ │ ├── test_email.html
│ │ │ ├── unpaid_hours_report.html
│ │ │ └── weekly_summary.html
│ │ ├── errors/
│ │ │ ├── 400.html
│ │ │ ├── 403.html
│ │ │ ├── 404.html
│ │ │ ├── 500.html
│ │ │ └── generic.html
│ │ ├── expense_categories/
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── expenses/
│ │ │ ├── dashboard.html
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── gantt/
│ │ │ └── view.html
│ │ ├── import_export/
│ │ │ └── index.html
│ │ ├── integrations/
│ │ │ ├── activitywatch_setup.html
│ │ │ ├── caldav_setup.html
│ │ │ ├── health.html
│ │ │ ├── list.html
│ │ │ ├── manage.html
│ │ │ ├── view.html
│ │ │ ├── wizard_asana.html
│ │ │ ├── wizard_base.html
│ │ │ ├── wizard_github.html
│ │ │ ├── wizard_gitlab.html
│ │ │ ├── wizard_jira.html
│ │ │ ├── wizard_microsoft_teams.html
│ │ │ ├── wizard_outlook_calendar.html
│ │ │ ├── wizard_quickbooks.html
│ │ │ ├── wizard_trello.html
│ │ │ └── wizard_xero.html
│ │ ├── inventory/
│ │ │ ├── adjustments/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ ├── low_stock/
│ │ │ │ └── list.html
│ │ │ ├── movements/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ ├── purchase_orders/
│ │ │ │ ├── form.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── reports/
│ │ │ │ ├── dashboard.html
│ │ │ │ ├── low_stock.html
│ │ │ │ ├── movement_history.html
│ │ │ │ ├── turnover.html
│ │ │ │ └── valuation.html
│ │ │ ├── reservations/
│ │ │ │ └── list.html
│ │ │ ├── stock_items/
│ │ │ │ ├── form.html
│ │ │ │ ├── history.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── stock_levels/
│ │ │ │ ├── item.html
│ │ │ │ ├── list.html
│ │ │ │ └── warehouse.html
│ │ │ ├── suppliers/
│ │ │ │ ├── form.html
│ │ │ │ ├── list.html
│ │ │ │ └── view.html
│ │ │ ├── transfers/
│ │ │ │ ├── form.html
│ │ │ │ └── list.html
│ │ │ └── warehouses/
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── invoice_approvals/
│ │ │ ├── list.html
│ │ │ ├── request.html
│ │ │ └── view.html
│ │ ├── invoices/
│ │ │ ├── _invoices_list.html
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── generate_from_time.html
│ │ │ ├── list.html
│ │ │ ├── pdf_default.html
│ │ │ ├── pdf_styles_default.css
│ │ │ └── view.html
│ │ ├── issues/
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ ├── new.html
│ │ │ └── view.html
│ │ ├── kanban/
│ │ │ ├── board.html
│ │ │ ├── columns.html
│ │ │ ├── create_column.html
│ │ │ └── edit_column.html
│ │ ├── kiosk/
│ │ │ ├── base.html
│ │ │ ├── dashboard.html
│ │ │ └── login.html
│ │ ├── leads/
│ │ │ ├── activity_form.html
│ │ │ ├── convert_to_client.html
│ │ │ ├── convert_to_deal.html
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── main/
│ │ │ ├── about.html
│ │ │ ├── dashboard.html
│ │ │ ├── donate.html
│ │ │ ├── help.html
│ │ │ └── search.html
│ │ ├── mileage/
│ │ │ ├── form.html
│ │ │ ├── gps.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── offline.html
│ │ ├── partials/
│ │ │ ├── _bottom_nav.html
│ │ │ └── _command_palette.html
│ │ ├── payment_gateways/
│ │ │ ├── create.html
│ │ │ ├── list.html
│ │ │ └── pay.html
│ │ ├── payments/
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── per_diem/
│ │ │ ├── form.html
│ │ │ ├── list.html
│ │ │ ├── rate_form.html
│ │ │ ├── rates_list.html
│ │ │ └── view.html
│ │ ├── project_templates/
│ │ │ ├── create.html
│ │ │ ├── create_project.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── projects/
│ │ │ ├── _kanban_tailwind.html
│ │ │ ├── _projects_list.html
│ │ │ ├── add_cost.html
│ │ │ ├── add_good.html
│ │ │ ├── archive.html
│ │ │ ├── create.html
│ │ │ ├── dashboard.html
│ │ │ ├── edit.html
│ │ │ ├── edit_cost.html
│ │ │ ├── edit_good.html
│ │ │ ├── goods.html
│ │ │ ├── list.html
│ │ │ ├── time_entries_overview.html
│ │ │ └── view.html
│ │ ├── quotes/
│ │ │ ├── _edit_quote_form_scripts.html
│ │ │ ├── _quotes_list.html
│ │ │ ├── accept.html
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ ├── pdf_default.html
│ │ │ ├── pdf_styles_default.css
│ │ │ └── view.html
│ │ ├── recurring_invoices/
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── recurring_tasks/
│ │ │ ├── form.html
│ │ │ └── list.html
│ │ ├── reports/
│ │ │ ├── builder.html
│ │ │ ├── custom_view.html
│ │ │ ├── export_form.html
│ │ │ ├── index.html
│ │ │ ├── iterative_view.html
│ │ │ ├── project_report.html
│ │ │ ├── saved_views_list.html
│ │ │ ├── schedule_form.html
│ │ │ ├── scheduled.html
│ │ │ ├── summary.html
│ │ │ ├── task_report.html
│ │ │ ├── time_entries_report.html
│ │ │ ├── unpaid_hours_report.html
│ │ │ ├── user_report.html
│ │ │ └── week_in_review.html
│ │ ├── saved_filters/
│ │ │ └── list.html
│ │ ├── settings/
│ │ │ └── keyboard_shortcuts.html
│ │ ├── setup/
│ │ │ └── initial_setup.html
│ │ ├── tasks/
│ │ │ ├── _kanban.html
│ │ │ ├── _tasks_list.html
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ ├── my_tasks.html
│ │ │ ├── overdue.html
│ │ │ └── view.html
│ │ ├── time_entry_templates/
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── list.html
│ │ │ └── view.html
│ │ ├── timer/
│ │ │ ├── _time_entries_list.html
│ │ │ ├── bulk_entry.html
│ │ │ ├── calendar.html
│ │ │ ├── edit_timer.html
│ │ │ ├── manual_entry.html
│ │ │ ├── time_entries_export_pdf.html
│ │ │ ├── time_entries_overview.html
│ │ │ ├── timer_page.html
│ │ │ └── view_timer.html
│ │ ├── user/
│ │ │ ├── license.html
│ │ │ ├── profile.html
│ │ │ └── settings.html
│ │ ├── weekly_goals/
│ │ │ ├── create.html
│ │ │ ├── edit.html
│ │ │ ├── index.html
│ │ │ └── view.html
│ │ └── workforce/
│ │ └── dashboard.html
│ └── utils/
│ ├── api_auth.py
│ ├── api_deprecation.py
│ ├── api_idempotency.py
│ ├── api_rate_limit.py
│ ├── api_responses.py
│ ├── audit.py
│ ├── auth_method.py
│ ├── backup.py
│ ├── budget_forecasting.py
│ ├── cache.py
│ ├── cache_redis.py
│ ├── cii_invoice.py
│ ├── cli.py
│ ├── client_lock.py
│ ├── config_manager.py
│ ├── context_processors.py
│ ├── data_export.py
│ ├── data_import.py
│ ├── datetime_utils.py
│ ├── db.py
│ ├── decorators.py
│ ├── donate_hide_code.py
│ ├── email.py
│ ├── env_validation.py
│ ├── error_handlers.py
│ ├── error_handling.py
│ ├── event_bus.py
│ ├── excel_export.py
│ ├── file_upload.py
│ ├── i18n.py
│ ├── i18n_helpers.py
│ ├── installation.py
│ ├── integration_http.py
│ ├── integration_sync_context.py
│ ├── invoice_numbering.py
│ ├── invoice_pdf_postprocess.py
│ ├── invoice_validators.py
│ ├── keyboard_shortcuts_defaults.py
│ ├── legacy_migrations.py
│ ├── license_utils.py
│ ├── logger.py
│ ├── mileage_pdf.py
│ ├── module_helpers.py
│ ├── module_registry.py
│ ├── ocr.py
│ ├── oidc_metadata.py
│ ├── overtime.py
│ ├── pagination.py
│ ├── pdf_generator.py
│ ├── pdf_generator_fallback.py
│ ├── pdf_generator_reportlab.py
│ ├── pdf_template_schema.py
│ ├── pdfa3.py
│ ├── per_diem_pdf.py
│ ├── performance.py
│ ├── permissions.py
│ ├── permissions_seed.py
│ ├── posthog_funnels.py
│ ├── posthog_monitoring.py
│ ├── posthog_segmentation.py
│ ├── powerpoint_export.py
│ ├── prepaid_hours.py
│ ├── query_logging.py
│ ├── query_optimization.py
│ ├── quote_access.py
│ ├── rate_limiting.py
│ ├── role_migration.py
│ ├── route_helpers.py
│ ├── safe_template_render.py
│ ├── scheduled_tasks.py
│ ├── scope_filter.py
│ ├── search.py
│ ├── secret_crypto.py
│ ├── seed_dev_data.py
│ ├── setup_logging.py
│ ├── stripe_integration.py
│ ├── summary_report_pdf.py
│ ├── support_report_generation.py
│ ├── telemetry.py
│ ├── template_filters.py
│ ├── time_entries_pdf.py
│ ├── time_entry_validation.py
│ ├── time_rounding.py
│ ├── timezone.py
│ ├── transactions.py
│ ├── validation.py
│ ├── version_compare.py
│ ├── webhook_dispatcher.py
│ ├── webhook_service.py
│ └── zugferd.py
├── app.py
├── babel.cfg
├── crowdin.yml
├── desktop/
│ ├── .npmrc
│ ├── README.md
│ ├── assets/
│ │ ├── .gitkeep
│ │ ├── README.md
│ │ └── icon.icns
│ ├── dist-renderer/
│ │ ├── assets/
│ │ │ ├── index-BqW2gGjC.js
│ │ │ └── index-D2aGha3a.css
│ │ └── index.html
│ ├── package-lock.json
│ ├── package.json
│ ├── scripts/
│ │ ├── build-all-platforms.js
│ │ └── clean-cache.js
│ ├── src/
│ │ ├── main/
│ │ │ ├── main.js
│ │ │ ├── preload.js
│ │ │ ├── tray.js
│ │ │ └── window.js
│ │ ├── renderer/
│ │ │ ├── css/
│ │ │ │ ├── brand-colors.css
│ │ │ │ ├── splash.css
│ │ │ │ └── styles.css
│ │ │ ├── index.html
│ │ │ ├── js/
│ │ │ │ ├── api/
│ │ │ │ │ └── client.js
│ │ │ │ ├── app.js
│ │ │ │ ├── bundle.js
│ │ │ │ ├── connection/
│ │ │ │ │ ├── connection_manager.js
│ │ │ │ │ ├── connection_state.js
│ │ │ │ │ ├── request_policy.js
│ │ │ │ │ └── timer_operations.js
│ │ │ │ ├── state.js
│ │ │ │ ├── storage/
│ │ │ │ │ └── storage.js
│ │ │ │ ├── ui/
│ │ │ │ │ └── notifications.js
│ │ │ │ └── utils/
│ │ │ │ └── helpers.js
│ │ │ └── splash.html
│ │ ├── renderer-react/
│ │ │ ├── index.html
│ │ │ └── src/
│ │ │ ├── main.jsx
│ │ │ ├── services/
│ │ │ │ ├── api.js
│ │ │ │ ├── diagnostics.js
│ │ │ │ └── store.js
│ │ │ ├── styles/
│ │ │ │ └── app.css
│ │ │ └── sync/
│ │ │ └── syncEngine.js
│ │ └── shared/
│ │ └── config.js
│ ├── test/
│ │ ├── api-client.test.js
│ │ ├── connection_manager.test.js
│ │ ├── integration_info_server.test.js
│ │ ├── react_renderer_package.test.js
│ │ └── timer_operations.test.js
│ └── vite.config.mjs
├── docker/
│ ├── Dockerfile.certgen
│ ├── Dockerfile.mkcert
│ ├── STARTUP_MIGRATION_CONFIG.md
│ ├── TROUBLESHOOTING_DB_CONNECTION.md
│ ├── debug_startup.sh
│ ├── docker-compose.analytics.yml
│ ├── docker-compose.https-auto.yml
│ ├── docker-compose.https-mkcert.yml
│ ├── docker-compose.local-test.yml
│ ├── docker-compose.remote-dev.yml
│ ├── docker-compose.remote.yml
│ ├── docs
│ ├── entrypoint-local-test-simple.sh
│ ├── entrypoint-local-test.sh
│ ├── entrypoint.py
│ ├── entrypoint.sh
│ ├── entrypoint_fixed.sh
│ ├── entrypoint_simple.sh
│ ├── fix-all-column-issues.py
│ ├── fix-all-issues.py
│ ├── fix-column-name-mismatch.py
│ ├── fix-docker-permissions.py
│ ├── fix-docker-permissions.sh
│ ├── fix-duplicate-columns.py
│ ├── fix-invoice-tables.py
│ ├── fix-invoices-now.py
│ ├── fix-permissions-aggressive.py
│ ├── fix-schema.py
│ ├── fix-settings-table.py
│ ├── fix-settings-table.sql
│ ├── fix-upload-permissions.py
│ ├── fix-upload-permissions.sh
│ ├── force-schema-update.py
│ ├── generate-mkcert-certs.sh
│ ├── init-database-enhanced.py
│ ├── init-database-simple.py
│ ├── init-database-sql.py
│ ├── init-database.py
│ ├── init-db.sh
│ ├── init.sh
│ ├── init.sql
│ ├── logrotate.conf.example
│ ├── migrate-add-company-branding.py
│ ├── migrate-add-missing-settings-columns.py
│ ├── migrate-add-project-costs.py
│ ├── migrate-add-task-columns.py
│ ├── migrate-add-tasks.py
│ ├── migrate-avatar-storage.py
│ ├── migrate-field-names.py
│ ├── migrate-logo-upload.py
│ ├── seed-dev-data.sh
│ ├── simple_test.sh
│ ├── start-enhanced.py
│ ├── start-fixed.py
│ ├── start-fixed.sh
│ ├── start-minimal.sh
│ ├── start-new.sh
│ ├── start-simple.sh
│ ├── start.py
│ ├── start.sh
│ ├── startup_with_migration.py
│ ├── supervisord.conf
│ ├── test-database-complete.py
│ ├── test-db-simple.py
│ ├── test-db.py
│ ├── test-packages.py
│ ├── test-pdf-generation.py
│ ├── test-permissions.py
│ ├── test-routing.py
│ ├── test-schema-fixed.py
│ ├── test-schema.py
│ ├── test-startup.sh
│ ├── test_db_connection.py
│ └── verify-database.py
├── docker-compose.example.yml
├── docker-compose.yml
├── docs/
│ ├── ADVANCED_PERMISSIONS.md
│ ├── API.md
│ ├── APPLY_FIXES_NOW.md
│ ├── APPLY_KANBAN_MIGRATION.md
│ ├── ARCHITECTURE.md
│ ├── ARCHITECTURE_AUDIT.md
│ ├── ASSETS.md
│ ├── AVATAR_PERSISTENCE_SUMMARY.md
│ ├── AVATAR_STORAGE_MIGRATION.md
│ ├── BRANDING.md
│ ├── BRAND_GUIDELINES.md
│ ├── BREAK_TIME_FEATURE.md
│ ├── BUDGET_ALERTS_AND_FORECASTING.md
│ ├── BUILD_CONFIGURATION.md
│ ├── BUILD_SCRIPTS.md
│ ├── BUILD_WINDOWS_PERMISSIONS.md
│ ├── BULK_TASK_OPERATIONS.md
│ ├── BULK_TIME_ENTRY_README.md
│ ├── CALENDAR_AGENDA_FEATURE.md
│ ├── CALENDAR_FEATURES_README.md
│ ├── CLIENT_FEATURES_COMPLETE_IMPLEMENTATION.md
│ ├── CLIENT_FEATURES_FINAL_IMPLEMENTATION.md
│ ├── CLIENT_FEATURES_IMPLEMENTATION.md
│ ├── CLIENT_FEATURES_IMPLEMENTATION_STATUS.md
│ ├── CLIENT_FEATURE_RECOMMENDATIONS.md
│ ├── CLIENT_MANAGEMENT_README.md
│ ├── CLIENT_NOTES_FEATURE.md
│ ├── CLIENT_PORTAL.md
│ ├── CODEBASE_AUDIT.md
│ ├── CODE_BASED_ANALYSIS_REPORT.md
│ ├── COMMAND_PALETTE_DEMO.html
│ ├── COMMAND_PALETTE_USAGE.md
│ ├── COMPLETE_IMPROVEMENTS_SUMMARY.md
│ ├── CONTRIBUTING_TRANSLATIONS.md
│ ├── CRM_FEATURES_IMPLEMENTATION.md
│ ├── CRM_IMPLEMENTATION_SUMMARY.md
│ ├── DATABASE_RECOVERY.md
│ ├── DATABASE_STARTUP_FIX_README.md
│ ├── DEFAULT_DATA_SEEDING.md
│ ├── DESKTOP_SETTINGS.md
│ ├── DEVELOPMENT.md
│ ├── DIAGNOSIS_STEPS.md
│ ├── DOCS_AUDIT.md
│ ├── DOCUMENTATION_REORGANIZATION_SUMMARY.md
│ ├── DOCUMENTATION_RESTRUCTURE_SUMMARY.md
│ ├── ENHANCED_DATABASE_STARTUP.md
│ ├── ENHANCED_INVOICE_SYSTEM_README.md
│ ├── ERROR_HANDLER_IMPROVEMENTS.md
│ ├── EXPENSE_TRACKING.md
│ ├── EXTRA_GOODS_FEATURE.md
│ ├── FAVORITE_PROJECTS_FEATURE.md
│ ├── FEATURES_COMPLETE.md
│ ├── FINAL_SYMLINK_FIX.md
│ ├── FIX_SYMLINK_ISSUE.md
│ ├── FIX_SYMLINK_PERMISSIONS.md
│ ├── FRONTEND.md
│ ├── GETTING_STARTED.md
│ ├── GITHUB_WORKFLOW_IMAGES.md
│ ├── IMPLEMENTATION_COMPLETE_SUMMARY.md
│ ├── IMPLEMENTATION_STATUS_UPDATE.md
│ ├── IMPORT_EXPORT_GUIDE.md
│ ├── INCOMPLETE_IMPLEMENTATIONS_ANALYSIS.md
│ ├── INVOICE_EXPENSES.md
│ ├── INVOICE_EXTRA_GOODS_PDF_EXPORT.md
│ ├── INVOICE_FEATURE_README.md
│ ├── INVOICE_INTERFACE_IMPROVEMENTS.md
│ ├── KEYBOARD_SHORTCUTS_DEVELOPER.md
│ ├── KEYBOARD_SHORTCUTS_IMPLEMENTATION.md
│ ├── KIOSK_MODE_INVENTORY_ANALYSIS.md
│ ├── KIOSK_MODE_INVENTORY_SUMMARY.md
│ ├── KIOSK_REVIEW_AND_IMPROVEMENTS.md
│ ├── LOGO_UPLOAD_IMPLEMENTATION_SUMMARY.md
│ ├── LOGO_UPLOAD_SYSTEM_README.md
│ ├── MOBILE_IMPROVEMENTS.md
│ ├── MULTISELECT_FILTERS_TESTING.md
│ ├── ONEDRIVE_FIX.md
│ ├── PAYMENT_TRACKING.md
│ ├── PDF_EDITOR_ENHANCED_FEATURES.md
│ ├── PDF_EDITOR_QUICK_START.md
│ ├── PDF_GENERATION_TROUBLESHOOTING.md
│ ├── PDF_LAYOUT_CUSTOMIZATION.md
│ ├── PERFORMANCE.md
│ ├── PRODUCT_UX_AUDIT.md
│ ├── PROFILE_PICTURE_UPLOAD_FIX.md
│ ├── PROJECT_ANALYSIS_REPORT.md
│ ├── PROJECT_ARCHIVING_GUIDE.md
│ ├── QUICK_FIX.md
│ ├── QUICK_FIX_SYMLINK.md
│ ├── QUICK_REFERENCE_GUIDE.md
│ ├── QUICK_START_CODE_SIGNING.md
│ ├── QUICK_WINS_IMPLEMENTATION.md
│ ├── QUICK_WINS_UI.md
│ ├── README.md
│ ├── REPORTLAB_MIGRATION_CHECKLIST.md
│ ├── REPORTLAB_MIGRATION_SUMMARY.md
│ ├── REQUIREMENTS.md
│ ├── SOLUTION_GUIDE.md
│ ├── SUBCONTRACTOR_ROLE.md
│ ├── TASK_MANAGEMENT.md
│ ├── TASK_MANAGEMENT_README.md
│ ├── TELEMETRY_QUICK_START.md
│ ├── TELEMETRY_TRANSPARENCY.md
│ ├── TESTING_COVERAGE_GUIDE.md
│ ├── TESTING_QUICK_REFERENCE.md
│ ├── TEST_AVATAR_PERSISTENCE.md
│ ├── TIMETRACKER_TEMPLATES_IMPLEMENTATION.md
│ ├── TIME_ENTRY_TEMPLATES.md
│ ├── TIME_ROUNDING_PREFERENCES.md
│ ├── TOAST_NOTIFICATION_DEMO.html
│ ├── TOAST_NOTIFICATION_SYSTEM.md
│ ├── TOAST_NOTIFICATION_VISUAL_GUIDE.md
│ ├── TRANSLATION_SYSTEM.md
│ ├── TROUBLESHOOTING_BUILD.md
│ ├── TROUBLESHOOTING_OIDC_DNS.md
│ ├── TROUBLESHOOTING_QUOTES_TEMPLATE_ID.md
│ ├── TROUBLESHOOTING_TRANSACTION_ERROR.md
│ ├── UI_GUIDELINES.md
│ ├── UPLOADS_PERSISTENCE.md
│ ├── WEEKLY_TIME_GOALS.md
│ ├── WINDOWS_BUILD.md
│ ├── WINDOWS_CODE_SIGNING.md
│ ├── admin/
│ │ ├── README.md
│ │ ├── SUPPORT_CONVERSION_AB_TESTS.md
│ │ ├── configuration/
│ │ │ ├── DESKTOP_BUILD_WINDOWS_TROUBLESHOOTING.md
│ │ │ ├── DOCKER_COMPOSE_SETUP.md
│ │ │ ├── DOCKER_PUBLIC_SETUP.md
│ │ │ ├── DOCKER_STARTUP_TROUBLESHOOTING.md
│ │ │ ├── EMAIL_CONFIGURATION.md
│ │ │ ├── LDAP_SETUP.md
│ │ │ ├── NGINX_PUBLIC_DOMAIN.md
│ │ │ ├── OIDC_SETUP.md
│ │ │ ├── PEPPOL_EINVOICING.md
│ │ │ └── SUPPORT_VISIBILITY.md
│ │ ├── deployment/
│ │ │ ├── OFFICIAL_BUILDS.md
│ │ │ ├── PORTAINER_DEPLOYMENT.md
│ │ │ ├── RELEASE_PROCESS.md
│ │ │ └── VERSION_MANAGEMENT.md
│ │ ├── monitoring/
│ │ │ ├── ANALYTICS_FILES_MANIFEST.md
│ │ │ ├── ANALYTICS_IMPLEMENTATION_SUMMARY.md
│ │ │ ├── ANALYTICS_QUICK_START.md
│ │ │ ├── POSTHOG_ADVANCED_FEATURES.md
│ │ │ ├── POSTHOG_ENHANCEMENTS_SUMMARY.md
│ │ │ ├── POSTHOG_QUICK_REFERENCE.md
│ │ │ ├── README_TELEMETRY_POLICY.md
│ │ │ ├── TELEMETRY_CHEAT_SHEET.md
│ │ │ ├── TELEMETRY_IMPLEMENTATION_SUMMARY.md
│ │ │ └── TELEMETRY_POSTHOG_MIGRATION.md
│ │ └── security/
│ │ ├── AUTOMATIC_HTTPS_SUMMARY.md
│ │ ├── CSRF_CONFIGURATION.md
│ │ ├── CSRF_DOCKER_CONFIGURATION_SUMMARY.md
│ │ ├── CSRF_INTEGRATION_REVIEW.md
│ │ ├── CSRF_IP_ACCESS_FIX.md
│ │ ├── CSRF_IP_ACCESS_GUIDE.md
│ │ ├── CSRF_IP_FIX_SUMMARY.md
│ │ ├── CSRF_TOKEN_FIX_SUMMARY.md
│ │ ├── CSRF_TROUBLESHOOTING.md
│ │ ├── HTTPS_MKCERT_GUIDE.md
│ │ ├── P0_SECURITY_IMPROVEMENTS.md
│ │ ├── README_HTTPS.md
│ │ └── README_HTTPS_AUTO.md
│ ├── all_tracked_events.md
│ ├── analytics.md
│ ├── api/
│ │ ├── API_CONSISTENCY_AUDIT.md
│ │ ├── API_ENHANCEMENTS.md
│ │ ├── API_TOKEN_SCOPES.md
│ │ ├── API_VERSIONING.md
│ │ ├── README.md
│ │ ├── RESPONSE_FORMAT.md
│ │ └── REST_API.md
│ ├── assets/
│ │ └── README.md
│ ├── bugfixes/
│ │ └── template_application_fix.md
│ ├── cicd/
│ │ ├── BUILD_CONFIGURATION_SUMMARY.md
│ │ ├── CI_CD_DOCUMENTATION.md
│ │ ├── CI_CD_FIXES.md
│ │ ├── CI_CD_FIXES_ROUND_2.md
│ │ ├── CI_CD_IMPLEMENTATION_SUMMARY.md
│ │ ├── CI_CD_QUICK_START.md
│ │ ├── CI_CD_WORKFLOW_ARCHITECTURE.md
│ │ ├── FINAL_CI_CD_SUMMARY.md
│ │ ├── GITHUB_ACTIONS_SETUP.md
│ │ ├── GITHUB_ACTIONS_VERIFICATION.md
│ │ ├── PIPELINE_CLEANUP_PLAN.md
│ │ ├── QUICK_REFERENCE_TESTING.md
│ │ ├── QUICK_START_BUILD.md
│ │ ├── README_BUILD_CONFIGURATION.md
│ │ ├── README_CI_CD_SECTION.md
│ │ ├── STREAMLINED_CI_CD.md
│ │ └── TESTING_WORKFLOW_STRATEGY.md
│ ├── competitive-analysis/
│ │ ├── GAP_RUBRIC.md
│ │ ├── PHASE_1_PRD.md
│ │ ├── PHASE_2_PRD.md
│ │ └── README.md
│ ├── deploy/
│ │ └── RENDER.md
│ ├── development/
│ │ ├── CODE_OF_CONDUCT.md
│ │ ├── CONTRIBUTING.md
│ │ ├── CONTRIBUTOR_GUIDE.md
│ │ ├── FRONTEND_QUALITY_GATES.md
│ │ ├── LOCAL_DEVELOPMENT_WITH_ANALYTICS.md
│ │ ├── LOCAL_TESTING_WITH_SQLITE.md
│ │ ├── MODULE_INTEGRATION_IMPLEMENTATION.md
│ │ ├── MODULE_INTEGRATION_PLAN.md
│ │ ├── MODULE_STRUCTURE_ANALYSIS.md
│ │ ├── PROJECT_STRUCTURE.md
│ │ ├── RBAC_PERMISSION_MODEL.md
│ │ ├── README.md
│ │ ├── SEED_DEV_DATA.md
│ │ └── SERVICE_LAYER_AND_BASE_CRUD.md
│ ├── events.md
│ ├── features/
│ │ ├── ALEMBIC_MIGRATION_README.md
│ │ ├── ALEMBIC_MIGRATION_SUMMARY.md
│ │ ├── BADGES.md
│ │ ├── CALDAV_INTEGRATION.md
│ │ ├── CALDAV_QUICK_SETUP.md
│ │ ├── CALENDAR_QUICK_START.md
│ │ ├── CALENDAR_QUICK_WINS_SUMMARY.md
│ │ ├── CALENDAR_QUICK_WINS_VISUAL_GUIDE.md
│ │ ├── CSV_EXPORT_ENHANCED.md
│ │ ├── INVENTORY_IMPLEMENTATION_STATUS.md
│ │ ├── INVENTORY_MANAGEMENT_PLAN.md
│ │ ├── INVENTORY_MISSING_FEATURES.md
│ │ ├── KEYBOARD_AND_NOTIFICATIONS_FIX.md
│ │ ├── KEYBOARD_SHORTCUTS_ENHANCED.md
│ │ ├── KEYBOARD_SHORTCUTS_FINAL_FIX.md
│ │ ├── KEYBOARD_SHORTCUTS_FIXED.md
│ │ ├── KEYBOARD_SHORTCUTS_README.md
│ │ ├── LAYOUT_IMPROVEMENTS_COMPLETE.md
│ │ ├── MIGRATION_INSTRUCTIONS.md
│ │ ├── MULTISELECT_FILTERS.md
│ │ ├── OVERTIME_TRACKING.md
│ │ ├── PROJECT_COSTS_FEATURE.md
│ │ ├── PROJECT_COSTS_IMPLEMENTATION_SUMMARY.md
│ │ ├── PROJECT_DASHBOARD.md
│ │ ├── QUICK_START_PROJECT_COSTS.md
│ │ ├── RUN_BLACK_FORMATTING.md
│ │ ├── SMART_NOTIFICATIONS.md
│ │ ├── TIME_ENTRY_DUPLICATION.md
│ │ ├── TIME_ENTRY_TEMPLATES.md
│ │ ├── USER_DELETION.md
│ │ ├── WORKFORCE_DELETE.md
│ │ ├── activity_feed.md
│ │ ├── kanban/
│ │ │ ├── CUSTOM_KANBAN_README.md
│ │ │ ├── DEBUG_KANBAN_COLUMNS.md
│ │ │ ├── KANBAN_AUTO_REFRESH_COMPLETE.md
│ │ │ ├── KANBAN_CUSTOMIZATION.md
│ │ │ ├── KANBAN_REFRESH_FINAL_FIX.md
│ │ │ └── KANBAN_REFRESH_SOLUTION.md
│ │ └── webhooks.md
│ ├── guides/
│ │ ├── DEPLOYMENT_GUIDE.md
│ │ ├── IMPROVEMENTS_QUICK_REFERENCE.md
│ │ ├── QUICK_START_GUIDE.md
│ │ ├── QUICK_START_LOCAL_DEVELOPMENT.md
│ │ └── README.md
│ ├── implementation-notes/
│ │ ├── ACTIVITY_LOGGING_INTEGRATION_GUIDE.md
│ │ ├── ADVANCED_FEATURES_IMPLEMENTATION_GUIDE.md
│ │ ├── ADVANCED_PERMISSIONS_IMPLEMENTATION_SUMMARY.md
│ │ ├── ADVANCED_REPORT_BUILDER_IMPLEMENTATION.md
│ │ ├── ANALYTICS_IMPROVEMENTS_SUMMARY.md
│ │ ├── APPLICATION_REVIEW_2025.md
│ │ ├── ARCHITECTURE_MIGRATION_GUIDE.md
│ │ ├── AVATAR_PERSISTENCE_CHANGELOG.md
│ │ ├── BROWSER_CACHE_FIX.md
│ │ ├── BUGFIX_DB_IMPORT.md
│ │ ├── BUGFIX_METADATA_RESERVED.md
│ │ ├── BULK_OPERATIONS_IMPROVEMENTS.md
│ │ ├── CALENDAR_IMPROVEMENTS_SUMMARY.md
│ │ ├── CHANGES_SUMMARY.md
│ │ ├── CHANGES_SUMMARY_TESTING_WORKFLOW.md
│ │ ├── COMMAND_PALETTE_CHANGELOG.md
│ │ ├── COMMAND_PALETTE_IMPROVEMENTS.md
│ │ ├── COMMENT_ATTACHMENTS_IMPLEMENTATION.md
│ │ ├── COMMENT_ATTACHMENTS_OPTIMIZATION.md
│ │ ├── COMPLETE_ADVANCED_FEATURES_SUMMARY.md
│ │ ├── COMPLETE_IMPLEMENTATION_CHECKLIST.md
│ │ ├── COMPLETE_IMPLEMENTATION_FINAL.md
│ │ ├── COMPLETE_IMPLEMENTATION_REVIEW.md
│ │ ├── COMPLETE_IMPLEMENTATION_SUMMARY.md
│ │ ├── COMPREHENSIVE_IMPLEMENTATION_STATUS.md
│ │ ├── COMPREHENSIVE_IMPLEMENTATION_SUMMARY.md
│ │ ├── CONFIGURATION_FINAL_SUMMARY.md
│ │ ├── COVERAGE_FIX_SUMMARY.md
│ │ ├── DASHBOARD_NAVBAR_IMPROVEMENTS.md
│ │ ├── DEFAULT_DATA_SEEDING_FIX_CHANGELOG.md
│ │ ├── DELETION_AND_STATUS_IMPROVEMENTS.md
│ │ ├── DOCUMENTATION_RESTRUCTURE_SUMMARY.md
│ │ ├── ENHANCEMENT_PLAN_IMPLEMENTATION_STATUS.md
│ │ ├── ENHANCEMENT_PLAN_PROGRESS_SUMMARY.md
│ │ ├── FEATURE_IMPLEMENTATION_PROGRESS.md
│ │ ├── FINAL_IMPLEMENTATION_REPORT.md
│ │ ├── FINAL_IMPLEMENTATION_SUMMARY.md
│ │ ├── FINAL_SUMMARY.md
│ │ ├── FORCE_NO_CACHE_FIX.md
│ │ ├── HIGH_IMPACT_FEATURES.md
│ │ ├── HIGH_IMPACT_SUMMARY.md
│ │ ├── IMPLEMENTATION_COMPLETE.md
│ │ ├── IMPLEMENTATION_COMPLETE_SUMMARY.md
│ │ ├── IMPLEMENTATION_PROGRESS_2025.md
│ │ ├── IMPLEMENTATION_STATUS.md
│ │ ├── IMPLEMENTATION_SUMMARY.md
│ │ ├── IMPLEMENTATION_SUMMARY_CONTINUED.md
│ │ ├── IMPLEMENTATION_SUMMARY_DEFAULT_DATA_SEEDING.md
│ │ ├── INTEGRATION_REFACTORING_PLAN.md
│ │ ├── INVENTORY_PO_FORM_JSON.md
│ │ ├── KANBAN_IMPROVEMENTS.md
│ │ ├── KEYBOARD_SHORTCUTS_SUMMARY.md
│ │ ├── MANUAL_ENTRY_WORKED_TIME_FIX_559.md
│ │ ├── MIGRATION_018_FIX_SUMMARY.md
│ │ ├── MIGRATION_VALIDATION_FIX.md
│ │ ├── NOTIFICATION_SYSTEM_SUMMARY.md
│ │ ├── OIDC_IMPROVEMENTS.md
│ │ ├── OIDC_LOGOUT_FIX_SUMMARY.md
│ │ ├── PROGRESS_UPDATE.md
│ │ ├── PROJECT_ANALYSIS_AND_IMPROVEMENTS.md
│ │ ├── QUICK_FIX_SUMMARY.md
│ │ ├── QUICK_START_ARCHITECTURE.md
│ │ ├── QUICK_WINS_IMPLEMENTATION.md
│ │ ├── QUICK_WINS_SUMMARY.md
│ │ ├── README_IMPROVEMENTS.md
│ │ ├── README_NEW_ARCHITECTURE.md
│ │ ├── REPORTS_IMPROVEMENTS_SUMMARY.md
│ │ ├── ROUTE_REGISTRATION_AND_TEMPLATES_COMPLETE.md
│ │ ├── SESSION_CLOSE_ERROR_FIX.md
│ │ ├── SESSION_SUMMARY.md
│ │ ├── SMOKETEST_FIXES_SUMMARY.md
│ │ ├── STYLING_CONSISTENCY_SUMMARY.md
│ │ ├── TESTING_COMPLETE.md
│ │ ├── TOAST_NOTIFICATION_IMPROVEMENTS.md
│ │ ├── TRANSLATION_FIXES_SUMMARY.md
│ │ ├── TRANSLATION_IMPROVEMENTS_SUMMARY.md
│ │ ├── UI_IMPROVEMENTS_SUMMARY.md
│ │ ├── UX_QUICK_WINS_IMPLEMENTATION.md
│ │ └── VERSION_MANAGEMENT_SUMMARY.md
│ ├── import_export/
│ │ └── README.md
│ ├── integrations/
│ │ ├── ACTIVITYWATCH.md
│ │ ├── LINEAR.md
│ │ └── XERO.md
│ ├── mobile-desktop-apps/
│ │ ├── FINAL_REVIEW.md
│ │ ├── IMPLEMENTATION_COMPLETE.md
│ │ ├── IMPLEMENTATION_SUMMARY.md
│ │ ├── README.md
│ │ └── REVIEW.md
│ ├── pdf_template_alternatives_research.md
│ ├── privacy.md
│ ├── reports/
│ │ ├── ALL_BUGFIXES_SUMMARY.md
│ │ ├── README.md
│ │ ├── TRANSLATION_ANALYSIS_REPORT.md
│ │ ├── UNPAID_BY_SALESMAN_AND_SCHEDULED_REPORTS.md
│ │ └── i18n_audit_report.md
│ ├── telemetry-architecture.md
│ ├── testing/
│ │ ├── TESTING_STRATEGY.md
│ │ ├── TEST_PERFORMANCE_OPTIMIZATIONS.md
│ │ ├── TEST_REPORT.md
│ │ └── TEST_RESULTS_AVATAR_PERSISTENCE.md
│ └── user-guides/
│ └── DUPLICATING_TIME_ENTRIES.md
├── donate_hide_public.pem
├── env.example
├── env.local-test.example
├── examples/
│ └── zapier/
│ └── webhook_time_entry_created.json
├── grafana/
│ └── provisioning/
│ └── datasources/
│ └── prometheus.yml
├── loki/
│ └── loki-config.yml
├── migrations/
│ ├── MIGRATION_GUIDE.md
│ ├── README.md
│ ├── add_analytics_column.sql
│ ├── add_analytics_setting.py
│ ├── add_project_costs.sql
│ ├── alembic.ini
│ ├── ensure_uploads_persistence.py
│ ├── env.py
│ ├── fix_invoice_currency.py
│ ├── fix_invoice_pdf_template_items_source.py
│ ├── legacy_schema_migration.py
│ ├── manage_migrations.py
│ ├── migrate_existing_database.py
│ ├── migrate_to_client_model.py
│ ├── migration_019_kanban_columns.py
│ ├── script.py.mako
│ ├── test_migration_system.py
│ └── versions/
│ ├── 001_initial_schema.py
│ ├── 002_add_user_full_name.py
│ ├── 003_add_user_theme_preference.py
│ ├── 004_add_task_activities_table.py
│ ├── 005_add_missing_columns.py
│ ├── 006_add_logo_and_task_timestamps.py
│ ├── 007_add_invoice_and_more_settings_columns.py
│ ├── 008_align_invoices_and_settings_more.py
│ ├── 009_add_invoice_created_by.py
│ ├── 010_enforce_single_active_timer.py
│ ├── 011_add_user_preferred_language.py
│ ├── 012_add_pdf_template_fields.py
│ ├── 013_add_comments_table.py
│ ├── 014_add_payment_tracking.py
│ ├── 015_add_user_oidc_fields.py
│ ├── 016_add_focus_recurring_rates_filters_and_project_budget.py
│ ├── 017_reporting_invoicing_extensions.py
│ ├── 018_add_project_costs_table.py
│ ├── 019_add_kanban_columns_table.py
│ ├── 020_add_user_avatar.py
│ ├── 021_add_extra_goods_table.py
│ ├── 022_add_project_code_field.py
│ ├── 023_add_user_favorite_projects.py
│ ├── 024_add_client_notes_table.py
│ ├── 026_add_project_archiving_metadata.py
│ ├── 027_add_user_time_rounding_preferences.py
│ ├── 028_add_weekly_time_goals.py
│ ├── 029_add_expenses_table.py
│ ├── 030_add_permission_system.py
│ ├── 031_add_standard_hours_per_day.py
│ ├── 032_add_api_tokens.py
│ ├── 033_add_email_settings.py
│ ├── 034_add_calendar_events_table.py
│ ├── 035_enhance_payments_table.py
│ ├── 036_add_pdf_design_json.py
│ ├── 037_advanced_expenses.py
│ ├── 038_fix_advanced_expenses_schema.py
│ ├── 039_add_budget_alerts_table.py
│ ├── 040_add_import_export_tables.py
│ ├── 041_add_invoice_pdf_templates_table.py
│ ├── 042_client_prepaid_hours.py
│ ├── 043_add_project_id_to_kanban_columns.py
│ ├── 044_add_audit_logs_table.py
│ ├── 045_add_recurring_invoices_and_email_tracking.py
│ ├── 046_add_webhooks_system.py
│ ├── 047_add_client_portal_fields.py
│ ├── 048_add_client_portal_credentials.py
│ ├── 049_add_client_password_setup_token.py
│ ├── 050_add_offers_table.py
│ ├── 051_rename_offers_to_quotes_and_add_features.py
│ ├── 052_add_quote_discount_fields.py
│ ├── 053_add_quote_payment_terms.py
│ ├── 054_add_quote_comments.py
│ ├── 055_add_quote_attachments.py
│ ├── 056_add_quote_approval_workflow.py
│ ├── 057_add_quote_templates.py
│ ├── 058_add_quote_versions.py
│ ├── 059_add_inventory_management.py
│ ├── 060_add_supplier_management.py
│ ├── 061_add_purchase_orders.py
│ ├── 062_add_performance_indexes.py
│ ├── 063_add_crm_features.py
│ ├── 064_add_kiosk_mode_settings.py
│ ├── 065_add_new_features.py
│ ├── 066_add_integration_framework.py
│ ├── 067_add_integration_credentials.py
│ ├── 067b_alias_067_add_integration_credentials.py
│ ├── 068_add_user_password_hash.py
│ ├── 069_add_workflow_automation.py
│ ├── 070_add_time_entry_approvals.py
│ ├── 071_add_recurring_tasks.py
│ ├── 072_add_client_portal_customization_and_team_chat.py
│ ├── 073_add_ai_features_and_gps_tracking.py
│ ├── 074_add_password_change_required.py
│ ├── 075_add_client_custom_fields_and_link_templates.py
│ ├── 076_add_client_billing_to_time_entries.py
│ ├── 077_add_ui_feature_flags.py
│ ├── 078_add_system_ui_feature_flags.py
│ ├── 079_rename_user_badges_metadata_column.py
│ ├── 080_fix_metadata_column_names.py
│ ├── 081_add_all_integration_credentials.py
│ ├── 082_add_global_integrations.py
│ ├── 083_add_paid_status_to_time_entries.py
│ ├── 084_add_custom_field_definitions.py
│ ├── 085_add_project_custom_fields.py
│ ├── 086_add_project_and_client_attachments.py
│ ├── 087_add_salesman_email_mapping.py
│ ├── 088_add_salesman_splitting_to_reports.py
│ ├── 089_allow_auto_entries_without_project_or_client.py
│ ├── 089_fix_roles_permissions_sequences.py
│ ├── 090_add_push_subscriptions_table.py
│ ├── 090_enhance_report_builder_iteration.py
│ ├── 092_add_missing_module_visibility_flags.py
│ ├── 093_remove_ui_allow_module_flags.py
│ ├── 094_add_donation_interactions.py
│ ├── 095_add_missing_ui_show_issues.py
│ ├── 096_add_missing_portal_issues_enabled.py
│ ├── 097_add_stock_lots_for_devaluation.py
│ ├── 098_add_invoice_peppol_transmissions.py
│ ├── 099_add_peppol_settings_columns.py
│ ├── 100_add_comment_attachments.py
│ ├── 100_add_gantt_colors_and_modules_disabled.py
│ ├── 101_add_issues_table.py
│ ├── 102_add_missing_quotes_template_id.py
│ ├── 103_add_missing_quotes_quote_number.py
│ ├── 104_add_missing_quotes_columns.py
│ ├── 105_fix_client_notifications_cascade_delete.py
│ ├── 106_add_reportlab_template_json.py
│ ├── 107_increase_invoice_prefix_length.py
│ ├── 108_add_decorative_images.py
│ ├── 109_add_pdf_template_date_format.py
│ ├── 110_add_disabled_module_ids.py
│ ├── 111_add_use_last_month_dates.py
│ ├── 112_add_invoices_peppol_compliant.py
│ ├── 113_add_invoice_buyer_reference.py
│ ├── 114_enhance_audit_logs_for_timeentry.py
│ ├── 115_add_exclude_weekends_to_weekly_goals.py
│ ├── 116_merge_three_heads.py
│ ├── 117_add_user_calendar_type_colors.py
│ ├── 118_add_locked_client_id.py
│ ├── 118_add_role_hidden_module_ids.py
│ ├── 119_add_settings_date_time_format.py
│ ├── 120_user_nullable_date_time_format.py
│ ├── 121_add_ui_show_donate_and_system_instance_id.py
│ ├── 122_add_settings_donate_ui_hidden.py
│ ├── 123_add_calendar_default_view.py
│ ├── 124_add_time_entry_requirements.py
│ ├── 125_add_default_daily_working_hours.py
│ ├── 126_add_overtime_include_weekends_to_users.py
│ ├── 127_add_user_clients_table.py
│ ├── 128_add_invoices_zugferd_pdf.py
│ ├── 129_add_task_tags.py
│ ├── 129_merge_118_128_heads.py
│ ├── 130_add_peppol_transport_mode_and_native.py
│ ├── 131_add_donation_interaction_variant.py
│ ├── 132_add_timesheet_governance_and_time_off.py
│ ├── 133_merge_132_and_129_task_tags_heads.py
│ ├── 134_add_overtime_weekly_mode.py
│ ├── 135_add_remind_to_log_settings.py
│ ├── 136_seed_overtime_leave_type.py
│ ├── 137_add_break_time_to_time_entries.py
│ ├── 138_add_default_break_rules_settings.py
│ ├── 139_add_keyboard_shortcuts_overrides.py
│ ├── 140_add_client_portal_dashboard_preferences.py
│ ├── 141_add_invoice_number_pattern.py
│ ├── 142_add_mail_test_recipient.py
│ ├── 143_add_task_custom_fields.py
│ ├── 144_api_idempotency_keys.py
│ ├── 145_add_quotes_requires_approval_columns.py
│ ├── 146_add_quote_item_position.py
│ ├── 147_add_quote_item_line_kind.py
│ ├── 148_add_user_dismissed_release_version.py
│ ├── 149_add_user_support_stats_reports_generated.py
│ ├── 150_add_smart_notifications.py
│ ├── 151_add_ai_helper_settings.py
│ ├── 152_add_user_totp_2fa.py
│ ├── 153_add_user_auth_provider.py
│ ├── 20250127_000001_add_client_features.py
│ ├── 20251220_000001_add_integration_external_event_links.py
│ └── add_quick_wins_features.py
├── mobile/
│ ├── README.md
│ ├── android/
│ │ ├── app/
│ │ │ ├── build.gradle
│ │ │ └── src/
│ │ │ └── main/
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java/
│ │ │ │ └── io/
│ │ │ │ └── flutter/
│ │ │ │ └── plugins/
│ │ │ │ └── GeneratedPluginRegistrant.java
│ │ │ └── kotlin/
│ │ │ └── com/
│ │ │ └── timetracker/
│ │ │ └── mobile/
│ │ │ └── MainActivity.kt
│ │ ├── build.gradle
│ │ ├── gradle/
│ │ │ └── wrapper/
│ │ │ ├── gradle-wrapper.jar
│ │ │ └── gradle-wrapper.properties
│ │ ├── gradle.properties
│ │ ├── gradlew
│ │ ├── gradlew.bat
│ │ ├── local.properties
│ │ └── settings.gradle
│ ├── assets/
│ │ ├── .gitkeep
│ │ └── icon/
│ │ └── README.md
│ ├── flutter_launcher_icons_ios.yaml
│ ├── ios/
│ │ ├── Flutter/
│ │ │ ├── Generated.xcconfig
│ │ │ ├── ephemeral/
│ │ │ │ ├── flutter_lldb_helper.py
│ │ │ │ └── flutter_lldbinit
│ │ │ └── flutter_export_environment.sh
│ │ ├── Podfile
│ │ └── Runner/
│ │ ├── Assets.xcassets/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── Contents.json
│ │ ├── GeneratedPluginRegistrant.h
│ │ ├── GeneratedPluginRegistrant.m
│ │ └── Info.plist
│ ├── lib/
│ │ ├── core/
│ │ │ ├── config/
│ │ │ │ └── app_config.dart
│ │ │ ├── constants/
│ │ │ │ └── app_constants.dart
│ │ │ ├── telemetry/
│ │ │ │ └── mobile_otel.dart
│ │ │ └── theme/
│ │ │ ├── app_theme.dart
│ │ │ └── app_tokens.dart
│ │ ├── data/
│ │ │ ├── api/
│ │ │ │ └── api_client.dart
│ │ │ ├── models/
│ │ │ │ ├── project.dart
│ │ │ │ ├── task.dart
│ │ │ │ ├── time_entry.dart
│ │ │ │ ├── time_entry_requirements.dart
│ │ │ │ ├── timer.dart
│ │ │ │ └── user_prefs.dart
│ │ │ └── storage/
│ │ │ ├── local_storage.dart
│ │ │ └── sync_service.dart
│ │ ├── domain/
│ │ │ ├── repositories/
│ │ │ │ └── time_tracking_repository.dart
│ │ │ └── usecases/
│ │ │ └── sync_usecase.dart
│ │ ├── main.dart
│ │ ├── presentation/
│ │ │ ├── providers/
│ │ │ │ ├── api_provider.dart
│ │ │ │ ├── finance_workforce_providers.dart
│ │ │ │ ├── projects_provider.dart
│ │ │ │ ├── tasks_provider.dart
│ │ │ │ ├── theme_mode_provider.dart
│ │ │ │ ├── time_entries_provider.dart
│ │ │ │ ├── time_entry_requirements_provider.dart
│ │ │ │ ├── timer_provider.dart
│ │ │ │ └── user_prefs_provider.dart
│ │ │ ├── screens/
│ │ │ │ ├── finance_workforce_screen.dart
│ │ │ │ ├── home_screen.dart
│ │ │ │ ├── login_screen.dart
│ │ │ │ ├── projects_screen.dart
│ │ │ │ ├── settings_screen.dart
│ │ │ │ ├── splash_screen.dart
│ │ │ │ ├── time_entries_screen.dart
│ │ │ │ ├── time_entry_form_screen.dart
│ │ │ │ └── timer_screen.dart
│ │ │ └── widgets/
│ │ │ ├── empty_state.dart
│ │ │ ├── error_view.dart
│ │ │ ├── start_timer_sheet.dart
│ │ │ ├── time_entry_card.dart
│ │ │ └── timer_widget.dart
│ │ └── utils/
│ │ ├── auth/
│ │ │ └── auth_service.dart
│ │ ├── date_format_utils.dart
│ │ ├── network/
│ │ │ ├── connection_diagnostics.dart
│ │ │ ├── connection_diagnostics_io.dart
│ │ │ └── connection_diagnostics_stub.dart
│ │ └── ssl/
│ │ ├── certificate_error.dart
│ │ ├── certificate_error_io.dart
│ │ ├── certificate_error_stub.dart
│ │ └── ssl_utils.dart
│ ├── pubspec.yaml
│ └── test/
│ ├── api_client_test.dart
│ ├── models_test.dart
│ └── widget_test.dart
├── nginx/
│ └── conf.d/
│ ├── example-public-domain.conf
│ └── https.conf
├── package.json
├── postcss.config.js
├── prometheus/
│ └── prometheus.yml
├── promtail/
│ └── promtail-config.yml
├── pyproject.toml
├── pytest.ini
├── render.yaml
├── requirements-dev.txt
├── requirements-test.txt
├── requirements.txt
├── scripts/
│ ├── README-BUILD.md
│ ├── apply_migration.py
│ ├── audit_i18n.py
│ ├── audit_migrations_portability.py
│ ├── build-all.bat
│ ├── build-all.sh
│ ├── build-desktop-no-sign.bat
│ ├── build-desktop-no-sign.sh
│ ├── build-desktop-simple.bat
│ ├── build-desktop-windows.bat
│ ├── build-desktop-windows.ps1
│ ├── build-desktop.bat
│ ├── build-desktop.sh
│ ├── build-mobile.bat
│ ├── build-mobile.sh
│ ├── check-desktop-assets.sh
│ ├── check_audit_logging.py
│ ├── check_audit_logs.py
│ ├── check_routes.py
│ ├── clear-all-electron-cache.bat
│ ├── clear-all-electron-cache.sh
│ ├── clear-electron-builder-cache.bat
│ ├── clear-electron-builder-cache.sh
│ ├── complete_all_translations.py
│ ├── complete_dutch_translations.py
│ ├── complete_nl_translations.py
│ ├── complete_spanish_translations.py
│ ├── complete_spanish_translations_final.py
│ ├── deploy-public.bat
│ ├── deploy-public.sh
│ ├── extract_translations.py
│ ├── fill_po_argos.py
│ ├── fix-desktop-build.bat
│ ├── fix-desktop-build.sh
│ ├── fix-onedrive-lock.ps1
│ ├── fix-windows-build.bat
│ ├── fix-windows-build.sh
│ ├── fix_missing_columns.py
│ ├── fix_quotes_template_id.py
│ ├── fix_translation_placeholders.py
│ ├── generate-certs.sh
│ ├── generate-changelog.py
│ ├── generate-icons.js
│ ├── generate-macos-icon.sh
│ ├── generate-mobile-icon.bat
│ ├── generate-mobile-icon.py
│ ├── generate-mobile-icon.sh
│ ├── generate_pwa_icons.py
│ ├── prepare-desktop-assets.sh
│ ├── quick_test_summary.py
│ ├── reset-dev-db.bat
│ ├── reset-dev-db.py
│ ├── reset-dev-db.sh
│ ├── run-tests.bat
│ ├── run-tests.sh
│ ├── run_model_tests.py
│ ├── run_tests.sh
│ ├── run_tests_individually.py
│ ├── run_tests_script.py
│ ├── sanitize_po_format_strings.py
│ ├── seed-dev-data.py
│ ├── setup-dev-analytics.bat
│ ├── setup-dev-analytics.sh
│ ├── setup-https-mkcert.bat
│ ├── setup-https-mkcert.sh
│ ├── setup-migrations.bat
│ ├── setup-migrations.sh
│ ├── start-https.bat
│ ├── start-https.sh
│ ├── start-local-test.bat
│ ├── start-local-test.ps1
│ ├── start-local-test.sh
│ ├── sync-desktop-version.py
│ ├── sync-mobile-version.py
│ ├── sync_translations.py
│ ├── test-build-desktop.bat
│ ├── test-docker-network.bat
│ ├── test-docker-network.sh
│ ├── test_audit_routes.py
│ ├── translate_all_spanish.py
│ ├── translate_dutch.py
│ ├── translate_spanish.py
│ ├── validate-setup.bat
│ ├── validate-setup.py
│ ├── validate-setup.sh
│ ├── verify-desktop-setup.sh
│ ├── verify_and_fix_schema.py
│ ├── verify_audit_setup.py
│ ├── verify_csrf_config.bat
│ ├── verify_csrf_config.sh
│ ├── version-manager.bat
│ ├── version-manager.ps1
│ ├── version-manager.py
│ └── version-manager.sh
├── setup.py
├── tailwind.config.js
├── tests/
│ ├── conftest.py
│ ├── factories.py
│ ├── models/
│ │ └── test_import_export_models.py
│ ├── smoke_test_email.py
│ ├── smoke_test_prepaid_hours.py
│ ├── smoke_test_project_dashboard.py
│ ├── smoke_test_user_settings.py
│ ├── test_activity_feed.py
│ ├── test_admin_dashboard_charts.py
│ ├── test_admin_email_routes.py
│ ├── test_admin_settings_logo.py
│ ├── test_admin_users.py
│ ├── test_analytics.py
│ ├── test_api_audit_activities_v1.py
│ ├── test_api_budget_alerts_v1.py
│ ├── test_api_calendar_v1.py
│ ├── test_api_client_notes_v1.py
│ ├── test_api_comments_v1.py
│ ├── test_api_comprehensive.py
│ ├── test_api_contract.py
│ ├── test_api_credit_notes_v1.py
│ ├── test_api_deprecation_headers.py
│ ├── test_api_expenses_v1.py
│ ├── test_api_favorites_v1.py
│ ├── test_api_invoice_templates_api_v1.py
│ ├── test_api_invoice_templates_v1.py
│ ├── test_api_invoices_v1.py
│ ├── test_api_kanban_v1.py
│ ├── test_api_mileage_v1.py
│ ├── test_api_payments_v1.py
│ ├── test_api_per_diem_v1.py
│ ├── test_api_project_costs_v1.py
│ ├── test_api_purchase_orders_v1.py
│ ├── test_api_recurring_invoices_v1.py
│ ├── test_api_route_contract.py
│ ├── test_api_saved_filters_v1.py
│ ├── test_api_tax_currency_v1.py
│ ├── test_api_time_entry_templates_v1.py
│ ├── test_api_v1.py
│ ├── test_api_v1_inventory_movements.py
│ ├── test_audit_log_model.py
│ ├── test_audit_log_routes.py
│ ├── test_audit_logging.py
│ ├── test_audit_trail_smoke.py
│ ├── test_basic.py
│ ├── test_budget_alert_model.py
│ ├── test_budget_alerts_smoke.py
│ ├── test_budget_forecasting.py
│ ├── test_bulk_task_operations.py
│ ├── test_calendar_event_model.py
│ ├── test_calendar_routes.py
│ ├── test_cii_invoice.py
│ ├── test_client_note_model.py
│ ├── test_client_notes_routes.py
│ ├── test_client_portal.py
│ ├── test_client_prepaid_model.py
│ ├── test_client_single_simplification.py
│ ├── test_comprehensive_tracking.py
│ ├── test_config_priority.py
│ ├── test_currency_display.py
│ ├── test_custom_field_definitions.py
│ ├── test_delete_actions.py
│ ├── test_demo_mode_and_safe_templates.py
│ ├── test_email.py
│ ├── test_enhanced_ui.py
│ ├── test_error_handling.py
│ ├── test_excel_export.py
│ ├── test_expenses.py
│ ├── test_extra_good_model.py
│ ├── test_factories_smoke.py
│ ├── test_favorite_projects.py
│ ├── test_i18n.py
│ ├── test_import_export.py
│ ├── test_installation_config.py
│ ├── test_integration/
│ │ ├── test_activitywatch_integration.py
│ │ ├── test_caldav_integration.py
│ │ ├── test_inventory_integration.py
│ │ └── test_jira_integration.py
│ ├── test_invoice_currency_fix.py
│ ├── test_invoice_currency_smoke.py
│ ├── test_invoice_email.py
│ ├── test_invoice_expenses.py
│ ├── test_invoice_numbering.py
│ ├── test_invoice_pdf_postprocess.py
│ ├── test_invoice_validators.py
│ ├── test_invoices.py
│ ├── test_keyboard_shortcuts.py
│ ├── test_keyboard_shortcuts_api.py
│ ├── test_keyboard_shortcuts_input_fix.py
│ ├── test_ldap_auth.py
│ ├── test_ldap_setup_wizard.py
│ ├── test_logo_pdf.py
│ ├── test_models/
│ │ ├── test_expense_category.py
│ │ ├── test_inventory_models.py
│ │ ├── test_mileage.py
│ │ ├── test_per_diem.py
│ │ ├── test_purchase_order.py
│ │ ├── test_supplier.py
│ │ └── test_webhook.py
│ ├── test_models_comprehensive.py
│ ├── test_models_extended.py
│ ├── test_multiselect_filters.py
│ ├── test_new_features.py
│ ├── test_oidc_logout.py
│ ├── test_oidc_session_cookie_bloat.py
│ ├── test_onboarding.py
│ ├── test_otel_integration.py
│ ├── test_overtime.py
│ ├── test_overtime_leave.py
│ ├── test_overtime_smoke.py
│ ├── test_payment_model.py
│ ├── test_payment_routes.py
│ ├── test_payment_smoke.py
│ ├── test_pdf_layout.py
│ ├── test_pdfa3.py
│ ├── test_peppol_identifiers.py
│ ├── test_peppol_service.py
│ ├── test_permissions.py
│ ├── test_permissions_routes.py
│ ├── test_prepaid_allocator.py
│ ├── test_profile_avatar.py
│ ├── test_project_archiving.py
│ ├── test_project_archiving_models.py
│ ├── test_project_costs.py
│ ├── test_project_dashboard.py
│ ├── test_project_inactive_status.py
│ ├── test_quick_wins.py
│ ├── test_reports_task_report.py
│ ├── test_repositories/
│ │ ├── __init__.py
│ │ ├── test_base_repository.py
│ │ └── test_time_entry_repository.py
│ ├── test_role_module_visibility.py
│ ├── test_routes/
│ │ ├── test_api_search.py
│ │ ├── test_api_smart_notifications.py
│ │ ├── test_api_v1_calendar_templates_refactored.py
│ │ ├── test_api_v1_expenses_complete.py
│ │ ├── test_api_v1_inventory_reports.py
│ │ ├── test_api_v1_inventory_transfers.py
│ │ ├── test_api_v1_invoices_tasks_expenses_refactored.py
│ │ ├── test_api_v1_mileage_refactored.py
│ │ ├── test_api_v1_payments_refactored.py
│ │ ├── test_api_v1_projects_refactored.py
│ │ ├── test_api_v1_quotes_refactored.py
│ │ ├── test_api_v1_recurring_invoices_credit_notes.py
│ │ ├── test_api_v1_reports_refactored.py
│ │ ├── test_api_v1_time_entries_complete.py
│ │ ├── test_api_v1_time_entries_refactored.py
│ │ ├── test_api_version_check.py
│ │ ├── test_auth.py
│ │ ├── test_inventory_routes.py
│ │ ├── test_main_dashboard_cached.py
│ │ ├── test_purchase_order_routes.py
│ │ ├── test_quotes_web.py
│ │ ├── test_reports_scope.py
│ │ ├── test_supplier_routes.py
│ │ └── test_timer_scope.py
│ ├── test_routes.py
│ ├── test_security.py
│ ├── test_service_worker.py
│ ├── test_services/
│ │ ├── __init__.py
│ │ ├── test_api_token_service.py
│ │ ├── test_comment_service.py
│ │ ├── test_export_service.py
│ │ ├── test_invoice_service.py
│ │ ├── test_notification_service.py
│ │ ├── test_payment_service.py
│ │ ├── test_payment_service_complete.py
│ │ ├── test_project_service.py
│ │ ├── test_recurring_invoice_service.py
│ │ ├── test_reporting_service.py
│ │ ├── test_stats_service.py
│ │ ├── test_task_service.py
│ │ ├── test_time_entry_bulk_service.py
│ │ ├── test_time_tracking_service.py
│ │ ├── test_time_tracking_service_complete.py
│ │ └── test_version_service.py
│ ├── test_silent_exception_fixes.py
│ ├── test_single_active_timer_setting.py
│ ├── test_support_services.py
│ ├── test_system_ui_flags.py
│ ├── test_task_edit_project.py
│ ├── test_tasks_filters_ui.py
│ ├── test_tasks_templates.py
│ ├── test_telemetry.py
│ ├── test_telemetry_consent_and_base.py
│ ├── test_time_entry_duplication.py
│ ├── test_time_entry_freeze.py
│ ├── test_time_entry_resume.py
│ ├── test_time_entry_templates.py
│ ├── test_time_rounding.py
│ ├── test_time_rounding_param.py
│ ├── test_timer_edit_own_time_entries.py
│ ├── test_timezone.py
│ ├── test_ui_quick_wins.py
│ ├── test_uploads_persistence.py
│ ├── test_user_report_entries_export_excel.py
│ ├── test_user_settings.py
│ ├── test_utils/
│ │ ├── test_api_auth_enhanced.py
│ │ ├── test_cache.py
│ │ ├── test_integration_sync_context.py
│ │ ├── test_scope_filter.py
│ │ ├── test_version_compare.py
│ │ └── test_webhook_service.py
│ ├── test_utils.py
│ ├── test_version_reading.py
│ ├── test_weekly_goals.py
│ └── test_zugferd.py
└── translations/
├── .keep
├── ar/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── de/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── en/
│ └── LC_MESSAGES/
│ └── messages.po
├── es/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── fi/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── fr/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── he/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── it/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── nb/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── nl/
│ └── LC_MESSAGES/
│ ├── .keep
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
├── no/
│ └── LC_MESSAGES/
│ ├── messages.po
│ ├── messages.po.bak
│ └── messages.po.bak2
└── pt/
└── LC_MESSAGES/
└── messages.po
Showing preview only (833K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (9292 symbols across 940 files)
FILE: app.py
function make_shell_context (line 13) | def make_shell_context():
function init_db (line 28) | def init_db():
function create_admin (line 53) | def create_admin():
FILE: app/__init__.py
function log_event (line 61) | def log_event(name: str, **kwargs):
function identify_user (line 77) | def identify_user(user_id, properties=None):
function track_event (line 90) | def track_event(user_id, event_name, properties=None):
function track_page_view (line 103) | def track_page_view(page_name, user_id=None, properties=None):
function create_app (line 127) | def create_app(config=None):
function init_database (line 1338) | def init_database(app):
FILE: app/blueprint_registry.py
function _is_dev_fail_fast (line 11) | def _is_dev_fail_fast(app):
function _record_blueprint_status (line 16) | def _record_blueprint_status(
function register_all_blueprints (line 47) | def register_all_blueprints(app, logger=None):
function _register_optional_blueprints (line 211) | def _register_optional_blueprints(app, logger=None):
FILE: app/config.py
class Config (line 5) | class Config:
class DevelopmentConfig (line 301) | class DevelopmentConfig(Config):
class TestingConfig (line 314) | class TestingConfig(Config):
method __init__ (line 325) | def __init__(self):
class ProductionConfig (line 331) | class ProductionConfig(Config):
method __init__ (line 342) | def __init__(self):
FILE: app/config/__init__.py
class Config (line 40) | class Config:
FILE: app/config/analytics_defaults.py
function get_version_from_setup (line 33) | def get_version_from_setup():
function get_analytics_config (line 118) | def get_analytics_config():
function has_analytics_configured (line 165) | def has_analytics_configured():
FILE: app/config/support_ui.py
function get_support_portal_base (line 9) | def get_support_portal_base(config: Dict[str, Any] | Any) -> str:
function build_support_checkout_urls (line 20) | def build_support_checkout_urls(config: Dict[str, Any] | Any) -> Dict[st...
function get_long_session_minutes (line 45) | def get_long_session_minutes() -> int:
function get_social_proof_text (line 52) | def get_social_proof_text(config: Dict[str, Any] | Any) -> str:
FILE: app/constants.py
class TimeEntryStatus (line 9) | class TimeEntryStatus(Enum):
class TimeEntrySource (line 18) | class TimeEntrySource(Enum):
class ProjectStatus (line 28) | class ProjectStatus(Enum):
class InvoiceStatus (line 36) | class InvoiceStatus(Enum):
class PaymentStatus (line 49) | class PaymentStatus(Enum):
class TaskStatus (line 58) | class TaskStatus(Enum):
class UserRole (line 68) | class UserRole(Enum):
class BillableStatus (line 77) | class BillableStatus(Enum):
class AuditAction (line 138) | class AuditAction(Enum):
class WebhookEvent (line 154) | class WebhookEvent(Enum):
class NotificationType (line 189) | class NotificationType(Enum):
class CacheKey (line 199) | class CacheKey:
FILE: app/integrations/activitywatch.py
function _to_local_naive (line 30) | def _to_local_naive(dt: datetime) -> datetime:
function _normalize_server_url (line 38) | def _normalize_server_url(url: str) -> str:
class ActivityWatchConnector (line 45) | class ActivityWatchConnector(BaseConnector):
method provider_name (line 53) | def provider_name(self) -> str:
method get_authorization_url (line 57) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 60) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 63) | def refresh_access_token(self) -> Dict[str, Any]:
method _get_server_url (line 67) | def _get_server_url(self) -> str:
method _get (line 74) | def _get(self, path: str, params: Optional[Dict[str, Any]] = None) -> ...
method test_connection (line 91) | def test_connection(self) -> Dict[str, Any]:
method sync_data (line 101) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method get_config_schema (line 328) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/asana.py
class AsanaConnector (line 15) | class AsanaConnector(BaseConnector):
method provider_name (line 25) | def provider_name(self) -> str:
method get_authorization_url (line 28) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 46) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 107) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 148) | def test_connection(self) -> Dict[str, Any]:
method sync_data (line 162) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method get_config_schema (line 291) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/base.py
class BaseConnector (line 10) | class BaseConnector(ABC):
method __init__ (line 18) | def __init__(self, integration, credentials):
method provider_name (line 31) | def provider_name(self) -> str:
method display_name (line 37) | def display_name(self) -> str:
method get_authorization_url (line 42) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 56) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 70) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 80) | def test_connection(self) -> Dict[str, Any]:
method get_access_token (line 89) | def get_access_token(self) -> Optional[str]:
method sync_data (line 110) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method handle_webhook (line 123) | def handle_webhook(
method get_config_schema (line 140) | def get_config_schema(self) -> Dict[str, Any]:
method validate_config (line 191) | def validate_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
method get_sync_settings (line 260) | def get_sync_settings(self) -> Dict[str, Any]:
method get_field_mappings (line 285) | def get_field_mappings(self) -> Dict[str, str]:
method get_status_mappings (line 296) | def get_status_mappings(self) -> Dict[str, str]:
FILE: app/integrations/caldav_calendar.py
function _ns (line 39) | def _ns(tag: str, ns: str) -> str:
function _ensure_trailing_slash (line 43) | def _ensure_trailing_slash(u: str) -> str:
function _to_local_naive (line 53) | def _to_local_naive(dt: datetime) -> datetime:
function _to_utc_aware (line 64) | def _to_utc_aware(dt_local_naive: datetime) -> datetime:
function _datetime_to_caldav_utc (line 71) | def _datetime_to_caldav_utc(dt_utc: datetime) -> str:
class CalDAVCalendar (line 82) | class CalDAVCalendar:
class CalDAVClient (line 87) | class CalDAVClient:
method __init__ (line 92) | def __init__(self, username: str, password: str, verify_ssl: bool = Tr...
method _request (line 98) | def _request(self, method: str, url: str, *, headers: Optional[Dict[st...
method _propfind (line 129) | def _propfind(self, url: str, xml_body: str, depth: str = "0") -> ET.E...
method _report (line 144) | def _report(self, url: str, xml_body: str, depth: str = "1") -> ET.Ele...
method discover_calendars (line 160) | def discover_calendars(self, server_url: str) -> List[CalDAVCalendar]:
method fetch_events (line 246) | def fetch_events(self, calendar_url: str, time_min_utc: datetime, time...
method create_or_update_event (line 421) | def create_or_update_event(
method _find_href (line 554) | def _find_href(self, root: ET.Element, prop_paths: List[Tuple[str, ......
class CalDAVCalendarConnector (line 577) | class CalDAVCalendarConnector(BaseConnector):
method provider_name (line 585) | def provider_name(self) -> str:
method get_authorization_url (line 589) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 592) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 595) | def refresh_access_token(self) -> Dict[str, Any]:
method _get_basic_creds (line 599) | def _get_basic_creds(self) -> Tuple[str, str]:
method _client (line 608) | def _client(self) -> CalDAVClient:
method test_connection (line 614) | def test_connection(self) -> Dict[str, Any]:
method sync_data (line 671) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method _sync_calendar_to_time_tracker (line 803) | def _sync_calendar_to_time_tracker(
method _sync_time_tracker_to_calendar (line 1025) | def _sync_time_tracker_to_calendar(self, cfg: Dict[str, Any], calendar...
method _generate_icalendar_event (line 1459) | def _generate_icalendar_event(
method get_config_schema (line 1493) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/github.py
class GitHubConnector (line 17) | class GitHubConnector(BaseConnector):
method provider_name (line 25) | def provider_name(self) -> str:
method get_authorization_url (line 28) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 46) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 104) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 119) | def test_connection(self) -> Dict[str, Any]:
method sync_data (line 140) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method handle_webhook (line 378) | def handle_webhook(
method get_config_schema (line 477) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/gitlab.py
class GitLabConnector (line 15) | class GitLabConnector(BaseConnector):
method provider_name (line 23) | def provider_name(self) -> str:
method _get_base_url (line 26) | def _get_base_url(self) -> str:
method get_authorization_url (line 35) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 61) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 125) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 172) | def test_connection(self) -> Dict[str, Any]:
method sync_data (line 192) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method get_config_schema (line 345) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/google_calendar.py
class GoogleCalendarConnector (line 20) | class GoogleCalendarConnector(BaseConnector):
method provider_name (line 31) | def provider_name(self) -> str:
method get_authorization_url (line 34) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 72) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 131) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 169) | def test_connection(self) -> Dict[str, Any]:
method _get_calendar_service (line 197) | def _get_calendar_service(self):
method sync_data (line 234) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method _create_calendar_event (line 660) | def _create_calendar_event(self, service, calendar_id: str, time_entry...
method _update_calendar_event (line 712) | def _update_calendar_event(self, service, calendar_id: str, event_id: ...
method _create_calendar_event_from_event (line 764) | def _create_calendar_event_from_event(self, service, calendar_id: str,...
method _update_calendar_event_from_event (line 817) | def _update_calendar_event_from_event(self, service, calendar_id: str,...
method get_config_schema (line 870) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/jira.py
class JiraConnector (line 24) | class JiraConnector(BaseConnector):
method provider_name (line 32) | def provider_name(self) -> str:
method get_authorization_url (line 35) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 60) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 102) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 140) | def test_connection(self) -> Dict[str, Any]:
method _extract_description_text (line 160) | def _extract_description_text(self, issue_fields: Dict[str, Any]) -> O...
method _upsert_task_from_issue (line 175) | def _upsert_task_from_issue(self, issue: Dict[str, Any], actor_id: int...
method sync_data (line 249) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method sync_issue (line 309) | def sync_issue(self, issue_key: str) -> Dict[str, Any]:
method _map_jira_status (line 384) | def _map_jira_status(self, jira_status: str) -> str:
method handle_webhook (line 400) | def handle_webhook(
method get_config_schema (line 528) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/linear.py
class LinearConnector (line 20) | class LinearConnector(BaseConnector):
method provider_name (line 28) | def provider_name(self) -> str:
method get_authorization_url (line 31) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 34) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 37) | def refresh_access_token(self) -> Dict[str, Any]:
method _api_key (line 40) | def _api_key(self) -> Optional[str]:
method _graphql (line 45) | def _graphql(self, query: str, variables: Optional[Dict] = None) -> Di...
method test_connection (line 64) | def test_connection(self) -> Dict[str, Any]:
method sync_data (line 73) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method get_config_schema (line 235) | def get_config_schema(cls) -> Dict[str, Any]:
FILE: app/integrations/microsoft_teams.py
class MicrosoftTeamsConnector (line 15) | class MicrosoftTeamsConnector(BaseConnector):
method provider_name (line 30) | def provider_name(self) -> str:
method _get_tenant_id (line 33) | def _get_tenant_id(self) -> str:
method get_authorization_url (line 42) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 69) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 133) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 184) | def test_connection(self) -> Dict[str, Any]:
method send_message (line 205) | def send_message(self, channel_id: str, message: str) -> Dict[str, Any]:
method sync_data (line 226) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method get_config_schema (line 250) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/outlook_calendar.py
class OutlookCalendarConnector (line 15) | class OutlookCalendarConnector(BaseConnector):
method provider_name (line 30) | def provider_name(self) -> str:
method _get_tenant_id (line 33) | def _get_tenant_id(self) -> str:
method get_authorization_url (line 42) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 69) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 134) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 185) | def test_connection(self) -> Dict[str, Any]:
method sync_data (line 203) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method _create_calendar_event (line 265) | def _create_calendar_event(self, token: str, calendar_id: str, time_en...
method _update_calendar_event (line 309) | def _update_calendar_event(self, token: str, calendar_id: str, event_i...
method get_config_schema (line 350) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/peppol.py
function _bool_env (line 31) | def _bool_env(name: str, default: bool = False) -> bool:
function peppol_enabled (line 36) | def peppol_enabled() -> bool:
class PeppolParty (line 60) | class PeppolParty:
function _money (line 73) | def _money(v: Any) -> str:
function _qty (line 82) | def _qty(v: Any) -> str:
function _text (line 90) | def _text(parent: ET.Element, tag: str, text: Optional[str]) -> Optional...
function _party (line 101) | def _party(parent: ET.Element, kind: str, party: PeppolParty) -> None:
function build_peppol_ubl_invoice_xml (line 140) | def build_peppol_ubl_invoice_xml(invoice: Any, supplier: PeppolParty, cu...
class PeppolAccessPointError (line 302) | class PeppolAccessPointError(RuntimeError):
function send_ubl_via_access_point (line 306) | def send_ubl_via_access_point(
FILE: app/integrations/peppol_as4.py
class PeppolAS4Error (line 38) | class PeppolAS4Error(RuntimeError):
function _soap_envelope (line 44) | def _soap_envelope(
function build_as4_message (line 97) | def build_as4_message(
function send_as4_message (line 153) | def send_as4_message(
FILE: app/integrations/peppol_identifiers.py
class PeppolIdentifierError (line 184) | class PeppolIdentifierError(ValueError):
method __init__ (line 187) | def __init__(self, message: str, field: Optional[str] = None):
function validate_scheme_id (line 192) | def validate_scheme_id(scheme_id: Optional[str], field: str = "scheme_id...
function validate_endpoint_id (line 210) | def validate_endpoint_id(endpoint_id: Optional[str], field: str = "endpo...
function validate_participant_identifiers (line 228) | def validate_participant_identifiers(
FILE: app/integrations/peppol_smp.py
class PeppolSMPError (line 27) | class PeppolSMPError(RuntimeError):
function _get_sml_base_url (line 33) | def _get_sml_base_url() -> str:
function _participant_identifier_to_hostname (line 42) | def _participant_identifier_to_hostname(participant_id: str, scheme_id: ...
function get_smp_url (line 53) | def get_smp_url(participant_id: str, scheme_id: str, sml_base_url: Optio...
function get_recipient_endpoint_url (line 111) | def get_recipient_endpoint_url(
FILE: app/integrations/peppol_transport.py
class PeppolTransportError (line 21) | class PeppolTransportError(RuntimeError):
class PeppolTransportProtocol (line 27) | class PeppolTransportProtocol(ABC):
method send (line 31) | def send(
class GenericTransport (line 48) | class GenericTransport(PeppolTransportProtocol):
method __init__ (line 51) | def __init__(
method send (line 61) | def send(
class NativePeppolTransport (line 98) | class NativePeppolTransport(PeppolTransportProtocol):
method __init__ (line 101) | def __init__(
method send (line 113) | def send(
FILE: app/integrations/quickbooks.py
class QuickBooksConnector (line 19) | class QuickBooksConnector(BaseConnector):
method provider_name (line 30) | def provider_name(self) -> str:
method get_base_url (line 33) | def get_base_url(self):
method get_authorization_url (line 38) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 66) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 124) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 169) | def test_connection(self) -> Dict[str, Any]:
method _api_request (line 188) | def _api_request(
method sync_data (line 240) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method _create_quickbooks_invoice (line 378) | def _create_quickbooks_invoice(self, invoice, access_token: str, realm...
method _create_quickbooks_expense (line 542) | def _create_quickbooks_expense(self, expense, access_token: str, realm...
method get_config_schema (line 643) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/registry.py
function register_connectors (line 23) | def register_connectors():
FILE: app/integrations/slack.py
class SlackConnector (line 14) | class SlackConnector(BaseConnector):
method provider_name (line 22) | def provider_name(self) -> str:
method get_authorization_url (line 25) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 43) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 87) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 127) | def test_connection(self) -> Dict[str, Any]:
method sync_data (line 148) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method handle_webhook (line 222) | def handle_webhook(
method send_message (line 253) | def send_message(self, channel: str, text: str) -> Dict[str, Any]:
method get_config_schema (line 275) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/trello.py
class TrelloConnector (line 18) | class TrelloConnector(BaseConnector):
method provider_name (line 28) | def provider_name(self) -> str:
method get_authorization_url (line 31) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 56) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 95) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 100) | def test_connection(self) -> Dict[str, Any]:
method sync_data (line 122) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method _sync_trello_to_timetracker (line 183) | def _sync_trello_to_timetracker(
method _sync_timetracker_to_trello (line 285) | def _sync_timetracker_to_trello(
method _map_trello_list_to_status (line 437) | def _map_trello_list_to_status(self, list_id: str) -> str:
method get_config_schema (line 469) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/integrations/xero.py
class XeroConnector (line 19) | class XeroConnector(BaseConnector):
method provider_name (line 29) | def provider_name(self) -> str:
method get_authorization_url (line 32) | def get_authorization_url(self, redirect_uri: str, state: str = None) ...
method exchange_code_for_tokens (line 63) | def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Di...
method refresh_access_token (line 121) | def refresh_access_token(self) -> Dict[str, Any]:
method test_connection (line 167) | def test_connection(self) -> Dict[str, Any]:
method _api_request (line 191) | def _api_request(
method sync_data (line 223) | def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
method _create_xero_invoice (line 285) | def _create_xero_invoice(self, invoice, access_token: str, tenant_id: ...
method _create_xero_expense (line 340) | def _create_xero_expense(self, expense, access_token: str, tenant_id: ...
method get_config_schema (line 373) | def get_config_schema(self) -> Dict[str, Any]:
FILE: app/models/activity.py
class Activity (line 6) | class Activity(db.Model):
method __repr__ (line 47) | def __repr__(self):
method log (line 51) | def log(
method get_recent (line 128) | def get_recent(cls, user_id=None, limit=50, entity_type=None):
method to_dict (line 146) | def to_dict(self):
method get_icon (line 163) | def get_icon(self):
method get_color (line 179) | def get_color(self):
FILE: app/models/api_idempotency_key.py
class ApiIdempotencyKey (line 8) | class ApiIdempotencyKey(db.Model):
FILE: app/models/api_token.py
class ApiToken (line 11) | class ApiToken(db.Model):
method __repr__ (line 42) | def __repr__(self):
method generate_token (line 46) | def generate_token():
method hash_token (line 53) | def hash_token(token):
method create_token (line 60) | def create_token(cls, user_id, name, description="", scopes="", expire...
method verify_token (line 93) | def verify_token(self, plain_token):
method is_valid (line 97) | def is_valid(self):
method has_scope (line 105) | def has_scope(self, required_scope):
method record_usage (line 134) | def record_usage(self, ip_address=None):
method to_dict (line 140) | def to_dict(self, include_token=False):
FILE: app/models/audit_log.py
class AuditLog (line 8) | class AuditLog(db.Model):
method __repr__ (line 80) | def __repr__(self):
method log_change (line 84) | def log_change(
method _encode_value (line 184) | def _encode_value(value):
method _decode_value (line 200) | def _decode_value(value_str):
method get_old_value (line 211) | def get_old_value(self):
method get_new_value (line 215) | def get_new_value(self):
method get_entity_metadata (line 219) | def get_entity_metadata(self):
method get_full_old_state (line 227) | def get_full_old_state(self):
method get_full_new_state (line 231) | def get_full_new_state(self):
method get_for_entity (line 236) | def get_for_entity(cls, entity_type, entity_id, limit=100):
method get_for_user (line 246) | def get_for_user(cls, user_id, limit=100):
method get_recent (line 251) | def get_recent(cls, limit=100, entity_type=None, user_id=None, action=...
method to_dict (line 266) | def to_dict(self):
method get_icon (line 291) | def get_icon(self):
method get_color (line 300) | def get_color(self):
FILE: app/models/budget_alert.py
class BudgetAlert (line 6) | class BudgetAlert(db.Model):
method __init__ (line 33) | def __init__(
method __repr__ (line 44) | def __repr__(self):
method acknowledge (line 47) | def acknowledge(self, user_id):
method to_dict (line 54) | def to_dict(self):
method get_active_alerts (line 73) | def get_active_alerts(cls, project_id=None, acknowledged=False):
method create_alert (line 83) | def create_alert(cls, project_id, alert_type, budget_consumed_percent,...
method _generate_message (line 119) | def _generate_message(alert_type, budget_consumed_percent, budget_amou...
method get_alert_summary (line 129) | def get_alert_summary(cls, project_id=None):
FILE: app/models/calendar_event.py
function _isoformat_calendar (line 7) | def _isoformat_calendar(dt):
class CalendarEvent (line 14) | class CalendarEvent(db.Model):
method __init__ (line 69) | def __init__(self, user_id, title, start_time, end_time, **kwargs):
method __repr__ (line 88) | def __repr__(self):
method to_dict (line 91) | def to_dict(self):
method duration_hours (line 116) | def duration_hours(self):
method get_events_in_range (line 124) | def get_events_in_range(user_id, start_date, end_date, include_tasks=F...
FILE: app/models/calendar_integration.py
class CalendarIntegration (line 8) | class CalendarIntegration(db.Model):
method __repr__ (line 48) | def __repr__(self):
class CalendarSyncEvent (line 52) | class CalendarSyncEvent(db.Model):
method __repr__ (line 88) | def __repr__(self):
FILE: app/models/client.py
class Client (line 14) | class Client(db.Model):
method __init__ (line 50) | def __init__(
method __repr__ (line 86) | def __repr__(self):
method is_active (line 90) | def is_active(self):
method total_projects (line 95) | def total_projects(self):
method active_projects (line 100) | def active_projects(self):
method total_hours (line 105) | def total_hours(self):
method total_billable_hours (line 113) | def total_billable_hours(self):
method estimated_total_cost (line 121) | def estimated_total_cost(self):
method prepaid_plan_enabled (line 130) | def prepaid_plan_enabled(self):
method prepaid_hours_decimal (line 139) | def prepaid_hours_decimal(self):
method prepaid_month_start (line 148) | def prepaid_month_start(self, reference_datetime):
method get_prepaid_consumed_hours (line 180) | def get_prepaid_consumed_hours(self, month_start):
method get_prepaid_remaining_hours (line 196) | def get_prepaid_remaining_hours(self, month_start):
method archive (line 204) | def archive(self):
method activate (line 209) | def activate(self):
method get_custom_field (line 214) | def get_custom_field(self, key, default=None):
method set_custom_field (line 220) | def set_custom_field(self, key, value):
method remove_custom_field (line 228) | def remove_custom_field(self, key):
method get_rendered_links (line 235) | def get_rendered_links(self):
method to_dict (line 262) | def to_dict(self):
method get_active_clients (line 287) | def get_active_clients(cls):
method get_all_clients (line 292) | def get_all_clients(cls):
method set_portal_password (line 297) | def set_portal_password(self, password):
method check_portal_password (line 304) | def check_portal_password(self, password):
method has_portal_access (line 311) | def has_portal_access(self):
method get_portal_data (line 315) | def get_portal_data(self):
method generate_password_setup_token (line 341) | def generate_password_setup_token(self, expires_hours=24):
method verify_password_setup_token (line 348) | def verify_password_setup_token(self, token):
method clear_password_setup_token (line 361) | def clear_password_setup_token(self):
method authenticate_portal (line 367) | def authenticate_portal(cls, username, password):
method find_by_password_token (line 382) | def find_by_password_token(cls, token):
FILE: app/models/client_attachment.py
function local_now (line 8) | def local_now():
class ClientAttachment (line 13) | class ClientAttachment(db.Model):
method __init__ (line 42) | def __init__(self, client_id, filename, original_filename, file_path, ...
method __repr__ (line 53) | def __repr__(self):
method file_size_mb (line 57) | def file_size_mb(self):
method file_size_kb (line 62) | def file_size_kb(self):
method file_size_display (line 67) | def file_size_display(self):
method file_extension (line 77) | def file_extension(self):
method is_image (line 82) | def is_image(self):
method is_pdf (line 87) | def is_pdf(self):
method is_document (line 92) | def is_document(self):
method download_url (line 97) | def download_url(self):
method to_dict (line 103) | def to_dict(self):
method get_client_attachments (line 126) | def get_client_attachments(cls, client_id, include_client_visible=True):
FILE: app/models/client_note.py
class ClientNote (line 7) | class ClientNote(db.Model):
method __init__ (line 40) | def __init__(self, content, user_id, client_id, is_important=False):
method __repr__ (line 60) | def __repr__(self):
method author_name (line 64) | def author_name(self):
method client_name (line 71) | def client_name(self):
method can_edit (line 75) | def can_edit(self, user):
method can_delete (line 79) | def can_delete(self, user):
method edit_content (line 83) | def edit_content(self, new_content, user, is_important=None):
method to_dict (line 102) | def to_dict(self):
method get_client_notes (line 119) | def get_client_notes(cls, client_id, order_by_important=False):
method get_important_notes (line 136) | def get_important_notes(cls, client_id=None):
method get_user_notes (line 146) | def get_user_notes(cls, user_id, limit=None):
method get_recent_notes (line 156) | def get_recent_notes(cls, limit=10):
FILE: app/models/client_notification.py
class NotificationType (line 12) | class NotificationType(enum.Enum):
class ClientNotification (line 28) | class ClientNotification(db.Model):
method __repr__ (line 62) | def __repr__(self):
method mark_as_read (line 65) | def mark_as_read(self):
method to_dict (line 71) | def to_dict(self):
method get_unread_count (line 88) | def get_unread_count(cls, client_id):
method get_recent_notifications (line 93) | def get_recent_notifications(cls, client_id, limit=20):
class ClientNotificationPreferences (line 98) | class ClientNotificationPreferences(db.Model):
method __repr__ (line 131) | def __repr__(self):
method should_send_email (line 134) | def should_send_email(self, notification_type):
method to_dict (line 152) | def to_dict(self):
FILE: app/models/client_portal_customization.py
class ClientPortalCustomization (line 11) | class ClientPortalCustomization(db.Model):
method __repr__ (line 59) | def __repr__(self):
method to_dict (line 62) | def to_dict(self):
method get_css_variables (line 87) | def get_css_variables(self):
FILE: app/models/client_portal_dashboard_preference.py
class ClientPortalDashboardPreference (line 22) | class ClientPortalDashboardPreference(db.Model):
method __repr__ (line 52) | def __repr__(self):
method to_dict (line 55) | def to_dict(self):
FILE: app/models/client_prepaid_consumption.py
class ClientPrepaidConsumption (line 7) | class ClientPrepaidConsumption(db.Model):
method __repr__ (line 28) | def __repr__(self):
method hours_consumed (line 33) | def hours_consumed(self) -> Decimal:
FILE: app/models/client_time_approval.py
class ClientApprovalStatus (line 14) | class ClientApprovalStatus(enum.Enum):
class ClientTimeApproval (line 23) | class ClientTimeApproval(db.Model):
method __repr__ (line 58) | def __repr__(self):
method to_dict (line 61) | def to_dict(self):
method approve (line 78) | def approve(self, contact_id: int, comment: str = None):
method reject (line 86) | def reject(self, contact_id: int, reason: str):
method cancel (line 94) | def cancel(self):
class ClientApprovalPolicy (line 100) | class ClientApprovalPolicy(db.Model):
method __repr__ (line 128) | def __repr__(self):
method to_dict (line 131) | def to_dict(self):
method applies_to_entry (line 143) | def applies_to_entry(self, time_entry) -> bool:
FILE: app/models/comment.py
class Comment (line 7) | class Comment(db.Model):
method __init__ (line 47) | def __init__(
method __repr__ (line 90) | def __repr__(self):
method is_reply (line 109) | def is_reply(self):
method target_type (line 114) | def target_type(self):
method target_name (line 125) | def target_name(self):
method reply_count (line 136) | def reply_count(self):
method can_edit (line 140) | def can_edit(self, user):
method can_delete (line 144) | def can_delete(self, user):
method edit_content (line 148) | def edit_content(self, new_content, user):
method delete_comment (line 157) | def delete_comment(self, user):
method to_dict (line 172) | def to_dict(self):
method get_project_comments (line 213) | def get_project_comments(cls, project_id, include_replies=True):
method get_task_comments (line 223) | def get_task_comments(cls, task_id, include_replies=True):
method get_user_comments (line 233) | def get_user_comments(cls, user_id, limit=None):
method get_quote_comments (line 243) | def get_quote_comments(cls, quote_id, include_replies=True, include_in...
method get_recent_comments (line 256) | def get_recent_comments(cls, limit=10):
FILE: app/models/comment_attachment.py
function local_now (line 8) | def local_now():
class CommentAttachment (line 13) | class CommentAttachment(db.Model):
method __init__ (line 38) | def __init__(self, comment_id, filename, original_filename, file_path,...
method __repr__ (line 47) | def __repr__(self):
method file_size_mb (line 51) | def file_size_mb(self):
method file_size_kb (line 56) | def file_size_kb(self):
method file_size_display (line 61) | def file_size_display(self):
method file_extension (line 71) | def file_extension(self):
method is_image (line 76) | def is_image(self):
method is_pdf (line 81) | def is_pdf(self):
method is_document (line 86) | def is_document(self):
method download_url (line 91) | def download_url(self):
method to_dict (line 97) | def to_dict(self):
method get_comment_attachments (line 118) | def get_comment_attachments(cls, comment_id):
FILE: app/models/contact.py
function local_now (line 7) | def local_now():
class Contact (line 12) | class Contact(db.Model):
method __init__ (line 57) | def __init__(self, client_id, first_name, last_name, created_by, **kwa...
method __repr__ (line 76) | def __repr__(self):
method full_name (line 80) | def full_name(self):
method display_name (line 85) | def display_name(self):
method to_dict (line 91) | def to_dict(self):
method get_active_contacts (line 117) | def get_active_contacts(cls, client_id=None):
method get_primary_contact (line 125) | def get_primary_contact(cls, client_id):
method set_as_primary (line 129) | def set_as_primary(self):
FILE: app/models/contact_communication.py
function local_now (line 7) | def local_now():
class ContactCommunication (line 12) | class ContactCommunication(db.Model):
method __init__ (line 52) | def __init__(self, contact_id, type, created_by, **kwargs):
method __repr__ (line 68) | def __repr__(self):
method to_dict (line 71) | def to_dict(self):
method get_recent_communications (line 92) | def get_recent_communications(cls, contact_id=None, limit=50):
FILE: app/models/currency.py
class Currency (line 6) | class Currency(db.Model):
method __repr__ (line 19) | def __repr__(self):
class ExchangeRate (line 23) | class ExchangeRate(db.Model):
method __repr__ (line 40) | def __repr__(self):
FILE: app/models/custom_field_definition.py
class CustomFieldDefinition (line 10) | class CustomFieldDefinition(db.Model):
method __repr__ (line 29) | def __repr__(self):
method to_dict (line 32) | def to_dict(self):
method get_active_definitions (line 48) | def get_active_definitions(cls):
method get_mandatory_definitions (line 92) | def get_mandatory_definitions(cls):
method get_by_key (line 136) | def get_by_key(cls, field_key):
method count_clients_with_value (line 179) | def count_clients_with_value(self):
FILE: app/models/custom_report.py
class CustomReportConfig (line 10) | class CustomReportConfig(db.Model):
method __repr__ (line 42) | def __repr__(self):
method to_dict (line 45) | def to_dict(self):
FILE: app/models/deal.py
function local_now (line 8) | def local_now():
class Deal (line 13) | class Deal(db.Model):
method __init__ (line 68) | def __init__(self, name, created_by, **kwargs):
method __repr__ (line 89) | def __repr__(self):
method weighted_value (line 93) | def weighted_value(self):
method is_open (line 100) | def is_open(self):
method is_won (line 105) | def is_won(self):
method is_lost (line 110) | def is_lost(self):
method close_won (line 114) | def close_won(self, close_date=None):
method close_lost (line 122) | def close_lost(self, reason=None, close_date=None):
method to_dict (line 132) | def to_dict(self):
method get_open_deals (line 164) | def get_open_deals(cls, user_id=None):
method get_deals_by_stage (line 172) | def get_deals_by_stage(cls, stage):
FILE: app/models/deal_activity.py
function local_now (line 7) | def local_now():
class DealActivity (line 12) | class DealActivity(db.Model):
method __init__ (line 42) | def __init__(self, deal_id, type, created_by, **kwargs):
method __repr__ (line 54) | def __repr__(self):
method to_dict (line 57) | def to_dict(self):
FILE: app/models/donation_interaction.py
class DonationInteraction (line 23) | class DonationInteraction(db.Model):
method __repr__ (line 51) | def __repr__(self):
method record_interaction (line 55) | def record_interaction(
method has_recent_donation_click (line 80) | def has_recent_donation_click(user_id: int, days: int = 30) -> bool:
method get_user_engagement_metrics (line 94) | def get_user_engagement_metrics(user_id: int) -> dict:
FILE: app/models/expense.py
class Expense (line 9) | class Expense(db.Model):
method __init__ (line 83) | def __init__(self, user_id, title, category, amount, expense_date, **k...
method __repr__ (line 108) | def __repr__(self):
method is_approved (line 112) | def is_approved(self):
method is_rejected (line 117) | def is_rejected(self):
method is_reimbursed (line 122) | def is_reimbursed(self):
method is_invoiced (line 127) | def is_invoiced(self):
method total_amount (line 132) | def total_amount(self):
method tag_list (line 137) | def tag_list(self):
method approve (line 143) | def approve(self, approved_by_user_id, notes=None):
method reject (line 152) | def reject(self, rejected_by_user_id, reason):
method mark_as_reimbursed (line 160) | def mark_as_reimbursed(self):
method mark_as_invoiced (line 167) | def mark_as_invoiced(self, invoice_id):
method unmark_as_invoiced (line 173) | def unmark_as_invoiced(self):
method to_dict (line 179) | def to_dict(self):
method get_expenses (line 221) | def get_expenses(
method get_total_expenses (line 266) | def get_total_expenses(
method get_expenses_by_category (line 307) | def get_expenses_by_category(cls, user_id=None, start_date=None, end_d...
method get_pending_approvals (line 335) | def get_pending_approvals(cls, user_id=None):
method get_pending_reimbursements (line 345) | def get_pending_reimbursements(cls, user_id=None):
method get_uninvoiced_expenses (line 355) | def get_uninvoiced_expenses(cls, project_id=None, client_id=None):
method get_expense_categories (line 368) | def get_expense_categories(cls):
method get_payment_methods (line 384) | def get_payment_methods(cls):
FILE: app/models/expense_category.py
class ExpenseCategory (line 9) | class ExpenseCategory(db.Model):
method __init__ (line 37) | def __init__(self, name, **kwargs):
method __repr__ (line 52) | def __repr__(self):
method get_spent_amount (line 55) | def get_spent_amount(self, start_date, end_date):
method get_budget_utilization (line 69) | def get_budget_utilization(self, period="monthly"):
method to_dict (line 103) | def to_dict(self):
method get_active_categories (line 125) | def get_active_categories(cls):
method get_categories_over_budget (line 130) | def get_categories_over_budget(cls, period="monthly"):
FILE: app/models/expense_gps.py
class MileageTrack (line 13) | class MileageTrack(db.Model):
method __repr__ (line 55) | def __repr__(self):
method to_dict (line 58) | def to_dict(self):
method calculate_distance (line 79) | def calculate_distance(self):
method calculate_distance_from_track_points (line 108) | def calculate_distance_from_track_points(self) -> Optional[float]:
FILE: app/models/extra_good.py
class ExtraGood (line 7) | class ExtraGood(db.Model):
method __init__ (line 48) | def __init__(
method __repr__ (line 92) | def __repr__(self):
method update_total (line 95) | def update_total(self):
method to_dict (line 100) | def to_dict(self):
method get_project_goods (line 123) | def get_project_goods(cls, project_id, billable_only=False):
method get_invoice_goods (line 133) | def get_invoice_goods(cls, invoice_id):
method get_total_amount (line 138) | def get_total_amount(cls, project_id=None, invoice_id=None, billable_o...
method get_goods_by_category (line 155) | def get_goods_by_category(cls, project_id=None, invoice_id=None):
FILE: app/models/focus_session.py
class FocusSession (line 6) | class FocusSession(db.Model):
method to_dict (line 39) | def to_dict(self):
FILE: app/models/gamification.py
class Badge (line 12) | class Badge(db.Model):
method __repr__ (line 35) | def __repr__(self):
method to_dict (line 38) | def to_dict(self):
class UserBadge (line 52) | class UserBadge(db.Model):
method __repr__ (line 76) | def __repr__(self):
method to_dict (line 79) | def to_dict(self):
class Leaderboard (line 91) | class Leaderboard(db.Model):
method __repr__ (line 121) | def __repr__(self):
method to_dict (line 124) | def to_dict(self):
class LeaderboardEntry (line 137) | class LeaderboardEntry(db.Model):
method __repr__ (line 169) | def __repr__(self):
method to_dict (line 172) | def to_dict(self):
FILE: app/models/import_export.py
class DataImport (line 10) | class DataImport(db.Model):
method __init__ (line 33) | def __init__(self, user_id, import_type, source_file=None):
method __repr__ (line 42) | def __repr__(self):
method start_processing (line 45) | def start_processing(self):
method complete (line 50) | def complete(self):
method fail (line 56) | def fail(self, error_message=None):
method partial_complete (line 77) | def partial_complete(self):
method update_progress (line 83) | def update_progress(self, total, successful, failed):
method add_error (line 94) | def add_error(self, error_message, record_data=None):
method set_summary (line 117) | def set_summary(self, summary_dict):
method to_dict (line 124) | def to_dict(self):
class DataExport (line 145) | class DataExport(db.Model):
method __init__ (line 169) | def __init__(self, user_id, export_type, export_format="json", filters...
method __repr__ (line 180) | def __repr__(self):
method start_processing (line 183) | def start_processing(self):
method complete (line 188) | def complete(self, file_path, file_size, record_count):
method fail (line 199) | def fail(self, error_message):
method is_expired (line 206) | def is_expired(self):
method to_dict (line 212) | def to_dict(self):
FILE: app/models/integration.py
class Integration (line 12) | class Integration(db.Model):
method __repr__ (line 42) | def __repr__(self):
class IntegrationCredential (line 46) | class IntegrationCredential(db.Model):
method __repr__ (line 67) | def __repr__(self):
method is_expired (line 71) | def is_expired(self):
method needs_refresh (line 77) | def needs_refresh(self):
class IntegrationEvent (line 86) | class IntegrationEvent(db.Model):
method __repr__ (line 106) | def __repr__(self):
FILE: app/models/integration_external_event_link.py
class IntegrationExternalEventLink (line 12) | class IntegrationExternalEventLink(db.Model):
method __repr__ (line 39) | def __repr__(self):
FILE: app/models/invoice.py
class Invoice (line 8) | class Invoice(db.Model):
method __init__ (line 75) | def __init__(self, invoice_number, project_id, client_name, due_date, ...
method __repr__ (line 103) | def __repr__(self):
method is_overdue (line 107) | def is_overdue(self):
method days_overdue (line 112) | def days_overdue(self):
method is_paid (line 119) | def is_paid(self):
method is_partially_paid (line 124) | def is_partially_paid(self):
method outstanding_amount (line 129) | def outstanding_amount(self):
method payment_percentage (line 135) | def payment_percentage(self):
method sorted_payments (line 142) | def sorted_payments(self):
method update_payment_status (line 148) | def update_payment_status(self):
method record_payment (line 160) | def record_payment(
method calculate_totals (line 200) | def calculate_totals(self):
method _apply_tax_rules_if_any (line 219) | def _apply_tax_rules_if_any(self):
method to_dict (line 250) | def to_dict(self):
method generate_invoice_number (line 290) | def generate_invoice_number(cls):
class InvoiceItem (line 295) | class InvoiceItem(db.Model):
method __init__ (line 324) | def __init__(
method __repr__ (line 337) | def __repr__(self):
method task_name_from_time_entries (line 341) | def task_name_from_time_entries(self):
method to_dict (line 358) | def to_dict(self):
FILE: app/models/invoice_approval.py
class InvoiceApproval (line 8) | class InvoiceApproval(db.Model):
method __repr__ (line 49) | def __repr__(self):
method is_pending (line 53) | def is_pending(self):
method is_approved (line 58) | def is_approved(self):
method is_rejected (line 63) | def is_rejected(self):
method to_dict (line 67) | def to_dict(self):
FILE: app/models/invoice_email.py
function local_now (line 7) | def local_now():
class InvoiceEmail (line 12) | class InvoiceEmail(db.Model):
method __init__ (line 48) | def __init__(self, invoice_id, recipient_email, subject, sent_by, **kw...
method __repr__ (line 56) | def __repr__(self):
method mark_opened (line 59) | def mark_opened(self):
method mark_paid (line 68) | def mark_paid(self):
method mark_failed (line 74) | def mark_failed(self, error_message):
method mark_bounced (line 79) | def mark_bounced(self):
method to_dict (line 83) | def to_dict(self):
FILE: app/models/invoice_image.py
function local_now (line 8) | def local_now():
class InvoiceImage (line 13) | class InvoiceImage(db.Model):
method __init__ (line 44) | def __init__(self, invoice_id, filename, original_filename, file_path,...
method __repr__ (line 59) | def __repr__(self):
method file_size_mb (line 63) | def file_size_mb(self):
method file_size_kb (line 68) | def file_size_kb(self):
method file_size_display (line 73) | def file_size_display(self):
method file_extension (line 83) | def file_extension(self):
method is_image (line 88) | def is_image(self):
method to_dict (line 92) | def to_dict(self):
method get_invoice_images (line 116) | def get_invoice_images(cls, invoice_id):
FILE: app/models/invoice_pdf_template.py
class InvoicePDFTemplate (line 11) | class InvoicePDFTemplate(db.Model):
method __repr__ (line 39) | def __repr__(self):
method get_template (line 43) | def get_template(cls, page_size="A4"):
method get_all_templates (line 78) | def get_all_templates(cls):
method get_default_template (line 83) | def get_default_template(cls):
method ensure_default_templates (line 88) | def ensure_default_templates(cls):
method to_dict (line 113) | def to_dict(self):
method get_template_json (line 127) | def get_template_json(self):
method set_template_json (line 138) | def set_template_json(self, template_dict):
method get_page_dimensions_mm (line 144) | def get_page_dimensions_mm(self):
method get_page_dimensions_px (line 148) | def get_page_dimensions_px(self, dpi=72):
method ensure_template_json (line 156) | def ensure_template_json(self):
FILE: app/models/invoice_peppol.py
class InvoicePeppolTransmission (line 6) | class InvoicePeppolTransmission(db.Model):
method mark_sent (line 35) | def mark_sent(self, message_id=None, response_payload=None):
method mark_failed (line 43) | def mark_failed(self, error_message, response_payload=None):
method to_dict (line 49) | def to_dict(self):
FILE: app/models/invoice_template.py
class InvoiceTemplate (line 6) | class InvoiceTemplate(db.Model):
method __repr__ (line 21) | def __repr__(self):
FILE: app/models/issue.py
class Issue (line 7) | class Issue(db.Model):
method __init__ (line 48) | def __init__(
method __repr__ (line 76) | def __repr__(self):
method is_open (line 80) | def is_open(self):
method is_resolved (line 85) | def is_resolved(self):
method is_closed (line 90) | def is_closed(self):
method status_display (line 95) | def status_display(self):
method priority_display (line 107) | def priority_display(self):
method priority_class (line 113) | def priority_class(self):
method mark_in_progress (line 123) | def mark_in_progress(self):
method mark_resolved (line 132) | def mark_resolved(self):
method mark_closed (line 142) | def mark_closed(self):
method cancel (line 149) | def cancel(self):
method link_to_task (line 158) | def link_to_task(self, task_id):
method create_task_from_issue (line 174) | def create_task_from_issue(self, project_id, assigned_to=None, created...
method reassign (line 206) | def reassign(self, user_id):
method update_priority (line 212) | def update_priority(self, priority):
method to_dict (line 222) | def to_dict(self):
method get_issues_by_client (line 256) | def get_issues_by_client(cls, client_id, status=None, priority=None):
method get_issues_by_project (line 269) | def get_issues_by_project(cls, project_id, status=None):
method get_issues_by_task (line 279) | def get_issues_by_task(cls, task_id):
method get_user_issues (line 284) | def get_user_issues(cls, user_id, status=None):
method get_open_issues (line 294) | def get_open_issues(cls):
FILE: app/models/kanban_column.py
class KanbanColumn (line 5) | class KanbanColumn(db.Model):
method __init__ (line 28) | def __init__(self, **kwargs):
method __repr__ (line 32) | def __repr__(self):
method to_dict (line 36) | def to_dict(self):
method get_active_columns (line 54) | def get_active_columns(cls, project_id=None):
method get_all_columns (line 74) | def get_all_columns(cls, project_id=None):
method get_column_by_key (line 94) | def get_column_by_key(cls, key, project_id=None):
method get_valid_status_keys (line 110) | def get_valid_status_keys(cls, project_id=None):
method initialize_default_columns (line 119) | def initialize_default_columns(cls, project_id=None):
method reorder_columns (line 181) | def reorder_columns(cls, column_ids, project_id=None):
FILE: app/models/lead.py
function local_now (line 8) | def local_now():
class Lead (line 13) | class Lead(db.Model):
method __init__ (line 67) | def __init__(self, first_name, last_name, created_by, **kwargs):
method __repr__ (line 86) | def __repr__(self):
method full_name (line 90) | def full_name(self):
method display_name (line 95) | def display_name(self):
method is_converted (line 102) | def is_converted(self):
method is_lost (line 107) | def is_lost(self):
method convert_to_client (line 111) | def convert_to_client(self, client_id, user_id):
method convert_to_deal (line 119) | def convert_to_deal(self, deal_id, user_id):
method mark_lost (line 127) | def mark_lost(self):
method to_dict (line 132) | def to_dict(self):
method get_active_leads (line 164) | def get_active_leads(cls, user_id=None):
method get_leads_by_status (line 172) | def get_leads_by_status(cls, status):
FILE: app/models/lead_activity.py
function local_now (line 7) | def local_now():
class LeadActivity (line 12) | class LeadActivity(db.Model):
method __init__ (line 42) | def __init__(self, lead_id, type, created_by, **kwargs):
method __repr__ (line 54) | def __repr__(self):
method to_dict (line 57) | def to_dict(self):
FILE: app/models/link_template.py
class LinkTemplate (line 10) | class LinkTemplate(db.Model):
method __repr__ (line 30) | def __repr__(self):
method render_url (line 33) | def render_url(self, field_value):
method to_dict (line 53) | def to_dict(self):
method get_active_templates (line 70) | def get_active_templates(cls, field_key=None):
FILE: app/models/mileage.py
class Mileage (line 9) | class Mileage(db.Model):
method __init__ (line 83) | def __init__(self, user_id, trip_date, purpose, start_location, end_lo...
method __repr__ (line 116) | def __repr__(self):
method total_distance_km (line 120) | def total_distance_km(self):
method total_amount (line 126) | def total_amount(self):
method approve (line 131) | def approve(self, approved_by_user_id, notes=None):
method reject (line 140) | def reject(self, rejected_by_user_id, reason):
method mark_as_reimbursed (line 148) | def mark_as_reimbursed(self):
method create_expense (line 155) | def create_expense(self):
method to_dict (line 177) | def to_dict(self):
method get_default_rates (line 220) | def get_default_rates(cls):
method get_pending_approvals (line 231) | def get_pending_approvals(cls, user_id=None):
method get_total_distance (line 241) | def get_total_distance(cls, user_id=None, start_date=None, end_date=No...
FILE: app/models/payment_gateway.py
class PaymentGateway (line 9) | class PaymentGateway(db.Model):
method __repr__ (line 33) | def __repr__(self):
class PaymentTransaction (line 37) | class PaymentTransaction(db.Model):
method __repr__ (line 81) | def __repr__(self):
method to_dict (line 84) | def to_dict(self):
FILE: app/models/payments.py
class Payment (line 7) | class Payment(db.Model):
method __repr__ (line 35) | def __repr__(self):
method calculate_net_amount (line 38) | def calculate_net_amount(self):
method to_dict (line 45) | def to_dict(self):
class CreditNote (line 66) | class CreditNote(db.Model):
method __repr__ (line 80) | def __repr__(self):
class InvoiceReminderSchedule (line 84) | class InvoiceReminderSchedule(db.Model):
method __repr__ (line 99) | def __repr__(self):
FILE: app/models/per_diem.py
class PerDiemRate (line 9) | class PerDiemRate(db.Model):
method __init__ (line 45) | def __init__(self, country, full_day_rate, half_day_rate, effective_fr...
method __repr__ (line 60) | def __repr__(self):
method to_dict (line 64) | def to_dict(self):
method get_rate_for_location (line 86) | def get_rate_for_location(cls, country, city=None, date=None):
class PerDiem (line 116) | class PerDiem(db.Model):
method __init__ (line 197) | def __init__(self, user_id, trip_purpose, start_date, end_date, countr...
method _calculate_amount (line 232) | def _calculate_amount(self):
method recalculate_amount (line 244) | def recalculate_amount(self):
method __repr__ (line 249) | def __repr__(self):
method total_days (line 254) | def total_days(self):
method trip_duration (line 259) | def trip_duration(self):
method approve (line 263) | def approve(self, approved_by_user_id, notes=None):
method reject (line 272) | def reject(self, rejected_by_user_id, reason):
method mark_as_reimbursed (line 280) | def mark_as_reimbursed(self):
method create_expense (line 287) | def create_expense(self):
method to_dict (line 311) | def to_dict(self):
method calculate_days_from_dates (line 358) | def calculate_days_from_dates(cls, start_date, end_date, departure_tim...
method get_pending_approvals (line 415) | def get_pending_approvals(cls, user_id=None):
FILE: app/models/permission.py
class Permission (line 8) | class Permission(db.Model):
method __init__ (line 21) | def __init__(self, name, description=None, category="general"):
method __repr__ (line 26) | def __repr__(self):
method to_dict (line 29) | def to_dict(self):
class Role (line 49) | class Role(db.Model):
method __init__ (line 69) | def __init__(self, name, description=None, is_system_role=False, hidde...
method __repr__ (line 76) | def __repr__(self):
method has_permission (line 79) | def has_permission(self, permission_name):
method add_permission (line 83) | def add_permission(self, permission):
method remove_permission (line 88) | def remove_permission(self, permission):
method get_permission_names (line 93) | def get_permission_names(self):
method to_dict (line 97) | def to_dict(self, include_permissions=False):
FILE: app/models/project.py
class Project (line 7) | class Project(db.Model):
method __init__ (line 44) | def __init__(
method __repr__ (line 101) | def __repr__(self):
method client (line 105) | def client(self):
method is_active (line 110) | def is_active(self):
method is_archived (line 115) | def is_archived(self):
method archived_by_user (line 120) | def archived_by_user(self):
method code_display (line 129) | def code_display(self):
method total_hours (line 143) | def total_hours(self):
method total_billable_hours (line 156) | def total_billable_hours(self):
method estimated_cost (line 169) | def estimated_cost(self):
method total_costs (line 176) | def total_costs(self):
method total_billable_costs (line 186) | def total_billable_costs(self):
method total_project_value (line 199) | def total_project_value(self):
method actual_hours (line 204) | def actual_hours(self):
method budget_consumed_amount (line 209) | def budget_consumed_amount(self):
method budget_threshold_exceeded (line 227) | def budget_threshold_exceeded(self):
method get_entries_by_user (line 236) | def get_entries_by_user(self, user_id=None, start_date=None, end_date=...
method get_user_totals (line 253) | def get_user_totals(self, start_date=None, end_date=None):
method archive (line 282) | def archive(self, user_id=None, reason=None):
method unarchive (line 296) | def unarchive(self):
method deactivate (line 305) | def deactivate(self):
method activate (line 311) | def activate(self):
method is_favorited_by (line 317) | def is_favorited_by(self, user):
method get_custom_field (line 328) | def get_custom_field(self, key, default=None):
method set_custom_field (line 334) | def set_custom_field(self, key, value):
method remove_custom_field (line 341) | def remove_custom_field(self, key):
method get_rendered_links (line 347) | def get_rendered_links(self):
method to_dict (line 374) | def to_dict(self, user=None):
FILE: app/models/project_attachment.py
function local_now (line 8) | def local_now():
class ProjectAttachment (line 13) | class ProjectAttachment(db.Model):
method __init__ (line 42) | def __init__(self, project_id, filename, original_filename, file_path,...
method __repr__ (line 53) | def __repr__(self):
method file_size_mb (line 57) | def file_size_mb(self):
method file_size_kb (line 62) | def file_size_kb(self):
method file_size_display (line 67) | def file_size_display(self):
method file_extension (line 77) | def file_extension(self):
method is_image (line 82) | def is_image(self):
method is_pdf (line 87) | def is_pdf(self):
method is_document (line 92) | def is_document(self):
method download_url (line 97) | def download_url(self):
method to_dict (line 103) | def to_dict(self):
method get_project_attachments (line 126) | def get_project_attachments(cls, project_id, include_client_visible=Tr...
FILE: app/models/project_cost.py
class ProjectCost (line 7) | class ProjectCost(db.Model):
method __init__ (line 38) | def __init__(
method __repr__ (line 62) | def __repr__(self):
method is_invoiced (line 66) | def is_invoiced(self):
method mark_as_invoiced (line 70) | def mark_as_invoiced(self, invoice_id):
method unmark_as_invoiced (line 76) | def unmark_as_invoiced(self):
method to_dict (line 82) | def to_dict(self):
method get_project_costs (line 105) | def get_project_costs(cls, project_id, start_date=None, end_date=None,...
method get_total_costs (line 124) | def get_total_costs(cls, project_id, start_date=None, end_date=None, u...
method get_uninvoiced_costs (line 144) | def get_uninvoiced_costs(cls, project_id):
method get_costs_by_category (line 153) | def get_costs_by_category(cls, project_id, start_date=None, end_date=N...
FILE: app/models/project_stock_allocation.py
class ProjectStockAllocation (line 9) | class ProjectStockAllocation(db.Model):
method __init__ (line 32) | def __init__(self, project_id, stock_item_id, warehouse_id, quantity_a...
method __repr__ (line 41) | def __repr__(self):
method quantity_remaining (line 45) | def quantity_remaining(self):
method record_usage (line 49) | def record_usage(self, quantity):
method to_dict (line 56) | def to_dict(self):
FILE: app/models/project_template.py
class ProjectTemplate (line 9) | class ProjectTemplate(db.Model):
method __repr__ (line 46) | def __repr__(self):
method to_dict (line 49) | def to_dict(self):
FILE: app/models/purchase_order.py
function _normalize_optional_text (line 9) | def _normalize_optional_text(value):
function _normalize_required_text (line 17) | def _normalize_required_text(value, field_name):
class PurchaseOrder (line 25) | class PurchaseOrder(db.Model):
method __init__ (line 59) | def __init__(
method __repr__ (line 84) | def __repr__(self):
method calculate_totals (line 87) | def calculate_totals(self):
method mark_as_sent (line 94) | def mark_as_sent(self):
method mark_as_received (line 100) | def mark_as_received(self, received_date=None):
method cancel (line 135) | def cancel(self):
method to_dict (line 141) | def to_dict(self):
class PurchaseOrderItem (line 165) | class PurchaseOrderItem(db.Model):
method __init__ (line 194) | def __init__(
method __repr__ (line 220) | def __repr__(self):
method update_line_total (line 223) | def update_line_total(self):
method to_dict (line 228) | def to_dict(self):
FILE: app/models/push_subscription.py
class PushSubscription (line 12) | class PushSubscription(db.Model):
method __init__ (line 33) | def __init__(self, user_id, endpoint, keys, user_agent=None):
method __repr__ (line 40) | def __repr__(self):
method to_dict (line 43) | def to_dict(self):
method update_last_used (line 56) | def update_last_used(self):
method get_user_subscriptions (line 63) | def get_user_subscriptions(cls, user_id):
method find_by_endpoint (line 68) | def find_by_endpoint(cls, user_id, endpoint):
FILE: app/models/quote.py
function local_now (line 10) | def local_now():
class Quote (line 15) | class Quote(db.Model):
method __init__ (line 107) | def __init__(self, quote_number, client_id, title, created_by, **kwargs):
method __repr__ (line 137) | def __repr__(self):
method is_draft (line 141) | def is_draft(self):
method is_sent (line 146) | def is_sent(self):
method is_accepted (line 151) | def is_accepted(self):
method is_rejected (line 156) | def is_rejected(self):
method is_expired (line 161) | def is_expired(self):
method can_be_accepted (line 168) | def can_be_accepted(self):
method has_project (line 173) | def has_project(self):
method can_be_sent (line 178) | def can_be_sent(self):
method calculate_totals (line 186) | def calculate_totals(self):
method discount_value (line 209) | def discount_value(self):
method subtotal_after_discount (line 221) | def subtotal_after_discount(self):
method calculate_due_date_from_payment_terms (line 225) | def calculate_due_date_from_payment_terms(self, issue_date=None):
method send (line 273) | def send(self):
method request_approval (line 281) | def request_approval(self):
method approve (line 290) | def approve(self, user_id, notes=None):
method reject_approval (line 303) | def reject_approval(self, user_id, reason):
method accept (line 315) | def accept(self, user_id, project_id=None):
method reject (line 327) | def reject(self):
method expire (line 336) | def expire(self):
method to_dict (line 342) | def to_dict(self):
method generate_quote_number (line 387) | def generate_quote_number(cls):
class QuoteItem (line 411) | class QuoteItem(db.Model):
method __init__ (line 448) | def __init__(
method __repr__ (line 501) | def __repr__(self):
method to_dict (line 504) | def to_dict(self):
class QuotePDFTemplate (line 527) | class QuotePDFTemplate(db.Model):
method __repr__ (line 555) | def __repr__(self):
method get_template (line 559) | def get_template(cls, page_size="A4"):
method get_all_templates (line 591) | def get_all_templates(cls):
method get_default_template (line 596) | def get_default_template(cls):
method get_template_json (line 605) | def get_template_json(self):
method set_template_json (line 616) | def set_template_json(self, template_dict):
method ensure_template_json (line 622) | def ensure_template_json(self):
FILE: app/models/quote_attachment.py
function local_now (line 8) | def local_now():
class QuoteAttachment (line 13) | class QuoteAttachment(db.Model):
method __init__ (line 42) | def __init__(self, quote_id, filename, original_filename, file_path, f...
method __repr__ (line 53) | def __repr__(self):
method file_size_mb (line 57) | def file_size_mb(self):
method file_size_kb (line 62) | def file_size_kb(self):
method file_size_display (line 67) | def file_size_display(self):
method file_extension (line 77) | def file_extension(self):
method is_image (line 82) | def is_image(self):
method is_pdf (line 87) | def is_pdf(self):
method is_document (line 92) | def is_document(self):
method download_url (line 97) | def download_url(self):
method to_dict (line 103) | def to_dict(self):
method get_quote_attachments (line 126) | def get_quote_attachments(cls, quote_id, include_client_visible=True):
FILE: app/models/quote_image.py
function local_now (line 8) | def local_now():
class QuoteImage (line 13) | class QuoteImage(db.Model):
method __init__ (line 44) | def __init__(self, quote_id, filename, original_filename, file_path, f...
method __repr__ (line 59) | def __repr__(self):
method file_size_mb (line 63) | def file_size_mb(self):
method file_size_kb (line 68) | def file_size_kb(self):
method file_size_display (line 73) | def file_size_display(self):
method file_extension (line 83) | def file_extension(self):
method is_image (line 88) | def is_image(self):
method to_dict (line 92) | def to_dict(self):
method get_quote_images (line 116) | def get_quote_images(cls, quote_id):
FILE: app/models/quote_template.py
function local_now (line 8) | def local_now():
class QuoteTemplate (line 13) | class QuoteTemplate(db.Model):
method __init__ (line 50) | def __init__(self, name, created_by, **kwargs):
method __repr__ (line 67) | def __repr__(self):
method items_list (line 71) | def items_list(self):
method items_list (line 81) | def items_list(self, value):
method data_dict (line 89) | def data_dict(self):
method data_dict (line 99) | def data_dict(self, value):
method increment_usage (line 106) | def increment_usage(self):
method apply_to_quote (line 111) | def apply_to_quote(self, quote):
method to_dict (line 138) | def to_dict(self):
method get_user_templates (line 162) | def get_user_templates(cls, user_id, include_public=True):
method get_public_templates (line 170) | def get_public_templates(cls):
method get_popular_templates (line 175) | def get_popular_templates(cls, limit=10):
FILE: app/models/quote_version.py
function local_now (line 8) | def local_now():
class QuoteVersion (line 13) | class QuoteVersion(db.Model):
method __init__ (line 37) | def __init__(self, quote_id, version_number, quote_data, changed_by, *...
method __repr__ (line 45) | def __repr__(self):
method data_dict (line 49) | def data_dict(self):
method to_dict (line 56) | def to_dict(self):
method create_version (line 71) | def create_version(cls, quote, changed_by, change_summary=None, fields...
method get_quote_versions (line 122) | def get_quote_versions(cls, quote_id):
method get_latest_version (line 127) | def get_latest_version(cls, quote_id):
FILE: app/models/rate_override.py
class RateOverride (line 7) | class RateOverride(db.Model):
method resolve_rate (line 35) | def resolve_rate(cls, project, user_id=None, on_date=None):
FILE: app/models/recurring_block.py
class RecurringBlock (line 6) | class RecurringBlock(db.Model):
method to_dict (line 48) | def to_dict(self):
FILE: app/models/recurring_invoice.py
class RecurringInvoice (line 9) | class RecurringInvoice(db.Model):
method __init__ (line 58) | def __init__(self, name, project_id, client_id, frequency, next_run_da...
method __repr__ (line 82) | def __repr__(self):
method calculate_next_run_date (line 85) | def calculate_next_run_date(self, from_date=None):
method should_generate_today (line 101) | def should_generate_today(self):
method generate_invoice (line 115) | def generate_invoice(self):
method to_dict (line 121) | def to_dict(self):
FILE: app/models/recurring_task.py
class RecurringTask (line 13) | class RecurringTask(db.Model):
method __init__ (line 53) | def __init__(self, name, project_id, frequency, next_run_date, created...
method __repr__ (line 71) | def __repr__(self):
method calculate_next_run_date (line 74) | def calculate_next_run_date(self, from_date=None):
method create_task (line 90) | def create_task(self):
method to_dict (line 124) | def to_dict(self):
FILE: app/models/reporting.py
class SavedReportView (line 6) | class SavedReportView(db.Model):
method __repr__ (line 25) | def __repr__(self):
class ReportEmailSchedule (line 29) | class ReportEmailSchedule(db.Model):
method __repr__ (line 60) | def __repr__(self):
FILE: app/models/salesman_email_mapping.py
class SalesmanEmailMapping (line 13) | class SalesmanEmailMapping(db.Model):
method __init__ (line 28) | def __init__(self, salesman_initial, email_address=None, email_pattern...
method __repr__ (line 36) | def __repr__(self):
method get_email (line 39) | def get_email(self):
method to_dict (line 51) | def to_dict(self):
method get_email_for_initial (line 67) | def get_email_for_initial(cls, initial):
method get_all_active (line 79) | def get_all_active(cls):
FILE: app/models/saved_filter.py
class SavedFilter (line 6) | class SavedFilter(db.Model):
method to_dict (line 28) | def to_dict(self):
FILE: app/models/settings.py
function _session_in_flush (line 14) | def _session_in_flush(session):
class Settings (line 36) | class Settings(db.Model):
method __init__ (line 206) | def __init__(self, **kwargs):
method __repr__ (line 316) | def __repr__(self):
method get_logo_url (line 319) | def get_logo_url(self):
method get_logo_path (line 325) | def get_logo_path(self):
method has_logo (line 340) | def has_logo(self):
method get_mail_config (line 348) | def get_mail_config(self):
method get_ai_config (line 362) | def get_ai_config(self, *, include_secrets: bool = False) -> dict:
method get_integration_credentials (line 408) | def get_integration_credentials(self, provider: str, *, include_secret...
method to_dict (line 502) | def to_dict(self):
method set_secret (line 622) | def set_secret(self, field: str, value: str) -> None:
method get_secret (line 635) | def get_secret(self, field: str) -> str:
method _encrypt_secrets_if_needed (line 640) | def _encrypt_secrets_if_needed(self) -> bool:
method get_settings (line 656) | def get_settings(cls):
method update_settings (line 778) | def update_settings(cls, **kwargs):
method get_system_instance_id (line 801) | def get_system_instance_id(cls):
method _initialize_from_env (line 824) | def _initialize_from_env(cls, settings_instance):
method sync_from_env (line 882) | def sync_from_env(cls):
FILE: app/models/stock_item.py
class StockItem (line 9) | class StockItem(db.Model):
method __init__ (line 46) | def __init__(
method __repr__ (line 86) | def __repr__(self):
method total_quantity_on_hand (line 90) | def total_quantity_on_hand(self):
method total_quantity_reserved (line 100) | def total_quantity_reserved(self):
method total_quantity_available (line 112) | def total_quantity_available(self):
method is_low_stock (line 121) | def is_low_stock(self):
method get_stock_level (line 132) | def get_stock_level(self, warehouse_id):
method get_available_quantity (line 141) | def get_available_quantity(self, warehouse_id):
method to_dict (line 152) | def to_dict(self):
FILE: app/models/stock_lot.py
class StockLot (line 13) | class StockLot(db.Model):
method adjust_on_hand (line 51) | def adjust_on_hand(self, quantity):
method __repr__ (line 56) | def __repr__(self):
class StockLotAllocation (line 63) | class StockLotAllocation(db.Model):
method __repr__ (line 85) | def __repr__(self):
FILE: app/models/stock_movement.py
class StockMovement (line 11) | class StockMovement(db.Model):
method __init__ (line 42) | def __init__(
method __repr__ (line 66) | def __repr__(self):
method to_dict (line 69) | def to_dict(self):
method record_movement (line 87) | def record_movement(
method _ensure_legacy_lot (line 169) | def _ensure_legacy_lot(cls, item, warehouse_id, moved_by, updated_stoc...
method _apply_lot_changes (line 214) | def _apply_lot_changes(
method record_devaluation (line 442) | def record_devaluation(
FILE: app/models/stock_reservation.py
class StockReservation (line 9) | class StockReservation(db.Model):
method __init__ (line 36) | def __init__(
method __repr__ (line 57) | def __repr__(self):
method is_expired (line 61) | def is_expired(self):
method fulfill (line 67) | def fulfill(self):
method cancel (line 81) | def cancel(self):
method expire (line 96) | def expire(self):
method create_reservation (line 111) | def create_reservation(
method to_dict (line 166) | def to_dict(self):
FILE: app/models/supplier.py
class Supplier (line 8) | class Supplier(db.Model):
method __init__ (line 36) | def __init__(
method __repr__ (line 68) | def __repr__(self):
method to_dict (line 71) | def to_dict(self):
FILE: app/models/supplier_stock_item.py
class SupplierStockItem (line 9) | class SupplierStockItem(db.Model):
method __init__ (line 36) | def __init__(
method __repr__ (line 62) | def __repr__(self):
method to_dict (line 65) | def to_dict(self):
FILE: app/models/task.py
class Task (line 7) | class Task(db.Model):
method __init__ (line 42) | def __init__(
method __repr__ (line 66) | def __repr__(self):
method is_active (line 70) | def is_active(self):
method is_overdue (line 75) | def is_overdue(self):
method total_hours (line 84) | def total_hours(self):
method total_billable_hours (line 105) | def total_billable_hours(self):
method progress_percentage (line 121) | def progress_percentage(self):
method status_display (line 133) | def status_display(self):
method priority_display (line 155) | def priority_display(self):
method priority_class (line 161) | def priority_class(self):
method tag_list (line 172) | def tag_list(self):
method start_task (line 178) | def start_task(self):
method pause_task (line 188) | def pause_task(self):
method mark_for_review (line 197) | def mark_for_review(self):
method complete_task (line 206) | def complete_task(self):
method cancel_task (line 216) | def cancel_task(self):
method reassign (line 225) | def reassign(self, user_id):
method update_priority (line 231) | def update_priority(self, priority):
method update_due_date (line 241) | def update_due_date(self, due_date):
method to_dict (line 247) | def to_dict(self):
method get_tasks_by_project (line 279) | def get_tasks_by_project(cls, project_id, status=None, priority=None):
method get_user_tasks (line 292) | def get_user_tasks(cls, user_id, status=None, include_assigned=True, i...
method get_overdue_tasks (line 312) | def get_overdue_tasks(cls):
FILE: app/models/task_activity.py
class TaskActivity (line 5) | class TaskActivity(db.Model):
method __init__ (line 20) | def __init__(self, task_id, event, user_id=None, details=None):
method __repr__ (line 26) | def __repr__(self):
FILE: app/models/tax_rule.py
class TaxRule (line 6) | class TaxRule(db.Model):
method __repr__ (line 28) | def __repr__(self):
FILE: app/models/team_chat.py
class ChatChannel (line 12) | class ChatChannel(db.Model):
method __repr__ (line 41) | def __repr__(self):
method to_dict (line 44) | def to_dict(self):
class ChatChannelMember (line 59) | class ChatChannelMember(db.Model):
method __repr__ (line 87) | def __repr__(self):
class ChatMessage (line 91) | class ChatMessage(db.Model):
method __repr__ (line 133) | def __repr__(self):
method to_dict (line 136) | def to_dict(self):
method parse_mentions (line 156) | def parse_mentions(self):
class ChatReadReceipt (line 175) | class ChatReadReceipt(db.Model):
method __repr__ (line 191) | def __repr__(self):
FILE: app/models/time_entry.py
function local_now (line 8) | def local_now():
class TimeEntry (line 17) | class TimeEntry(db.Model):
method __init__ (line 46) | def __init__(
method __repr__ (line 121) | def __repr__(self):
method is_active (line 132) | def is_active(self):
method is_paused (line 137) | def is_paused(self):
method break_formatted (line 142) | def break_formatted(self):
method duration_hours (line 151) | def duration_hours(self):
method duration_formatted (line 158) | def duration_formatted(self):
method tag_list (line 176) | def tag_list(self):
method current_duration_seconds (line 183) | def current_duration_seconds(self):
method _naive_dt (line 198) | def _naive_dt(self, dt):
method calculate_duration (line 209) | def calculate_duration(self):
method stop_timer (line 240) | def stop_timer(self, end_time=None):
method pause_timer (line 256) | def pause_timer(self):
method resume_timer (line 266) | def resume_timer(self):
method update_notes (line 281) | def update_notes(self, notes):
method update_tags (line 287) | def update_tags(self, tags):
method set_billable (line 293) | def set_billable(self, billable):
method set_paid (line 299) | def set_paid(self, paid, invoice_number=None):
method to_dict (line 310) | def to_dict(self):
method get_active_timers (line 342) | def get_active_timers(cls):
method get_user_active_timer (line 347) | def get_user_active_timer(cls, user_id):
method get_entries_for_period (line 352) | def get_entries_for_period(cls, start_date=None, end_date=None, user_i...
method get_total_hours_for_period (line 374) | def get_total_hours_for_period(
FILE: app/models/time_entry_approval.py
class ApprovalStatus (line 13) | class ApprovalStatus(enum.Enum):
class TimeEntryApproval (line 22) | class TimeEntryApproval(db.Model):
method __repr__ (line 70) | def __repr__(self):
method to_dict (line 73) | def to_dict(self):
method approve (line 90) | def approve(self, approver_id: int, comment: str = None):
method reject (line 98) | def reject(self, approver_id: int, reason: str):
method cancel (line 106) | def cancel(self):
class ApprovalPolicy (line 112) | class ApprovalPolicy(db.Model):
method __repr__ (line 148) | def __repr__(self):
method to_dict (line 152) | def to_dict(self):
method get_approvers (line 168) | def get_approvers(self):
method applies_to_entry (line 174) | def applies_to_entry(self, time_entry) -> bool:
FILE: app/models/time_entry_template.py
class TimeEntryTemplate (line 6) | class TimeEntryTemplate(db.Model):
method __repr__ (line 39) | def __repr__(self):
method default_duration (line 43) | def default_duration(self):
method default_duration (line 50) | def default_duration(self, hours):
method record_usage (line 57) | def record_usage(self):
method increment_usage (line 62) | def increment_usage(self):
method to_dict (line 68) | def to_dict(self):
FILE: app/models/time_off.py
class TimeOffRequestStatus (line 10) | class TimeOffRequestStatus(enum.Enum):
class LeaveType (line 18) | class LeaveType(db.Model):
method to_dict (line 31) | def to_dict(self):
class TimeOffRequest (line 47) | class TimeOffRequest(db.Model):
method to_dict (line 84) | def to_dict(self):
class CompanyHoliday (line 107) | class CompanyHoliday(db.Model):
method to_dict (line 120) | def to_dict(self):
FILE: app/models/timesheet_period.py
class TimesheetPeriodStatus (line 10) | class TimesheetPeriodStatus(enum.Enum):
class TimesheetPeriod (line 18) | class TimesheetPeriod(db.Model):
method is_locked (line 66) | def is_locked(self) -> bool:
method contains_date (line 72) | def contains_date(self, value: date) -> bool:
method to_dict (line 75) | def to_dict(self):
FILE: app/models/timesheet_policy.py
class TimesheetPolicy (line 6) | class TimesheetPolicy(db.Model):
method get_approver_ids (line 21) | def get_approver_ids(self):
method to_dict (line 35) | def to_dict(self):
FILE: app/models/user.py
class User (line 11) | class User(UserMixin, db.Model):
method __init__ (line 184) | def __init__(self, username, role="user", email=None, full_name=None):
method __repr__ (line 193) | def __repr__(self):
method set_password (line 196) | def set_password(self, password):
method check_password (line 206) | def check_password(self, password):
method has_password (line 216) | def has_password(self):
method set_two_factor_secret (line 220) | def set_two_factor_secret(self, secret: str) -> None:
method get_two_factor_secret (line 230) | def get_two_factor_secret(self) -> str:
method is_admin (line 234) | def is_admin(self):
method is_super_admin (line 243) | def is_super_admin(self):
method active_timer (line 249) | def active_timer(self):
method total_hours (line 256) | def total_hours(self):
method display_name (line 269) | def display_name(self):
method get_recent_entries (line 275) | def get_recent_entries(self, limit=10):
method update_last_login (line 286) | def update_last_login(self):
method is_online (line 291) | def is_online(self):
method get_status (line 300) | def get_status(self):
method to_dict (line 320) | def to_dict(self, total_hours_override=None):
method get_avatar_url (line 362) | def get_avatar_url(self):
method get_avatar_path (line 368) | def get_avatar_path(self):
method has_avatar (line 382) | def has_avatar(self):
method add_favorite_project (line 388) | def add_favorite_project(self, project):
method remove_favorite_project (line 394) | def remove_favorite_project(self, project):
method is_project_favorite (line 400) | def is_project_favorite(self, project):
method get_favorite_projects (line 411) | def get_favorite_projects(self, status="active"):
method has_permission (line 419) | def has_permission(self, permission_name):
method _auto_assign_role_from_legacy (line 446) | def _auto_assign_role_from_legacy(self):
method has_any_permission (line 461) | def has_any_permission(self, *permission_names):
method has_all_permissions (line 465) | def has_all_permissions(self, *permission_names):
method add_role (line 469) | def add_role(self, role):
method remove_role (line 474) | def remove_role(self, role):
method get_all_permissions (line 479) | def get_all_permissions(self):
method get_role_names (line 487) | def get_role_names(self):
method primary_role_name (line 492) | def primary_role_name(self):
method is_scope_restricted (line 503) | def is_scope_restricted(self):
method is_client_portal_user (line 508) | def is_client_portal_user(self):
method get_allowed_client_ids (line 512) | def get_allowed_client_ids(self):
method get_allowed_project_ids (line 523) | def get_allowed_project_ids(self):
method get_client_portal_data (line 538) | def get_client_portal_data(self):
FILE: app/models/user_client.py
class UserClient (line 8) | class UserClient(db.Model):
method __repr__ (line 20) | def __repr__(self):
FILE: app/models/user_favorite_project.py
class UserFavoriteProject (line 6) | class UserFavoriteProject(db.Model):
method __repr__ (line 19) | def __repr__(self):
method to_dict (line 22) | def to_dict(self):
FILE: app/models/user_smart_notification_dismissal.py
class UserSmartNotificationDismissal (line 8) | class UserSmartNotificationDismissal(db.Model):
FILE: app/models/warehouse.py
class Warehouse (line 8) | class Warehouse(db.Model):
method __init__ (line 32) | def __init__(
method __repr__ (line 54) | def __repr__(self):
method to_dict (line 57) | def to_dict(self):
FILE: app/models/warehouse_stock.py
class WarehouseStock (line 9) | class WarehouseStock(db.Model):
method __init__ (line 33) | def __init__(self, warehouse_id, stock_item_id, quantity_on_hand=0, qu...
method __repr__ (line 40) | def __repr__(self):
method quantity_available (line 44) | def quantity_available(self):
method reserve (line 48) | def reserve(self, quantity):
method release_reservation (line 57) | def release_reservation(self, quantity):
method adjust_on_hand (line 65) | def adjust_on_hand(self, quantity):
method record_count (line 73) | def record_count(self, counted_quantity, counted_by=None):
method to_dict (line 81) | def to_dict(self):
FILE: app/models/webhook.py
class Webhook (line 13) | class Webhook(db.Model):
method __repr__ (line 65) | def __repr__(self):
method generate_secret (line 69) | def generate_secret():
method set_secret (line 73) | def set_secret(self, secret=None):
method verify_signature (line 79) | def verify_signature(self, payload, signature):
method generate_signature (line 103) | def generate_signature(self, payload):
method subscribes_to (line 122) | def subscribes_to(self, event_type):
method to_dict (line 135) | def to_dict(self, include_secret=False):
class WebhookDelivery (line 167) | class WebhookDelivery(db.Model):
method __repr__ (line 214) | def __repr__(self):
method hash_payload (line 218) | def hash_payload(payload):
method mark_success (line 224) | def mark_success(self, status_code, response_body=None, response_heade...
method mark_failed (line 241) | def mark_failed(
method mark_retrying (line 261) | def mark_retrying(self, next_retry_at):
method to_dict (line 267) | def to_dict(self):
FILE: app/models/weekly_time_goal.py
function local_now (line 8) | def local_now():
class WeeklyTimeGoal (line 20) | class WeeklyTimeGoal(db.Model):
method __init__ (line 41) | def __init__(self, user_id, target_hours, week_start_date=None, notes=...
method __repr__ (line 82) | def __repr__(self):
method actual_hours (line 86) | def actual_hours(self):
method progress_percentage (line 107) | def progress_percentage(self):
method remaining_hours (line 115) | def remaining_hours(self):
method is_completed (line 121) | def is_completed(self):
method is_overdue (line 126) | def is_overdue(self):
method days_remaining (line 132) | def days_remaining(self):
method average_hours_per_day (line 153) | def average_hours_per_day(self):
method week_label (line 160) | def week_label(self):
method update_status (line 168) | def update_status(self):
method to_dict (line 186) | def to_dict(self):
method get_current_week_goal (line 210) | def get_current_week_goal(user_id):
method get_or_create_current_week (line 231) | def get_or_create_current_week(user_id, default_target_hours=40):
FILE: app/models/workflow.py
class WorkflowRule (line 12) | class WorkflowRule(db.Model):
method __repr__ (line 46) | def __repr__(self):
method to_dict (line 49) | def to_dict(self):
class WorkflowExecution (line 68) | class WorkflowExecution(db.Model):
method __repr__ (line 89) | def __repr__(self):
method to_dict (line 92) | def to_dict(self):
FILE: app/repositories/base_repository.py
class BaseRepository (line 25) | class BaseRepository(Generic[ModelType]):
method __init__ (line 41) | def __init__(self, model: type[ModelType]):
method get_by_id (line 50) | def get_by_id(self, id: int) -> Optional[ModelType]:
method get_all (line 62) | def get_all(self, limit: Optional[int] = None, offset: int = 0) -> Lis...
method find_by (line 78) | def find_by(self, **kwargs) -> List[ModelType]:
method find_one_by (line 90) | def find_one_by(self, **kwargs) -> Optional[ModelType]:
method create (line 102) | def create(self, **kwargs) -> ModelType:
method update (line 116) | def update(self, instance: ModelType, **kwargs) -> ModelType:
method delete (line 132) | def delete(self, instance: ModelType) -> bool:
method count (line 148) | def count(self, **kwargs) -> int:
method exists (line 163) | def exists(self, **kwargs) -> bool:
method query (line 175) | def query(self) -> Query:
FILE: app/repositories/client_repository.py
class ClientRepository (line 14) | class ClientRepository(BaseRepository[Client]):
method __init__ (line 17) | def __init__(self):
method get_with_projects (line 20) | def get_with_projects(self, client_id: int) -> Optional[Client]:
method get_active_clients (line 24) | def get_active_clients(self) -> List[Client]:
method get_by_name (line 28) | def get_by_name(self, name: str) -> Optional[Client]:
FILE: app/repositories/comment_repository.py
class CommentRepository (line 14) | class CommentRepository(BaseRepository[Comment]):
method __init__ (line 17) | def __init__(self):
method get_by_project (line 20) | def get_by_project(
method get_by_task (line 34) | def get_by_task(self, task_id: int, include_replies: bool = True, incl...
method get_by_quote (line 46) | def get_by_quote(
method get_replies (line 67) | def get_replies(self, parent_id: int, include_relations: bool = False)...
FILE: app/repositories/expense_repository.py
class ExpenseRepository (line 15) | class ExpenseRepository(BaseRepository[Expense]):
method __init__ (line 18) | def __init__(self):
method get_by_project (line 21) | def get_by_project(
method get_billable (line 42) | def get_billable(
method get_total_amount (line 59) | def get_total_amount(
FILE: app/repositories/invoice_repository.py
class InvoiceRepository (line 16) | class InvoiceRepository(BaseRepository[Invoice]):
method __init__ (line 19) | def __init__(self):
method get_by_project (line 22) | def get_by_project(self, project_id: int, include_relations: bool = Fa...
method get_by_client (line 31) | def get_by_client(
method get_by_status (line 45) | def get_by_status(self, status: str, include_relations: bool = False) ...
method get_overdue (line 54) | def get_overdue(self, include_relations: bool = False) -> List[Invoice]:
method get_with_relations (line 66) | def get_with_relations(self, invoice_id: int) -> Optional[Invoice]:
method generate_invoice_number (line 70) | def generate_invoice_number(self) -> str:
method mark_as_sent (line 74) | def mark_as_sent(self, invoice_id: int) -> Optional[Invoice]:
method mark_as_paid (line 82) | def mark_as_paid(
FILE: app/repositories/payment_repository.py
class PaymentRepository (line 17) | class PaymentRepository(BaseRepository[Payment]):
method __init__ (line 20) | def __init__(self):
method get_by_invoice (line 23) | def get_by_invoice(self, invoice_id: int, include_relations: bool = Fa...
method get_by_date_range (line 32) | def get_by_date_range(self, start_date: date, end_date: date, include_...
method get_by_status (line 43) | def get_by_status(self, status: str, include_relations: bool = False) ...
method get_total_amount (line 52) | def get_total_amount(
method get_total_for_invoice (line 77) | def get_total_for_invoice(self, invoice_id: int) -> Decimal:
FILE: app/repositories/project_repository.py
class ProjectRepository (line 15) | class ProjectRepository(BaseRepository[Project]):
method __init__ (line 18) | def __init__(self):
method get_active_projects (line 21) | def get_active_projects(
method get_by_client (line 39) | def get_by_client(
method get_with_stats (line 53) | def get_with_stats(self, project_id: int) -> Optional[Project]:
method archive (line 60) | def archive(self, project_id: int, archived_by: int, reason: Optional[...
method unarchive (line 73) | def unarchive(self, project_id: int) -> Optional[Project]:
method get_billable_projects (line 84) | def get_billable_projects(self, client_id: Optional[int] = None) -> Li...
FILE: app/repositories/recurring_invoice_repository.py
class RecurringInvoiceRepository (line 11) | class RecurringInvoiceRepository(BaseRepository[RecurringInvoice]):
method __init__ (line 14) | def __init__(self):
method list_for_user (line 17) | def list_for_user(
FILE: app/repositories/task_repository.py
class TaskRepository (line 15) | class TaskRepository(BaseRepository[Task]):
method __init__ (line 18) | def __init__(self):
method get_by_project (line 21) | def get_by_project(
method get_by_assignee (line 35) | def get_by_assignee(
method get_by_status (line 49) | def get_by_status(
method get_overdue (line 63) | def get_overdue(self, include_relations: bool = False) -> List[Task]:
FILE: app/repositories/time_entry_repository.py
class TimeEntryRepository (line 17) | class TimeEntryRepository(BaseRepository[TimeEntry]):
method __init__ (line 20) | def __init__(self):
method get_active_timer (line 23) | def get_active_timer(self, user_id: int) -> Optional[TimeEntry]:
method get_by_user (line 27) | def get_by_user(
method get_by_project (line 48) | def get_by_project(
method get_by_date_range (line 69) | def get_by_date_range(
method count_for_date_range (line 100) | def count_for_date_range(
method get_billable_entries (line 123) | def get_billable_entries(
method stop_timer (line 151) | def stop_timer(self, entry_id: int, end_time: datetime) -> Optional[Ti...
method create_timer (line 160) | def create_timer(
method create_manual_entry (line 184) | def create_manual_entry(
method get_distinct_project_ids_for_user (line 222) | def get_distinct_project_ids_for_user(self, user_id: int) -> List[int]:
method get_total_duration (line 233) | def get_total_duration(
method get_task_aggregates (line 268) | def get_task_aggregates(
FILE: app/repositories/user_repository.py
class UserRepository (line 13) | class UserRepository(BaseRepository[User]):
method __init__ (line 16) | def __init__(self):
method get_by_username (line 19) | def get_by_username(self, username: str) -> Optional[User]:
method get_by_role (line 23) | def get_by_role(self, role: str) -> List[User]:
method get_active_users (line 27) | def get_active_users(self) -> List[User]:
method get_admins (line 31) | def get_admins(self) -> List[User]:
FILE: app/routes/activity_feed.py
function activity_feed (line 22) | def activity_feed():
function api_activity_feed (line 89) | def api_activity_feed():
FILE: app/routes/admin.py
function _ldap_admin_display (line 55) | def _ldap_admin_display():
function _inject_ldap_admin_display (line 89) | def _inject_ldap_admin_display():
function _convert_json_template_to_html_css (line 101) | def _convert_json_template_to_html_css(template_json, page_size="A4", in...
function admin_required (line 602) | def admin_required(f):
function allowed_logo_file (line 620) | def allowed_logo_file(filename):
function get_upload_folder (line 625) | def get_upload_folder():
function admin_dashboard (line 640) | def admin_dashboard():
function admin_dashboard_alias (line 791) | def admin_dashboard_alias():
function list_users (line 803) | def list_users():
function create_user (line 821) | def create_user():
function edit_user (line 888) | def edit_user(user_id):
function delete_user (line 1046) | def delete_user(user_id):
function telemetry_dashboard (line 1088) | def telemetry_dashboard():
function toggle_telemetry (line 1130) | def toggle_telemetry():
function clear_cache (line 1160) | def clear_cache():
function manage_modules (line 1168) | def manage_modules():
function settings (line 1298) | def settings():
function admin_ldap_test (line 1607) | def admin_ldap_test():
function admin_verify_donate_hide_code (line 1617) | def admin_verify_donate_hide_code():
function pdf_layout (line 1656) | def pdf_layout():
function pdf_layout_reset (line 1960) | def pdf_layout_reset():
function quote_pdf_layout (line 2006) | def quote_pdf_layout():
function quote_pdf_layout_reset (line 2282) | def quote_pdf_layout_reset():
function quote_pdf_layout_export_json (line 2320) | def quote_pdf_layout_export_json(page_size):
function quote_pdf_layout_import_json (line 2355) | def quote_pdf_layout_import_json():
function pdf_layout_export_json (line 2431) | def pdf_layout_export_json(page_size):
function pdf_layout_import_json (line 2466) | def pdf_layout_import_json():
function pdf_layout_debug (line 2542) | def pdf_layout_debug():
function pdf_layout_default (line 2593) | def pdf_layout_default():
function pdf_layout_preview (line 2628) | def pdf_layout_preview():
function quote_pdf_layout_preview (line 3300) | def quote_pdf_layout_preview():
function upload_logo (line 3885) | def upload_logo():
function remove_logo (line 3957) | def remove_logo():
function upload_template_image (line 3986) | def upload_template_image():
function serve_template_image (line 4039) | def serve_template_image(filename):
function serve_uploaded_logo (line 4051) | def serve_uploaded_logo(filename):
function backups_management (line 4073) | def backups_management():
function create_backup_manual (line 4102) | def create_backup_manual():
function download_backup (line 4119) | def download_backup(filename):
function delete_backup (line 4140) | def delete_backup(filename):
function restore (line 4168) | def restore(filename=None):
function system_info (line 4245) | def system_info():
function oidc_debug (line 4286) | def oidc_debug():
function oidc_test (line 4357) | def oidc_test():
function oidc_user_detail (line 4553) | def oidc_user_detail(user_id):
function oidc_setup_wizard (line 4566) | def oidc_setup_wizard():
function oidc_wizard_test_connection (line 4598) | def oidc_wizard_test_connection():
function oidc_wizard_validate_config (line 4667) | def oidc_wizard_validate_config():
function oidc_wizard_generate_config (line 4721) | def oidc_wizard_generate_config():
function _ldap_wizard_truthy (line 4802) | def _ldap_wizard_truthy(val) -> bool:
function _ldap_wizard_int (line 4811) | def _ldap_wizard_int(val, default: int, *, lo: int | None = None, hi: in...
function _ldap_wizard_cfg_from_json (line 4823) | def _ldap_wizard_cfg_from_json(data: dict) -> dict[str, object]:
function _ldap_wizard_escape_env_value (line 4848) | def _ldap_wizard_escape_env_value(value: object) -> str:
function ldap_setup_wizard (line 4859) | def ldap_setup_wizard():
function ldap_wizard_test_connection (line 4894) | def ldap_wizard_test_connection():
function ldap_wizard_validate_config (line 4908) | def ldap_wizard_validate_config():
function ldap_wizard_generate_config (line 4970) | def ldap_wizard_generate_config():
function api_tokens (line 5047) | def api_tokens():
function create_api_token (line 5060) | def create_api_token():
function toggle_api_token (line 5112) | def toggle_api_token(token_id):
function delete_api_token (line 5133) | def delete_api_token(token_id):
function email_support (line 5157) | def email_support():
function test_email (line 5175) | def test_email():
function email_config_status (line 5206) | def email_config_status():
function save_email_config (line 5218) | def save_email_config():
function get_email_config (line 5291) | def get_email_config():
function list_email_templates (line 5319) | def list_email_templates():
function create_email_template (line 5331) | def create_email_template():
function send_email_template_test (line 5393) | def send_email_template_test(template_id):
function view_email_template (line 5431) | def view_email_template(template_id):
function edit_email_template (line 5443) | def edit_email_template(template_id):
function delete_email_template (line 5492) | def delete_email_template(template_id):
function list_integrations_admin (line 5519) | def list_integrations_admin():
function integration_setup (line 5527) | def integration_setup(provider):
FILE: app/routes/analytics.py
function analytics_dashboard (line 18) | def analytics_dashboard():
function hours_by_day (line 38) | def hours_by_day():
function hours_forecast (line 100) | def hours_forecast():
function hours_by_project (line 163) | def hours_by_project():
function hours_by_user (line 221) | def hours_by_user():
function hours_by_hour (line 266) | def hours_by_hour():
function billable_vs_nonbillable (line 312) | def billable_vs_nonbillable():
function weekly_trends (line 358) | def weekly_trends():
function overtime_analytics (line 455) | def overtime_analytics():
function project_efficiency (line 558) | def project_efficiency():
function today_by_task (line 619) | def today_by_task():
function summary_with_comparison (line 687) | def summary_with_comparison():
function task_completion (line 775) | def task_completion():
function revenue_metrics (line 844) | def revenue_metrics():
function insights (line 945) | def insights():
function payments_over_time (line 1064) | def payments_over_time():
function payments_by_status (line 1123) | def payments_by_status():
function payments_by_method (line 1164) | def payments_by_method():
function payment_summary (line 1213) | def payment_summary():
function revenue_vs_payments (line 1295) | def revenue_vs_payments():
FILE: app/routes/api.py
function _ai_error_response (line 38) | def _ai_error_response(exc: AIServiceError):
function health_check (line 44) | def health_check():
function ai_context_preview (line 51) | def ai_context_preview():
function ai_test_connection (line 62) | def ai_test_connection():
function ai_chat (line 74) | def ai_chat():
function ai_confirm_action (line 86) | def ai_confirm_action():
function _effective_user_for_version_api (line 96) | def _effective_user_for_version_api():
function api_version_check (line 110) | def api_version_check():
function api_version_dismiss (line 123) | def api_version_dismiss():
function timer_status (line 155) | def timer_status():
function get_recent_tags (line 180) | def get_recent_tags():
function search (line 210) | def search():
function upcoming_deadlines (line 249) | def upcoming_deadlines():
function list_tasks_for_project (line 290) | def list_tasks_for_project():
function api_start_timer (line 316) | def api_start_timer():
function api_stop_timer (line 368) | def api_stop_timer():
function api_stop_timer_at (line 396) | def api_stop_timer_at():
function api_resume_timer (line 448) | def api_resume_timer():
function get_entries (line 510) | def get_entries():
function project_burndown (line 575) | def project_burndown(project_id):
function start_focus_session (line 636) | def start_focus_session():
function finish_focus_session (line 669) | def finish_focus_session():
function focus_sessions_summary (line 690) | def focus_sessions_summary():
function recurring_blocks_list_create (line 704) | def recurring_blocks_list_create():
function recurring_block_update_delete (line 761) | def recurring_block_update_delete(block_id):
function saved_filters_list_create (line 798) | def saved_filters_list_create():
function delete_saved_filter (line 820) | def delete_saved_filter(filter_id):
function create_entry (line 833) | def create_entry():
function bulk_entries_action (line 935) | def bulk_entries_action():
function calendar_events (line 961) | def calendar_events():
function calendar_export (line 1122) | def calendar_export():
function get_projects (line 1247) | def get_projects():
function get_project_tasks (line 1256) | def get_project_tasks(project_id):
function create_task_inline (line 1287) | def create_task_inline():
function get_entry (line 1405) | def get_entry(entry_id):
function get_users (line 1417) | def get_users():
function get_stats (line 1442) | def get_stats():
function value_dashboard_stats (line 1486) | def value_dashboard_stats():
function week_comparison (line 1495) | def week_comparison():
function update_entry (line 1573) | def update_entry(entry_id):
function delete_entry (line 1671) | def delete_entry(entry_id):
function allowed_image_file (line 1708) | def allowed_image_file(filename: str) -> bool:
function get_editor_upload_folder (line 1712) | def get_editor_upload_folder() -> str:
function upload_editor_image (line 1720) | def upload_editor_image():
function upload_editor_images_bulk (line 1743) | def upload_editor_images_bulk():
function serve_editor_image (line 1787) | def serve_editor_image(filename):
function get_activities (line 1801) | def get_activities():
function dashboard_stats (line 1868) | def dashboard_stats():
function dashboard_sparklines (line 1909) | def dashboard_sparklines():
function summary_today (line 1957) | def summary_today():
function api_smart_notifications (line 1967) | def api_smart_notifications():
function api_smart_notifications_dismiss (line 1976) | def api_smart_notifications_dismiss():
function activity_timeline (line 1995) | def activity_timeline():
function get_activity_stats (line 2028) | def get_activity_stats():
function handle_connect (line 2091) | def handle_connect():
function handle_disconnect (line 2097) | def handle_disconnect():
function handle_join_user_room (line 2103) | def handle_join_user_room(data):
function handle_leave_user_room (line 2112) | def handle_leave_user_room(data):
function _get_client_id_from_session (line 2121) | def _get_client_id_from_session():
function handle_join_client_room (line 2139) | def handle_join_client_room(data):
function handle_leave_client_room (line 2149) | def handle_leave_client_room(data):
FILE: app/routes/api_docs.py
function openapi_spec (line 32) | def openapi_spec():
FILE: app/routes/api_v1.py
function api_info (line 90) | def api_info():
function health_check (line 206) | def health_check():
function auth_login (line 223) | def auth_login():
function list_time_entry_approvals (line 274) | def list_time_entry_approvals():
function get_time_entry_approval (line 288) | def get_time_entry_approval(approval_id):
function approve_time_entry (line 306) | def approve_time_entry(approval_id):
function reject_time_entry (line 323) | def reject_time_entry(approval_id):
function cancel_time_entry_approval (line 343) | def cancel_time_entry_approval(approval_id):
function request_time_entry_approval (line 359) | def request_time_entry_approval(entry_id):
function bulk_approve_time_entries (line 381) | def bulk_approve_time_entries():
function list_per_diems (line 402) | def list_per_diems():
function get_per_diem (line 438) | def get_per_diem(pd_id):
function create_per_diem (line 456) | def create_per_diem():
function update_per_diem (line 503) | def update_per_diem(pd_id):
function delete_per_diem (line 549) | def delete_per_diem(pd_id):
function list_per_diem_rates (line 569) | def list_per_diem_rates():
function create_per_diem_rate (line 598) | def create_per_diem_rate():
function list_budget_alerts (line 643) | def list_budget_alerts():
function create_budget_alert (line 680) | def create_budget_alert():
function acknowledge_budget_alert (line 707) | def acknowledge_budget_alert(alert_id):
function list_calendar_events (line 726) | def list_calendar_events():
function get_calendar_event (line 759) | def get_calendar_event(event_id):
function create_calendar_event (line 777) | def create_calendar_event():
function update_calendar_event (line 815) | def update_calendar_event(event_id):
function delete_calendar_event (line 846) | def delete_calendar_event(event_id):
function list_kanban_columns (line 869) | def list_kanban_columns():
function create_kanban_column (line 886) | def create_kanban_column():
function update_kanban_column (line 915) | def update_kanban_column(col_id):
function delete_kanban_column (line 935) | def delete_kanban_column(col_id):
function reorder_kanban_columns (line 955) | def reorder_kanban_columns():
function list_saved_filters (line 975) | def list_saved_filters():
function get_saved_filter (line 1007) | def get_saved_filter(filter_id):
function create_saved_filter (line 1025) | def create_saved_filter():
function update_saved_filter (line 1050) | def update_saved_filter(filter_id):
function delete_saved_filter (line 1073) | def delete_saved_filter(filter_id):
function list_time_entry_templates (line 1096) | def list_time_entry_templates():
function get_time_entry_template (line 1128) | def get_time_entry_template(tpl_id):
function create_time_entry_template (line 1150) | def create_time_entry_template():
function update_time_entry_template (line 1179) | def update_time_entry_template(tpl_id):
function delete_time_entry_template (line 1215) | def delete_time_entry_template(tpl_id):
function list_comments (line 1242) | def list_comments():
function create_comment (line 1268) | def create_comment():
function list_quotes (line 1292) | def list_quotes():
function get_quote (line 1345) | def get_quote(quote_id):
function create_quote (line 1369) | def create_quote():
function update_quote (line 1454) | def update_quote(quote_id):
function delete_quote (line 1545) | def delete_quote(quote_id):
function update_comment (line 1578) | def update_comment(comment_id):
function delete_comment (line 1608) | def delete_comment(comment_id):
function list_client_notes (line 1634) | def list_client_notes(client_id):
function create_client_note (line 1665) | def create_client_note(client_id):
function get_client_note (line 1684) | def get_client_note(note_id):
function update_client_note (line 1701) | def update_client_note(note_id):
function delete_client_note (line 1728) | def delete_client_note(note_id):
function list_project_costs (line 1753) | def list_project_costs(project_id):
function create_project_cost (line 1795) | def create_project_cost(project_id):
function get_project_cost (line 1829) | def get_project_cost(cost_id):
function update_project_cost (line 1843) | def update_project_cost(cost_id):
function delete_project_cost (line 1872) | def delete_project_cost(cost_id):
function list_tax_rules (line 1891) | def list_tax_rules():
function create_tax_rule (line 1921) | def create_tax_rule():
function update_tax_rule (line 1954) | def update_tax_rule(rule_id):
function delete_tax_rule (line 1994) | def delete_tax_rule(rule_id):
function list_currencies (line 2006) | def list_currencies():
function create_currency (line 2026) | def create_currency():
function update_currency (line 2049) | def update_currency(code):
function list_exchange_rates (line 2061) | def list_exchange_rates():
function create_exchange_rate (line 2094) | def create_exchange_rate():
function update_exchange_rate (line 2123) | def update_exchange_rate(rate_id):
function list_favorite_projects (line 2148) | def list_favorite_projects():
function add_favorite_project (line 2155) | def add_favorite_project():
function remove_favorite_project (line 2172) | def remove_favorite_project(project_id):
function list_audit_logs (line 2184) | def list_audit_logs():
function list_activities (line 2206) | def list_activities():
function list_invoice_pdf_templates (line 2220) | def list_invoice_pdf_templates():
function get_invoice_pdf_template (line 2227) | def get_invoice_pdf_template(page_size):
function list_invoice_templates (line 2237) | def list_invoice_templates():
function get_invoice_template (line 2261) | def get_invoice_template(template_id):
function create_invoice_template (line 2281) | def create_invoice_template():
function update_invoice_template (line 2306) | def update_invoice_template(template_id):
function delete_invoice_template (line 2331) | def delete_invoice_template(template_id):
function list_recurring_invoices (line 2344) | def list_recurring_invoices():
function get_recurring_invoice (line 2386) | def get_recurring_invoice(ri_id):
function create_recurring_invoice (line 2401) | def create_recurring_invoice():
function update_recurring_invoice (line 2447) | def update_recurring_invoice(ri_id):
function delete_recurring_invoice (line 2495) | def delete_recurring_invoice(ri_id):
function generate_from_recurring_invoice (line 2505) | def generate_from_recurring_invoice(ri_id):
function list_credit_notes (line 2520) | def list_credit_notes():
function get_credit_note (line 2572) | def get_credit_note(cn_id):
function create_credit_note (line 2595) | def create_credit_note():
function update_credit_note (line 2643) | def update_credit_note(cn_id):
function delete_credit_note (line 2665) | def delete_credit_note(cn_id):
function report_summary (line 2681) | def report_summary():
function get_current_user (line 2783) | def get_current_user():
function list_users (line 2808) | def list_users():
function list_webhooks (line 2859) | def list_webhooks():
function create_webhook (line 2901) | def create_webhook():
function get_webhook (line 2972) | def get_webhook(webhook_id):
function update_webhook (line 3003) | def update_webhook(webhook_id):
function delete_webhook (line 3086) | def delete_webhook(webhook_id):
function list_webhook_deliveries (line 3120) | def list_webhook_deliveries(webhook_id):
function list_webhook_events (line 3171) | def list_webhook_events():
function list_stock_items_api (line 3194) | def list_stock_items_api():
function get_stock_item_api (line 3220) | def get_stock_item_api(item_id):
function create_stock_item_api (line 3228) | def create_stock_item_api():
function update_stock_item_api (line 3264) | def update_stock_item_api(item_id):
function delete_stock_item_api (line 3304) | def delete_stock_item_api(item_id):
function get_stock_availability_api (line 3321) | def get_stock_availability_api(item_id):
function list_warehouses_api (line 3351) | def list_warehouses_api():
function get_stock_levels_api (line 3367) | def get_stock_levels_api():
function create_stock_movement_api (line 3404) | def create_stock_movement_api():
function list_transfers_api (line 3655) | def list_transfers_api():
function create_transfer_api (line 3734) | def create_transfer_api():
function get_transfer_api (line 3842) | def get_transfer_api(reference_id):
function get_inventory_valuation_report_api (line 3881) | def get_inventory_valuation_report_api():
function get_inventory_movement_history_report_api (line 3903) | def get_inventory_movement_history_report_api():
function get_inventory_turnover_report_api (line 3935) | def get_inventory_turnover_report_api():
function get_inventory_low_stock_report_api (line 3966) | def get_inventory_low_stock_report_api():
function list_suppliers_api (line 3985) | def list_suppliers_api():
function get_supplier_api (line 4011) | def get_supplier_api(supplier_id):
function create_supplier_api (line 4021) | def create_supplier_api():
function update_supplier_api (line 4060) | def update_supplier_api(supplier_id):
function delete_supplier_api (line 4102) | def delete_supplier_api(supplier_id):
function get_supplier_stock_items_api (line 4121) | def get_supplier_stock_items_api(supplier_id):
function list_purchase_orders_api (line 4146) | def list_purchase_orders_api():
function get_purchase_order_api (line 4171) | def get_purchase_order_api(po_id):
function create_purchase_order_api (line 4181) | def create_purchase_order_api():
function update_purchase_order_api (line 4289) | def update_purchase_order_api(po_id):
function delete_purchase_order_api (line 4371) | def delete_purchase_order_api(po_id):
function receive_purchase_order_api (line 4405) | def receive_purchase_order_api(po_id):
function search (line 4451) | def search():
function _is_api_approver (line 4549) | def _is_api_approver(user) -> bool:
function list_timesheet_periods (line 4564) | def list_timesheet_periods():
function create_or_get_timesheet_period (line 4582) | def create_or_get_timesheet_period():
function submit_timesheet_period (line 4600) | def submit_timesheet_period(period_id):
function approve_timesheet_period (line 4611) | def approve_timesheet_period(period_id):
function reject_timesheet_period (line 4628) | def reject_timesheet_period(period_id):
function close_timesheet_period (line 4647) | def close_timesheet_period(period_id):
function delete_timesheet_period_api (line 4664) | def delete_timesheet_period_api(period_id):
function get_timesheet_policy (line 4675) | def get_timesheet_policy():
function update_timesheet_policy (line 4686) | def update_timesheet_policy():
function list_leave_types_api (line 4720) | def list_leave_types_api():
function create_leave_type_api (line 4730) | def create_leave_type_api():
function delete_leave_type_api (line 4757) | def delete_leave_type_api(leave_type_id):
function list_time_off_requests_api (line 4770) | def list_time_off_requests_api():
function create_time_off_request_api (line 4798) | def create_time_off_request_api():
function approve_time_off_request_api (line 4833) | def approve_time_off_request_api(request_id):
function reject_time_off_request_api (line 4850) | def reject_time_off_request_api(request_id):
function delete_time_off_request_api (line 4867) | def delete_time_off_request_api(request_id):
function time_off_balances_api (line 4882) | def time_off_balances_api():
function list_holidays_api (line 4894) | def list_holidays_api():
function create_holiday_api (line 4910) | def create_holiday_api():
function delete_holiday_api (line 4933) | def delete_holiday_api(holiday_id):
function export_payroll_csv (line 4949) | def export_payroll_csv():
function capacity_report_api (line 5018) | def capacity_report_api():
function compliance_locked_periods_api (line 5047) | def compliance_locked_periods_api():
function compliance_audit_events_api (line 5061) | def compliance_audit_events_api():
function mileage_gps_start_api (line 5080) | def mileage_gps_start_api():
function mileage_gps_add_point_api (line 5097) | def mileage_gps_add_point_api(track_id):
function mileage_gps_stop_api (line 5114) | def mileage_gps_stop_api(track_id):
function mileage_gps_create_expense_api (line 5131) | def mileage_gps_create_expense_api(track_id):
function mileage_gps_list_api (line 5147) | def mileage_gps_list_api():
function not_found (line 5165) | def not_found(error):
function internal_error (line 5171) | def internal_error(error):
FILE: app/routes/api_v1_ai.py
function _ai_error_response (line 11) | def _ai_error_response(exc: AIServiceError):
function ai_context_preview (line 17) | def ai_context_preview():
function ai_chat (line 27) | def ai_chat():
function ai_confirm_action (line 38) | def ai_confirm_action():
FILE: app/routes/api_v1_clients.py
function list_clients (line 19) | def list_clients():
function get_client (line 50) | def get_client(client_id):
function create_client (line 67) | def create_client():
function _resolve_actor_for_invoice_unbilled (line 98) | def _resolve_actor_for_invoice_unbilled():
function post_client_invoice_unbilled (line 177) | def post_client_invoice_unbilled(client_id):
FILE: app/routes/api_v1_common.py
function paginate_query (line 9) | def paginate_query(query, page=None, per_page=None):
function parse_datetime (line 32) | def parse_datetime(dt_str):
function _parse_date (line 52) | def _parse_date(dstr):
function _parse_date_range (line 64) | def _parse_date_range(start_date_str, end_date_str):
function _require_module_enabled_for_api (line 75) | def _require_module_enabled_for_api(module_id: str):
FILE: app/routes/api_v1_contacts.py
function list_contacts (line 19) | def list_contacts(client_id):
function create_contact (line 35) | def create_contact(client_id):
function get_contact (line 77) | def get_contact(contact_id):
function update_contact (line 88) | def update_contact(contact_id):
function delete_contact (line 128) | def delete_contact(contact_id):
FILE: app/routes/api_v1_deals.py
function list_deals (line 21) | def list_deals():
function get_deal (line 61) | def get_deal(deal_id):
function create_deal (line 74) | def create_deal():
function update_deal (line 119) | def update_deal(deal_id):
function delete_deal (line 157) | def delete_deal(deal_id):
FILE: app/routes/api_v1_expenses.py
function list_expenses (line 19) | def list_expenses():
function get_expense (line 67) | def get_expense(expense_id):
function create_expense (line 85) | def create_expense():
function update_expense (line 137) | def update_expense(expense_id):
function delete_expense (line 171) | def delete_expense(expense_id):
FILE: app/routes/api_v1_invoices.py
function list_invoices (line 18) | def list_invoices():
function get_invoice (line 49) | def get_invoice(invoice_id):
function create_invoice (line 65) | def create_invoice():
function update_invoice (line 114) | def update_invoice(invoice_id):
function delete_invoice (line 153) | def delete_invoice(invoice_id):
FILE: app/routes/api_v1_leads.py
function list_leads (line 22) | def list_leads():
function get_lead (line 71) | def get_lead(lead_id):
function create_lead (line 84) | def create_lead():
function update_lead (line 124) | def update_lead(lead_id):
function delete_lead (line 164) | def delete_lead(lead_id):
FILE: app/routes/api_v1_mileage.py
function list_mileage (line 21) | def list_mileage():
function get_mileage (line 63) | def get_mileage(entry_id):
function create_mileage (line 79) | def create_mileage():
function update_mileage (line 126) | def update_mileage(entry_id):
function delete_mileage (line 174) | def delete_mileage(entry_id):
FILE: app/routes/api_v1_payments.py
function list_payments (line 19) | def list_payments():
function get_payment (line 48) | def get_payment(payment_id):
function create_payment (line 60) | def create_payment():
function update_payment (line 101) | def update_payment(payment_id):
function delete_payment (line 128) | def delete_payment(payment_id):
FILE: app/routes/api_v1_projects.py
function list_projects (line 23) | def list_projects():
function get_project (line 58) | def get_project(project_id):
function create_project (line 76) | def create_project():
function update_project (line 114) | def update_project(project_id):
function delete_project (line 150) | def delete_project(project_id):
FILE: app/routes/api_v1_tasks.py
function list_tasks (line 17) | def list_tasks():
function get_task (line 51) | def get_task(task_id):
function create_task (line 67) | def create_task():
function update_task (line 99) | def update_task(task_id):
function delete_task (line 131) | def delete_task(task_id):
FILE: app/routes/api_v1_time_entries.py
function list_time_entries (line 24) | def list_time_entries():
function get_time_entry (line 75) | def get_time_entry(entry_id):
function import_time_entries_csv (line 93) | def import_time_entries_csv():
function bulk_time_entries (line 115) | def bulk_time_entries():
function create_time_entry (line 145) | def create_time_entry():
function update_time_entry (line 246) | def update_time_entry(entry_id):
function delete_time_entry (line 318) | def delete_time_entry(entry_id):
function timer_status (line 341) | def timer_status():
function start_timer (line 351) | def start_timer():
function pause_timer (line 392) | def pause_timer():
function resume_timer (line 409) | def resume_timer():
function stop_timer (line 426) | def stop_timer():
FILE: app/routes/audit_logs.py
function list_audit_logs (line 20) | def list_audit_logs():
function view_audit_log (line 111) | def view_audit_log(log_id):
function entity_history (line 124) | def entity_history(entity_type, entity_id):
function api_audit_logs (line 203) | def api_audit_logs():
function audit_logs_status (line 233) | def audit_logs_status():
FILE: app/routes/auth.py
function allowed_avatar_file (line 37) | def allowed_avatar_file(filename: str) -> bool:
function get_avatar_upload_folder (line 41) | def get_avatar_upload_folder() -> str:
function _login_template_vars (line 51) | def _login_template_vars():
function _password_reset_serializer (line 73) | def _password_reset_serializer():
function _make_password_reset_token (line 79) | def _make_password_reset_token(user: User) -> str:
function _verify_password_reset_token (line 84) | def _verify_password_reset_token(token: str, *, max_age_seconds: int) ->...
function _finalize_login_after_verification (line 106) | def _finalize_login_after_verification(user: User, *, log_auth_method: s...
function forgot_password (line 168) | def forgot_password():
function reset_password (line 235) | def reset_password(token: str):
function login (line 282) | def login():
function two_factor (line 545) | def two_factor():
function two_factor_setup (line 590) | def two_factor_setup():
function logout (line 683) | def logout():
function profile (line 755) | def profile():
function edit_profile (line 762) | def edit_profile():
function change_password (line 870) | def change_password():
function remove_avatar (line 923) | def remove_avatar():
function serve_uploaded_avatar (line 947) | def serve_uploaded_avatar(filename):
function update_theme_preference (line 954) | def update_theme_preference():
function login_oidc (line 977) | def login_oidc():
function oidc_callback (line 1126) | def oidc_callback():
FILE: app/routes/budget_alerts.py
function budget_dashboard (line 33) | def budget_dashboard():
function get_burn_rate (line 96) | def get_burn_rate(project_id):
function get_completion_estimate (line 124) | def get_completion_estimate(project_id):
function get_resource_allocation (line 151) | def get_resource_allocation(project_id):
function get_cost_trends (line 178) | def get_cost_trends(project_id):
function get_project_budget_status (line 210) | def get_project_budget_status(project_id):
function get_alerts (line 234) | def get_alerts():
function acknowledge_alert (line 261) | def acknowledge_alert(alert_id):
function check_project_alerts (line 287) | def check_project_alerts(project_id):
function project_budget_detail (line 321) | def project_budget_detail(project_id):
function get_budget_summary (line 378) | def get_budget_summary():
FILE: app/routes/calendar.py
function view_calendar (line 25) | def view_calendar():
function get_events (line 76) | def get_events():
function create_event (line 170) | def create_event():
function get_event (line 229) | def get_event(event_id):
function update_event (line 242) | def update_event(event_id):
function delete_event (line 309) | def delete_event(event_id):
function move_event (line 344) | def move_event(event_id):
function resize_event (line 375) | def resize_event(event_id):
function view_event (line 409) | def view_event(event_id):
function new_event (line 423) | def new_event():
function edit_event (line 460) | def edit_event(event_id):
function list_integrations (line 481) | def list_integrations():
function connect_google (line 489) | def connect_google():
function disconnect_integration (line 496) | def disconnect_integration(integration_id):
FILE: app/routes/client_notes.py
function _enforce_clients_module (line 14) | def _enforce_clients_module():
function create_note (line 29) | def create_note(client_id):
function edit_note (line 66) | def edit_note(client_id, note_id):
function delete_note (line 112) | def delete_note(client_id, note_id):
function toggle_important (line 148) | def toggle_important(client_id, note_id):
function list_notes (line 187) | def list_notes(client_id):
function get_note (line 206) | def get_note(note_id):
function get_important_notes (line 220) | def get_important_notes():
function get_recent_notes (line 236) | def get_recent_notes():
function get_user_notes (line 252) | def get_user_notes(user_id):
FILE: app/routes/client_portal.py
function handle_forbidden (line 56) | def handle_forbidden(error):
function handle_not_found (line 97) | def handle_not_found(error):
function handle_internal_error (line 116) | def handle_internal_error(error):
function get_current_client (line 137) | def get_current_client():
function inject_get_current_client (line 156) | def inject_get_current_client():
function check_client_portal_access (line 187) | def check_client_portal_access():
function get_portal_data (line 270) | def get_portal_data(client):
function get_dashboard_preferences (line 292) | def get_dashboard_preferences(client_id, user_id=None):
function get_effective_widget_layout (line 306) | def get_effective_widget_layout(client_id, user_id=None):
function login (line 316) | def login():
function logout (line 354) | def logout():
function set_password (line 362) | def set_password():
function client_portal_base (line 409) | def client_portal_base():
function dashboard (line 419) | def dashboard():
function dashboard_preferences_get (line 496) | def dashboard_preferences_get():
function dashboard_preferences_post (line 508) | def dashboard_preferences_post():
function projects (line 555) | def projects():
function invoices (line 581) | def invoices():
function view_invoice (line 610) | def view_invoice(invoice_id):
function quotes (line 627) | def quotes():
function view_quote (line 643) | def view_quote(quote_id):
function time_entries (line 660) | def time_entries():
function issues (line 709) | def issues():
function new_issue (line 743) | def new_issue():
function view_issue (line 835) | def view_issue(issue_id):
function time_entry_approvals (line 860) | def time_entry_approvals():
function view_approval (line 903) | def view_approval(approval_id):
function approve_time_entry (line 923) | def approve_time_entry(approval_id):
function reject_time_entry (line 963) | def reject_time_entry(approval_id):
function accept_quote (line 1009) | def accept_quote(quote_id):
function reject_quote (line 1054) | def reject_quote(quote_id):
function pay_invoice (line 1105) | def pay_invoice(invoice_id):
function project_comments (line 1142) | def project_comments(project_id):
function notifications (line 1199) | def notifications():
function mark_notification_read (line 1228) | def mark_notification_read(notification_id):
function mark_all_notifications_read (line 1247) | def mark_all_notifications_read():
function documents (line 1267) | def documents():
function download_attachment (line 1316) | def download_attachment(attachment_id):
function _report_days_from_request (line 1364) | def _report_days_from_request():
function reports (line 1373) | def reports():
function _reports_csv_response (line 1407) | def _reports_csv_response(client, report_data, date_range_days):
function activity_feed (line 1448) | def activity_feed():
FILE: app/routes/client_portal_customization.py
function allowed_file (line 34) | def allowed_file(filename):
function get_upload_folder (line 38) | def get_upload_folder():
function edit_customization (line 47) | def edit_customization(client_id):
function update_customization (line 66) | def update_customization(client_id):
function serve_portal_upload (line 144) | def serve_portal_upload(filename):
FILE: app/routes/clients.py
function _wants_json_response (line 41) | def _wants_json_response() -> bool:
function _enforce_clients_module (line 54) | def _enforce_clients_module():
function list_clients (line 77) | def list_clients():
function create_client (line 230) | def create_client():
function view_client (line 432) | def view_client(client_id):
function edit_client (line 573) | def edit_client(client_id):
function send_portal_password_email (line 736) | def send_portal_password_email(client_id):
function archive_client (line 809) | def archive_client(client_id):
function activate_client (line 838) | def activate_client(client_id):
function delete_client (line 865) | def delete_client(client_id):
function bulk_delete_clients (line 924) | def bulk_delete_clients():
function bulk_status_change (line 1014) | def bulk_status_change():
function export_clients (line 1084) | def export_clients():
function api_clients (line 1226) | def api_clients():
function upload_client_attachment (line 1245) | def upload_client_attachment(client_id):
function download_client_attachment (line 1363) | def download_client_attachment(attachment_id):
function delete_client_attachment (line 1387) | def delete_client_attachment(attachment_id):
FILE: app/routes/comments.py
function create_comment (line 18) | def create_comment():
function edit_comment (line 103) | def edit_comment(comment_id):
function delete_comment (line 146) | def delete_comment(comment_id):
function list_comments (line 186) | def list_comments():
function get_comment (line 223) | def get_comment(comment_id):
function get_recent_comments (line 235) | def get_recent_comments():
function get_user_comments (line 249) | def get_user_comments(user_id):
function upload_comment_attachment (line 268) | def upload_comment_attachment(comment_id):
function download_attachment (line 360) | def download_attachment(attachment_id):
function delete_attachment (line 379) | def delete_attachment(attachment_id):
FILE: app/routes/contacts.py
function list_contacts (line 21) | def list_contacts(client_id):
function create_contact (line 30) | def create_contact(client_id):
function view_contact (line 73) | def view_contact(contact_id):
function edit_contact (line 82) | def edit_contact(contact_id):
function delete_contact (line 120) | def delete_contact(contact_id):
function set_primary_contact (line 139) | def set_primary_contact(contact_id):
function create_communication (line 156) | def create_communication(contact_id):
FILE: app/routes/custom_field_definitions.py
function list_custom_field_definitions (line 20) | def list_custom_field_definitions():
function create_custom_field_definition (line 32) | def create_custom_field_definition():
function edit_custom_field_definition (line 87) | def edit_custom_field_definition(definition_id):
function delete_custom_field_definition (line 143) | def delete_custom_field_definition(definition_id):
FILE: app/routes/custom_reports.py
function report_builder (line 29) | def report_builder(view_id=None):
function save_report_view (line 86) | def save_report_view():
function view_custom_report (line 186) | def view_custom_report(view_id):
function preview_report (line 220) | def preview_report():
function get_report_data (line 252) | def get_report_data(view_id):
function generate_report_data (line 275) | def generate_report_data(config, user_id=None):
function list_saved_views (line 638) | def list_saved_views():
function edit_saved_view (line 655) | def edit_saved_view(view_id):
function get_custom_field_values (line 671) | def get_custom_field_values():
function delete_saved_view (line 693) | def delete_saved_view(view_id):
function _generate_iterative_reports (line 723) | def _generate_iterative_reports(saved_view: SavedReportView, config: dic...
FILE: app/routes/deals.py
function list_deals (line 26) | def list_deals():
function pipeline_view (line 70) | def pipeline_view():
function create_deal (line 96) | def create_deal():
function view_deal (line 162) | def view_deal(deal_id):
function edit_deal (line 179) | def edit_deal(deal_id):
function close_won (line 242) | def close_won(deal_id):
function close_lost (line 268) | def close_lost(deal_id):
function create_activity (line 296) | def create_activity(deal_id):
function get_deal_contacts (line 337) | def get_deal_contacts(deal_id):
FILE: app/routes/expense_categories.py
function list_categories (line 19) | def list_categories():
function create_category (line 38) | def create_category():
function view_category (line 107) | def view_category(category_id):
function edit_category (line 132) | def edit_category(category_id):
function delete_category (line 193) | def delete_category(category_id):
function api_list_categories (line 221) | def api_list_categories():
function api_get_category (line 230) | def api_get_category(category_id):
function api_budget_alerts (line 240) | def api_budget_alerts():
FILE: app/routes/expenses.py
function allowed_file (line 38) | def allowed_file(filename):
function get_receipt_upload_folder (line 43) | def get_receipt_upload_folder():
function list_expenses (line 84) | def list_expenses():
function create_expense (line 250) | def create_expense():
function view_expense (line 429) | def view_expense(expense_id):
function serve_receipt (line 449) | def serve_receipt(filename):
function edit_expense (line 501) | def edit_expense(expense_id):
function delete_expense (line 656) | def delete_expense(expense_id):
function bulk_delete_expenses (line 699) | def bulk_delete_expenses():
function bulk_update_status (line 769) | def bulk_update_status():
function approve_expense (line 833) | def approve_expense(expense_id):
function reject_expense (line 865) | def reject_expense(expense_id):
function mark_reimbursed (line 901) | def mark_reimbursed(expense_id):
function export_expenses (line 936) | def export_expenses():
function dashboard (line 1049) | def dashboard():
function api_list_expenses (line 1137) | def api_list_expenses():
function api_get_expense (line 1178) | def api_get_expense(expense_id):
function api_scan_receipt (line 1191) | def api_scan_receipt():
function scan_receipt_page (line 1253) | def scan_receipt_page():
function create_expense_from_scan (line 1340) | def create_expense_from_scan():
FILE: app/routes/gantt.py
function gantt_view (line 21) | def gantt_view():
function gantt_data (line 31) | def gantt_data():
FILE: app/routes/import_export.py
function import_export_page (line 38) | def import_export_page():
function import_csv (line 46) | def import_csv():
function import_toggl (line 89) | def import_toggl():
function import_harvest (line 148) | def import_harvest():
function import_status (line 207) | def import_status(import_id):
function import_history (line 221) | def import_history():
function export_gdpr (line 241) | def export_gdpr():
function export_filtered (line 293) | def export_filtered():
function export_backup (line 361) | def export_backup():
function download_export (line 405) | def download_export(export_id):
function export_status (line 433) | def export_status(export_id):
function export_history (line 447) | def export_history():
function restore_backup (line 467) | def restore_backup():
function start_migration_wizard (line 558) | def start_migration_wizard():
function preview_migration (line 598) | def preview_migration(wizard_id):
function execute_migration (line 615) | def execute_migration(wizard_id):
function import_csv_clients_route (line 634) | def import_csv_clients_route():
function download_csv_template (line 747) | def download_csv_template():
function download_csv_template_clients (line 766) | def download_csv_template_clients():
FILE: app/routes/integrations.py
function has_setup_wizard (line 30) | def has_setup_wizard(provider):
function list_integrations (line 43) | def list_integrations():
function integrations_health (line 64) | def integrations_health():
function connect_integration (line 99) | def connect_integration(provider):
function oauth_callback (line 201) | def oauth_callback(provider):
function manage_integration (line 319) | def manage_integration(provider):
function view_integration (line 798) | def view_integration(integration_id):
function test_integration (line 846) | def test_integration(integration_id):
function delete_integration (line 889) | def delete_integration(integration_id):
function reset_integration (line 914) | def reset_integration(integration_id):
function sync_integration (line 942) | def sync_integration(integration_id):
function caldav_setup (line 1009) | def caldav_setup():
function activitywatch_setup (line 1180) | def activitywatch_setup():
function integration_webhook (line 1299) | def integration_webhook(provider):
function setup_wizard (line 1361) | def setup_wizard(provider):
function test_connection_wizard (line 1579) | def test_connection_wizard(provider):
FILE: app/routes/inventory.py
function _provisional_po_number (line 35) | def _provisional_po_number():
function _finalize_po_number (line 40) | def _finalize_po_number(purchase_order):
function search_stock_items (line 53) | def search_stock_items():
function get_item_availability (line 90) | def get_item_availability(item_id):
function list_stock_items (line 122) | def list_stock_items():
function new_stock_item (line 179) | def new_stock_item():
function view_stock_item (line 313) | def view_stock_item(item_id):
function edit_stock_item (line 437) | def edit_stock_item(item_id):
function delete_stock_item (line 581) | def delete_stock_item(item_id):
function list_warehouses (line 613) | def list_warehouses():
function new_warehouse (line 631) | def new_warehouse():
function view_warehouse (line 673) | def view_warehouse(warehouse_id):
function edit_warehouse (line 699) | def edit_warehouse(warehouse_id):
function delete_warehouse (line 739) | def delete_warehouse(warehouse_id):
function stock_levels (line 770) | def stock_levels():
function stock_levels_by_warehouse (line 819) | def stock_levels_by_warehouse(warehouse_id):
function stock_levels_by_item (line 864) | def stock_levels_by_item(item_id):
function list_movements (line 880) | def list_movements():
function new_movement (line 916) | def new_movement():
function list_transfers (line 1187) | def list_transfers():
function new_transfer (line 1230) | def new_transfer():
function list_adjustments (line 1341) | def list_adjustments():
function new_adjustment (line 1394) | def new_adjustment():
function stock_item_history (line 1446) | def stock_item_history(item_id):
function low_stock_alerts (line 1503) | def low_stock_alerts():
function list_reservations (line 1547) | def list_reservations():
function fulfill_reservation (line 1565) | def fulfill_reservation(reservation_id):
function cancel_reservation (line 1586) | def cancel_reservation(reservation_id):
function list_suppliers (line 1610) | def list_suppliers():
function new_supplier (line 1633) | def new_supplier():
function view_supplier (line 1680) | def view_supplier(supplier_id):
function edit_supplier (line 1703) | def edit_supplier(supplier_id):
function delete_supplier (line 1749) | def delete_supplier(supplier_id):
function list_purchase_orders (line 1781) | def list_purchase_orders():
function new_purchase_order (line 1811) | def new_purchase_order():
function view_purchase_order (line 1946) | def view_purchase_order(po_id):
function edit_purchase_order (line 1961) | def edit_purchase_order(po_id):
function send_purchase_order (line 2071) | def send_purchase_order(po_id):
function cancel_purchase_order (line 2095) | def cancel_purchase_order(po_id):
function delete_purchase_order (line 2118) | def delete_purchase_order(po_id):
function receive_purchase_order (line 2148) | def receive_purchase_order(po_id):
function reports_dashboard (line 2194) | def reports_dashboard():
function reports_valuation (line 2247) | def reports_valuation():
function reports_movement_history (line 2320) | def reports_movement_history():
function reports_turnover (line 2375) | def reports_turnover():
function reports_low_stock (line 2439) | def reports_low_stock():
FILE: app/routes/invoice_approvals.py
function request_approval (line 23) | def request_approval(invoice_id):
function list_approvals (line 64) | def list_approvals():
function approve (line 75) | def approve(approval_id):
function reject (line 94) | def reject(approval_id):
function view_approval (line 118) | def view_approval(approval_id):
FILE: app/routes/invoices.py
function list_invoices (line 57) | def list_invoices():
function create_invoice (line 96) | def create_invoice():
function view_invoice (line 251) | def view_invoice(invoice_id):
function edit_invoice (line 385) | def edit_invoice(invoice_id):
function update_invoice_status (line 613) | def update_invoice_status(invoice_id):
function delete_invoice (line 739) | def delete_invoice(invoice_id):
function bulk_delete_invoices (line 764) | def bulk_delete_invoices():
function bulk_update_status (line 821) | def bulk_update_status():
function generate_from_time (line 888) | def generate_from_time(invoice_id):
function export_invoice_csv (line 1058) | def export_invoice_csv(invoice_id):
function export_invoice_ubl (line 1111) | def export_invoice_ubl(invoice_id):
function export_invoice_pdf (line 1140) | def export_invoice_pdf(invoice_id):
function duplicate_invoice (line 1315) | def duplicate_invoice(invoice_id):
function export_invoices_excel (line 1388) | def export_invoices_excel():
function send_invoice_email_route (line 1413) | def send_invoice_email_route(invoice_id):
function send_invoice_peppol_route (line 1472) | def send_invoice_peppol_route(invoice_id):
function get_invoice_email_history (line 1497) | def get_invoice_email_history(invoice_id):
function resend_invoice_email (line 1518) | def resend_invoice_email(invoice_id, email_id):
function upload_invoice_image (line 1580) | def upload_invoice_image(invoice_id):
function update_invoice_image_position (line 1713) | def update_invoice_image_position(invoice_id, image_id):
function delete_invoice_image (line 1748) | def delete_invoice_image(invoice_id, image_id):
function get_invoice_image_base64 (line 1800) | def get_invoice_image_base64(invoice_id, image_id):
FILE: app/routes/invoices_refactored.py
function list_invoices (line 32) | def list_invoices():
function create_invoice (line 105) | def create_invoice():
function mark_invoice_sent (line 210) | def mark_invoice_sent(invoice_id):
function mark_invoice_paid (line 229) | def mark_invoice_paid(invoice_id):
FILE: app/routes/issues.py
function list_issues (line 25) | def list_issues():
function new_issue (line 162) | def new_issue():
function view_issue (line 244) | def view_issue(issue_id):
function edit_issue (line 302) | def edit_issue(issue_id):
function link_task (line 392) | def link_task(issue_id):
function create_task_from_issue (line 413) | def create_task_from_issue(issue_id):
function update_status (line 439) | def update_status(issue_id):
function assign_issue (line 474) | def assign_issue(issue_id):
function update_priority (line 491) | def update_priority(issue_id):
function delete_issue (line 512) | def delete_issue(issue_id):
FILE: app/routes/kanban.py
function board (line 17) | def board():
function list_columns (line 97) | def list_columns():
function create_column (line 122) | def create_column():
function edit_column (line 227) | def edit_column(column_id):
function delete_column (line 293) | def delete_column(column_id):
function toggle_column (line 368) | def toggle_column(column_id):
function reorder_columns (line 416) | def reorder_columns():
function api_list_columns (line 457) | def api_list_columns():
FILE: app/routes/kiosk.py
function kiosk_dashboard (line 24) | def kiosk_dashboard():
function kiosk_login (line 84) | def kiosk_login():
function kiosk_logout (line 159) | def kiosk_logout():
function barcode_lookup (line 186) | def barcode_lookup():
function adjust_stock (line 256) | def adjust_stock():
function transfer_stock (line 338) | def transfer_stock():
function kiosk_start_timer (line 449) | def kiosk_start_timer():
function kiosk_stop_timer (line 513) | def kiosk_stop_timer():
function kiosk_timer_status (line 538) | def kiosk_timer_status():
function kiosk_warehouses (line 566) | def kiosk_warehouses():
function kiosk_projects (line 576) | def kiosk_projects():
function kiosk_settings_api (line 621) | def kiosk_settings_api():
FILE: app/routes/leads.py
function list_leads (line 28) | def list_leads():
function create_lead (line 78) | def create_lead():
function view_lead (line 123) | def view_lead(lead_id):
function edit_lead (line 134) | def edit_lead(lead_id):
function convert_to_client (line 177) | def convert_to_client(lead_id):
function convert_to_deal (line 232) | def convert_to_deal(lead_id):
function mark_lost (line 291) | def mark_lost(lead_id):
function create_activity (line 309) | def create_activity(lead_id):
FILE: app/routes/link_templates.py
function list_link_templates (line 20) | def list_link_templates():
function create_link_template (line 29) | def create_link_template():
function edit_link_template (line 83) | def edit_link_template(template_id):
function delete_link_template (line 136) | def delete_link_template(template_id):
FILE: app/routes/main.py
function dashboard (line 33) | def dashboard():
function health_check (line 290) | def health_check():
function readiness_check (line 296) | def readiness_check():
function about (line 306) | def about():
function help (line 312) | def help():
function donate (line 319) | def donate():
function track_donation_click (line 356) | def track_donation_click():
function track_banner_dismissal (line 385) | def track_banner_dismissal():
function track_support_impression (line 413) | def track_support_impression():
function request_soft_support_prompt (line 437) | def request_soft_support_prompt():
function track_support_event (line 470) | def track_support_event():
function debug_i18n (line 510) | def debug_i18n():
function set_language (line 546) | def set_language():
function search (line 604) | def search():
function manifest (line 629) | def manifest():
function offline_page (line 635) | def offline_page():
function service_worker (line 643) | def service_worker():
FILE: app/routes/mileage.py
function list_mileage (line 22) | def list_mileage():
function _mileage_export_query (line 147) | def _mileage_export_query():
function export_mileage_csv (line 205) | def export_mileage_csv():
function export_mileage_pdf (line 272) | def export_mileage_pdf():
function create_mileage (line 321) | def create_mileage():
function view_mileage (line 428) | def view_mileage(mileage_id):
function edit_mileage (line 447) | def edit_mileage(mileage_id):
function delete_mileage (line 520) | def delete_mileage(mileage_id):
function bulk_delete_mileage (line 549) | def bulk_delete_mileage():
function bulk_update_status (line 619) | def bulk_update_status():
function approve_mileage (line 687) | def approve_mileage(mileage_id):
function reject_mileage (line 720) | def reject_mileage(mileage_id):
function mark_reimbursed (line 757) | def mark_reimbursed(mileage_id):
function gps_tracking_page (line 789) | def gps_tracking_page():
function web_gps_start (line 799) | def web_gps_start():
function web_gps_add_point (line 816) | def web_gps_add_point(track_id):
function web_gps_stop (line 835) | def web_gps_stop(track_id):
function web_gps_create_expense (line 852) | def web_gps_create_expense(track_id):
function api_list_mileage (line 869) | def api_list_mileage():
function api_get_mileage (line 889) | def api_get_mileage(mileage_id):
function api_get_default_rates (line 903) | def api_get_default_rates():
FILE: app/routes/offers.py
function list_quotes (line 18) | def list_quotes():
function create_quote (line 42) | def create_quote():
function view_quote (line 180) | def view_quote(quote_id):
function edit_quote (line 189) | def edit_quote(quote_id):
function send_quote (line 280) | def send_quote(quote_id):
function accept_quote (line 304) | def accept_quote(quote_id):
function reject_quote (line 366) | def reject_quote(quote_id):
function delete_quote (line 394) | def delete_quote(quote_id):
FILE: app/routes/payment_gateways.py
function list_gateways (line 26) | def list_gateways():
function create_gateway (line 36) | def create_gateway():
function pay_invoice (line 72) | def pay_invoice(invoice_id):
function stripe_webhook (line 128) | def stripe_webhook():
function payment_success (line 222) | def payment_success(invoice_id):
FILE: app/routes/payments.py
function list_payments (line 21) | def list_payments():
function view_payment (line 126) | def view_payment(payment_id):
function create_payment (line 141) | def create_payment():
function edit_payment (line 319) | def edit_payment(payment_id):
function delete_payment (line 427) | def delete_payment(payment_id):
function bulk_delete_payments (line 466) | def bulk_delete_payments():
function bulk_update_status (line 532) | def bulk_update_status():
function payment_stats (line 606) | def payment_stats():
function export_payments_excel (line 674) | def export_payments_excel():
function get_user_invoices (line 733) | def get_user_invoices():
FILE: app/routes/per_diem.py
function list_per_diem (line 23) | def list_per_diem():
function _per_diem_export_query (line 114) | def _per_diem_export_query():
function export_per_diem_csv (line 161) | def export_per_diem_csv():
function export_per_diem_pdf (line 226) | def export_per_diem_pdf():
function create_per_diem (line 275) | def create_per_diem():
function view_per_diem (line 429) | def view_per_diem(per_diem_id):
function edit_per_diem (line 448) | def edit_per_diem(per_diem_id):
function delete_per_diem (line 518) | def delete_per_diem(per_diem_id):
function bulk_delete_per_diem (line 547) | def bulk_delete_per_diem():
function bulk_update_status (line 605) | def bulk_update_status():
function approve_per_diem (line 661) | def approve_per_diem(per_diem_id):
function reject_per_diem (line 694) | def reject_per_diem(per_diem_id):
function list_rates (line 733) | def list_rates():
function create_rate (line 752) | def create_rate():
function edit_rate (line 806) | def edit_rate(rate_id):
function delete_rate (line 865) | def delete_rate(rate_id):
function api_list_per_diem (line 905) | def api_list_per_diem():
function api_search_rates (line 925) | def api_search_rates():
function api_calculate_days (line 947) | def api_calculate_days():
FILE: app/routes/permissions.py
function _get_modules_by_category_for_roles (line 18) | def _get_modules_by_category_for_roles():
function _sanitize_hidden_module_ids (line 31) | def _sanitize_hidden_module_ids(module_ids):
function list_roles (line 49) | def list_roles():
function create_role (line 62) | def create_role():
function edit_role (line 133) | def edit_role(role_id):
function view_role (line 222) | def view_role(role_id):
function delete_role (line 238) | def delete_role(role_id):
function list_permissions (line 271) | def list_permissions():
function manage_user_roles (line 298) | def manage_user_roles(user_id):
function get_user_permissions (line 368) | def get_user_permissions(user_id):
function get_role_permissions (line 390) | def get_role_permissions(role_id):
FILE: app/routes/project_templates.py
function _parse_tasks_from_request_form (line 20) | def _parse_tasks_from_request_form():
function list_templates (line 64) | def list_templates():
function create_template (line 101) | def create_template():
function view_template (line 176) | def view_template(template_id):
function edit_template (line 196) | def edit_template(template_id):
function delete_template (line 283) | def delete_template(template_id):
function create_project_from_template (line 301) | def create_project_from_template(template_id):
FILE: app/routes/projects.py
function list_projects (line 55) | def list_projects():
function export_projects (line 181) | def export_projects():
function create_project (line 299) | def create_project():
function view_project (line 549) | def view_project(project_id):
function project_dashboard (line 658) | def project_dashboard(project_id):
function project_time_entries_overview (line 842) | def project_time_entries_overview(project_id):
function edit_project (line 905) | def edit_project(project_id):
function archive_project (line 1107) | def archive_project(project_id):
function unarchive_project (line 1149) | def unarchive_project(project_id):
function deactivate_project (line 1186) | def deactivate_project(project_id):
function activate_project (line 1209) | def activate_project(project_id):
function delete_project (line 1233) | def delete_project(project_id):
function bulk_delete_projects (line 1268) | def bulk_delete_projects():
function bulk_status_change (line 1338) | def bulk_status_change():
function favorite_project (line 1440) | def favorite_project(project_id):
function unfavorite_project (line 1485) | def unfavorite_project(project_id):
function list_costs (line 1533) | def list_costs(project_id):
function add_cost (line 1587) | def add_cost(project_id):
function edit_cost (line 1647) | def edit_cost(project_id, cost_id):
function delete_cost (line 1714) | def delete_cost(project_id, cost_id):
function api_project_costs (line 1747) | def api_project_costs(project_id):
function list_goods (line 1788) | def list_goods(project_id):
function add_good (line 1814) | def add_good(project_id):
function edit_good (line 1878) | def edit_good(project_id, good_id):
function delete_good (line 1949) | def delete_good(project_id, good_id):
function api_project_goods (line 1982) | def api_project_goods(project_id):
function upload_project_attachment (line 2004) | def upload_project_attachment(project_id):
function download_project_attachment (line 2122) | def download_project_attachment(attachment_id):
function delete_project_attachment (line 2146) | def delete_project_attachment(attachment_id):
FILE: app/routes/projects_refactored_example.py
function list_projects (line 26) | def list_projects():
function view_project (line 97) | def view_project(project_id):
function create_project (line 172) | def create_project():
FILE: app/routes/push_notifications.py
function subscribe_push (line 20) | def subscribe_push():
function unsubscribe_push (line 66) | def unsubscribe_push():
function list_subscriptions (line 97) | def list_subscriptions():
FILE: app/routes/quotes.py
function _parse_quote_form_date (line 18) | def _parse_quote_form_date(value):
function _pad_form_list (line 27) | def _pad_form_list(values, length):
function _quote_form_inventory_context (line 34) | def _quote_form_inventory_context():
function list_quotes (line 64) | def list_quotes():
function create_quote (line 114) | def create_quote():
function view_quote (line 479) | def view_quote(quote_id):
function edit_quote (line 509) | def edit_quote(quote_id):
function send_quote (line 860) | def send_quote(quote_id):
function accept_quote (line 935) | def accept_quote(quote_id):
function reject_quote (line 1049) | def reject_quote(quote_id):
function delete_quote (line 1077) | def delete_quote(quote_id):
function upload_attachment (line 1103) | def upload_attachment(quote_id):
function download_attachment (line 1218) | def download_attachment(attachment_id):
function delete_attachment (line 1247) | def delete_attachment(attachment_id):
function request_approval (line 1292) | def request_approval(quote_id):
function approve_quote (line 1335) | def approve_quote(quote_id):
function reject_approval (line 1375) | def reject_approval(quote_id):
function list_templates (line 1417) | def list_templates():
function create_template (line 1426) | def create_template():
function save_template_from_quote (line 1503) | def save_template_from_quote(template_id):
function export_quote_pdf (line 1567) | def export_quote_pdf(quote_id):
function send_quote_email (line 1656) | def send_quote_email(quote_id):
function duplicate_quote (line 1723) | def duplicate_quote(quote_id):
function bulk_action (line 1818) | def bulk_action():
function upload_quote_image (line 1959) | def upload_quote_image(quote_id):
function update_quote_image_position (line 2093) | def update_quote_image_position(quote_id, image_id):
function delete_quote_image (line 2129) | def delete_quote_image(quote_id, image_id):
function get_quote_image_base64 (line 2181) | def get_quote_image_base64(quote_id, image_id):
FILE: app/routes/recurring_invoices.py
function list_recurring_invoices (line 21) | def list_recurring_invoices():
function create_recurring_invoice (line 43) | def create_recurring_invoice():
function view_recurring_invoice (line 167) | def view_recurring_invoice(recurring_id):
function edit_recurring_invoice (line 185) | def edit_recurring_invoice(recurring_id):
function delete_recurring_invoice (line 245) | def delete_recurring_invoice(recurring_id):
function generate_invoice_now (line 267) | def generate_invoice_now(recurring_id):
FILE: app/routes/recurring_tasks.py
function list_recurring_tasks (line 22) | def list_recurring_tasks():
function create_recurring_task (line 37) | def create_recurring_task():
function view_recurring_task (line 76) | def view_recurring_task(task_id):
function toggle_recurring_task (line 90) | def toggle_recurring_task(task_id):
FILE: app/routes/reports.py
function reports (line 48) | def reports():
function week_in_review (line 75) | def week_in_review():
function comparison_view (line 90) | def comparison_view():
function project_report (line 103) | def project_report():
function user_report (line 162) | def user_report():
function export_form (line 285) | def export_form():
function export_csv (line 335) | def export_csv():
function export_summary_pdf (line 527) | def export_summary_pdf():
function summary_report (line 587) | def summary_report():
function task_report (line 669) | def task_report():
function _time_entries_report_query (line 764) | def _time_entries_report_query(request, require_dates=True, return_query...
function time_entries_report (line 853) | def time_entries_report():
function time_entries_export_excel (line 954) | def time_entries_export_excel():
function time_entries_export_csv (line 995) | def time_entries_export_csv():
function export_excel (line 1058) | def export_excel():
function export_project_excel (line 1138) | def export_project_excel():
function export_user_excel (line 1235) | def export_user_excel():
function export_user_entries_excel (line 1404) | def export_user_entries_excel():
function export_task_excel (line 1489) | def export_task_excel():
function unpaid_hours_report (line 1627) | def unpaid_hours_report():
function export_unpaid_hours_excel (line 1734) | def export_unpaid_hours_excel():
FILE: app/routes/salesman_reports.py
function list_email_mappings (line 24) | def list_email_mappings():
function get_email_mappings_api (line 36) | def get_email_mappings_api():
function create_email_mapping (line 47) | def create_email_mapping():
function update_email_mapping (line 88) | def update_email_mapping(mapping_id):
function delete_email_mapping (line 121) | def delete_email_mapping(mapping_id):
function get_unpaid_hours_by_salesman (line 141) | def get_unpaid_hours_by_salesman():
function preview_salesman_email (line 211) | def preview_salesman_email():
function generate_salesman_reports (line 248) | def generate_salesman_reports():
FILE: app/routes/saved_filters.py
function list_filters (line 28) | def list_filters():
function get_filters_api (line 45) | def get_filters_api():
function create_filter_api (line 62) | def create_filter_api():
function get_filter_api (line 126) | def get_filter_api(filter_id):
function update_filter_api (line 136) | def update_filter_api(filter_id):
function delete_filter_api (line 198) | def delete_filter_api(filter_id):
function delete_filter (line 240) | def delete_filter(filter_id):
FILE: app/routes/scheduled_reports.py
function api_list_scheduled (line 23) | def api_list_scheduled():
function list_scheduled (line 61) | def list_scheduled():
function create_scheduled (line 111) | def create_scheduled():
function delete_scheduled (line 162) | def delete_scheduled(schedule_id):
function api_create_scheduled (line 178) | def api_create_scheduled():
function api_toggle_scheduled (line 243) | def api_toggle_scheduled(schedule_id):
function api_delete_scheduled (line 261) | def api_delete_scheduled(schedule_id):
function api_saved_views (line 275) | def api_saved_views():
function fix_scheduled (line 295) | def fix_scheduled(schedule_id):
function api_trigger_scheduled (line 347) | def api_trigger_scheduled(schedule_id):
FILE: app/routes/settings.py
function index (line 19) | def index():
function keyboard_shortcuts (line 27) | def keyboard_shortcuts():
function profile (line 35) | def profile():
function preferences (line 43) | def preferences():
function _keyboard_shortcuts_config (line 53) | def _keyboard_shortcuts_config():
function api_keyboard_shortcuts_get (line 62) | def api_keyboard_shortcuts_get():
function api_keyboard_shortcuts_save (line 71) | def api_keyboard_shortcuts_save():
function api_keyboard_shortcuts_reset (line 94) | def api_keyboard_shortcuts_reset():
FILE: app/routes/setup.py
function initial_setup (line 24) | def initial_setup():
function _render_setup (line 136) | def _render_setup(settings, timezones):
FILE: app/routes/tasks.py
function _is_safe_next_url (line 34) | def _is_safe_next_url(next_url):
function list_tasks (line 48) | def list_tasks():
function create_task (line 175) | def create_task():
function view_task (line 316) | def view_task(task_id):
function edit_task (line 389) | def edit_task(task_id):
function update_task_status (line 619) | def update_task_status(task_id):
function update_task_priority (line 729) | def update_task_priority(task_id):
function assign_task (line 750) | def assign_task(task_id):
function delete_task (line 777) | def delete_task(task_id):
function bulk_delete_tasks (line 826) | def bulk_delete_tasks():
function bulk_update_status (line 908) | def bulk_update_status():
function bulk_update_due_date (line 977) | def bulk_update_due_date():
function bulk_update_priority (line 1056) | def bulk_update_priority():
function bulk_assign_tasks (line 1131) | def bulk_assign_tasks():
function bulk_move_project (line 1190) | def bulk_move_project():
function export_tasks (line 1267) | def export_tasks():
function my_tasks (line 1393) | def my_tasks():
function overdue_tasks (line 1481) | def overdue_tasks():
function api_task (line 1495) | def api_task(task_id):
function api_update_status (line 1508) | def api_update_status(task_id):
FILE: app/routes/team_chat.py
function chat_index (line 23) | def chat_index():
function chat_channel (line 50) | def chat_channel(channel_id):
function send_message (line 95) | def send_message(channel_id):
function api_channels (line 181) | def api_channels():
function api_messages (line 226) | def api_messages(channel_id):
function api_message (line 337) | def api_message(message_id):
function api_react (line 367) | def api_react(message_id):
function download_attachment (line 396) | def download_attachment(channel_id, message_id):
function upload_attachment (line 437) | def upload_attachment(channel_id):
function api_chat_users (line 533) | def api_chat_users():
function api_create_direct_message (line 549) | def api_create_direct_message(user_id):
FILE: app/routes/time_approvals.py
function list_approvals (line 21) | def list_approvals():
function view_approval (line 39) | def view_approval(approval_id):
function approve_entry (line 62) | def approve_entry(approval_id):
function reject_entry (line 83) | def reject_entry(approval_id):
function request_approval (line 111) | def request_approval(entry_id):
function cancel_approval (line 137) | def cancel_approval(approval_id):
function bulk_approve (line 157) | def bulk_approve():
function api_pending_approvals (line 174) | def api_pending_approvals():
FILE: app/routes/time_entry_templates.py
function list_templates (line 28) | def list_templates():
function create_template (line 45) | def create_template():
function view_template (line 137) | def view_template(template_id):
function edit_template (line 153) | def edit_template(template_id):
function delete_template (line 240) | def delete_template(template_id):
function get_templates_api (line 278) | def get_templates_api():
function get_template_api (line 295) | def get_template_api(template_id):
function use_template_api (line 311) | def use_template_api(template_id):
function get_project_tasks_api (line 334) | def get_project_tasks_api(project_id):
FILE: app/routes/timer.py
function _parse_optional_int (line 28) | def _parse_optional_int(value):
function _edit_timer_form_projects_tasks (line 38) | def _edit_timer_form_projects_tasks(timer, can_edit_schedule):
function _edit_timer_render_kwargs (line 55) | def _edit_timer_render_kwargs(timer, can_edit_schedule, show_source_drop...
function start_timer (line 68) | def start_timer():
function start_timer_from_template (line 332) | def start_timer_from_template(template_id):
function start_timer_for_project (line 426) | def start_timer_for_project(project_id):
function stop_timer (line 527) | def stop_timer():
function pause_timer (line 647) | def pause_timer():
function resume_timer (line 672) | def resume_timer():
function adjust_timer (line 697) | def adjust_timer():
function timer_status (line 740) | def timer_status():
function edit_timer (line 768) | def edit_timer(timer_id):
function view_timer (line 972) | def view_timer(timer_id):
function delete_timer (line 1028) | def delete_timer(timer_id):
function bulk_delete_time_entries (line 1139) | def bulk_delete_time_entries():
function manual_entry (line 1219) | def manual_entry():
function manual_entry_for_project (line 1567) | def manual_entry_for_project(project_id):
function bulk_entry (line 1605) | def bulk_entry():
function timer_page (line 1847) | def timer_page():
function calendar_view (line 1924) | def calendar_view():
function bulk_entry_for_project (line 1933) | def bulk_entry_for_project(project_id):
function duplicate_timer (line 1953) | def duplicate_timer(timer_id):
function resume_timer_by_id (line 2013) | def resume_timer_by_id(timer_id):
function time_entries_overview (line 2209) | def time_entries_overview():
function export_time_entries_csv (line 2572) | def export_time_entries_csv():
function export_time_entries_pdf (line 2785) | def export_time_entries_pdf():
function bulk_mark_paid (line 2974) | def bulk_mark_paid():
FILE: app/routes/timer_refactored.py
function start_timer (line 29) | def start_timer():
function stop_timer (line 118) | def stop_timer():
function api_timer_status (line 170) | def api_timer_status():
function api_start_timer (line 196) | def api_start_timer():
FILE: app/routes/user.py
function profile (line 25) | def profile():
function settings (line 47) | def settings():
function license (line 239) | def license():
function verify_donate_hide_code (line 277) | def verify_donate_hide_code():
function update_preferences (line 311) | def update_preferences():
function set_theme (line 369) | def set_theme():
function set_language (line 390) | def set_language():
function set_language_direct (line 419) | def set_language_direct(language):
FILE: app/routes/webhooks.py
function list_webhooks (line 20) | def list_webhooks():
function create_webhook (line 34) | def create_webhook():
function view_webhook (line 97) | def view_webhook(webhook_id):
function edit_webhook (line 120) | def edit_webhook(webhook_id):
function delete_webhook (line 163) | def delete_webhook(webhook_id):
function test_webhook (line 186) | def test_webhook(webhook_id):
FILE: app/routes/weekly_goals.py
function index (line 19) | def index():
function create (line 65) | def create():
function view (line 139) | def view(goal_id):
function edit (line 202) | def edit(goal_id):
function delete (line 269) | def delete(goal_id):
function api_current_goal (line 306) | def api_current_goal():
function api_list_goals (line 322) | def api_list_goals():
function api_get_goal (line 346) | def api_get_goal(goal_id):
function api_stats (line 363) | def api_stats():
FILE: app/routes/workflows.py
function list_workflows (line 21) | def list_workflows():
function create_workflow (line 35) | def create_workflow():
function view_workflow (line 90) | def view_workflow(workflow_id):
function edit_workflow (line 111) | def edit_workflow(workflow_id):
function delete_workflow (line 162) | def delete_workflow(workflow_id):
function toggle_workflow (line 182) | def toggle_workflow(workflow_id):
function api_list_workflows (line 198) | def api_list_workflows():
function api_create_workflow (line 207) | def api_create_workflow():
function api_get_workflow (line 232) | def api_get_workflow(workflow_id):
function api_update_workflow (line 245) | def api_update_workflow(workflow_id):
function api_delete_workflow (line 270) | def api_delete_workflow(workflow_id):
function test_workflow (line 286) | def test_workflow(workflow_id):
FILE: app/routes/workforce.py
function _parse_date (line 16) | def _parse_date(value):
function _can_approve (line 25) | def _can_approve() -> bool:
function dashboard (line 34) | def dashboard():
function create_period (line 110) | def create_period():
function submit_period (line 130) | def submit_period(period_id):
function approve_period (line 145) | def approve_period(period_id):
function reject_period (line 167) | def reject_period(period_id):
function close_period (line 189) | def close_period(period_id):
function delete_period (line 211) | def delete_period(period_id):
function update_policy (line 226) | def update_policy():
function create_leave_type (line 249) | def create_leave_type():
function delete_leave_type (line 276) | def delete_leave_type(leave_type_id):
function create_time_off_request (line 294) | def create_time_off_request():
function approve_time_off_request (line 329) | def approve_time_off_request(request_id):
function reject_time_off_request (line 353) | def reject_time_off_request(request_id):
function delete_time_off_request (line 377) | def delete_time_off_request(request_id):
function create_holiday (line 396) | def create_holiday():
function delete_holiday (line 417) | def delete_holiday(holiday_id):
function payroll_export_csv (line 435) | def payroll_export_csv():
function capacity_export_csv (line 499) | def capacity_export_csv():
function locked_periods_export_csv (line 563) | def locked_periods_export_csv():
function audit_events_export_csv (line 612) | def audit_events_export_csv():
FILE: app/schemas/client_schema.py
class ClientSchema (line 10) | class ClientSchema(Schema):
class ClientCreateSchema (line 29) | class ClientCreateSchema(Schema):
class ClientUpdateSchema (line 41) | class ClientUpdateSchema(Schema):
FILE: app/schemas/comment_schema.py
class CommentSchema (line 8) | class CommentSchema(Schema):
class CommentCreateSchema (line 29) | class CommentCreateSchema(Schema):
class CommentUpdateSchema (line 40) | class CommentUpdateSchema(Schema):
FILE: app/schemas/expense_schema.py
class ExpenseSchema (line 10) | class ExpenseSchema(Schema):
class ExpenseCreateSchema (line 30) | class ExpenseCreateSchema(Schema):
class ExpenseUpdateSchema (line 42) | class ExpenseUpdateSchema(Schema):
FILE: app/schemas/invoice_schema.py
class InvoiceItemSchema (line 12) | class InvoiceItemSchema(Schema):
class InvoiceSchema (line 23) | class InvoiceSchema(Schema):
class InvoiceCreateSchema (line 58) | class InvoiceCreateSchema(Schema):
class InvoiceUpdateSchema (line 70) | class InvoiceUpdateSchema(Schema):
FILE: app/schemas/payment_schema.py
class PaymentSchema (line 11) | class PaymentSchema(Schema):
class PaymentCreateSchema (line 35) | class PaymentCreateSchema(Schema):
class PaymentUpdateSchema (line 50) | class PaymentUpdateSchema(Schema):
FILE: app/schemas/project_schema.py
class ProjectSchema (line 12) | class ProjectSchema(Schema):
class ProjectCreateSchema (line 39) | class ProjectCreateSchema(Schema):
class ProjectUpdateSchema (line 54) | class ProjectUpdateSchema(Schema):
FILE: app/schemas/task_schema.py
class TaskSchema (line 10) | class TaskSchema(Schema):
class TaskCreateSchema (line 31) | class TaskCreateSchema(Schema):
class TaskUpdateSchema (line 43) | class TaskUpdateSchema(Schema):
FILE: app/schemas/time_entry_schema.py
class TimeEntrySchema (line 12) | class TimeEntrySchema(Schema):
class TimeEntryCreateSchema (line 40) | class TimeEntryCreateSchema(Schema):
method validate_end_time (line 56) | def validate_end_time(self, value, **kwargs):
method validate_project_or_client (line 64) | def validate_project_or_client(self, value, **kwargs):
method validate_client_or_project (line 74) | def validate_client_or_project(self, value, **kwargs):
method validate_task_with_project (line 84) | def validate_task_with_project(self, value, **kwargs):
class TimeEntryUpdateSchema (line 92) | class TimeEntryUpdateSchema(Schema):
class TimerStartSchema (line 112) | class TimerStartSchema(Schema):
class TimerStopSchema (line 121) | class TimerStopSchema(Schema):
FILE: app/schemas/user_schema.py
class UserSchema (line 10) | class UserSchema(Schema):
class UserCreateSchema (line 27) | class UserCreateSchema(Schema):
class UserUpdateSchema (line 38) | class UserUpdateSchema(Schema):
FILE: app/schemas/version_check.py
class VersionCheckResponse (line 6) | class VersionCheckResponse(TypedDict):
FILE: app/services/ai_categorization_service.py
class AICategorizationService (line 19) | class AICategorizationService:
method categorize_time_entry (line 62) | def categorize_time_entry(self, time_entry: TimeEntry) -> Dict[str, Any]:
method suggest_project_for_entry (line 96) | def suggest_project_for_entry(self, description: str, user_id: int) ->...
method suggest_task_for_entry (line 127) | def suggest_task_for_entry(self, description: str, project_id: int) ->...
method auto_categorize_batch (line 153) | def auto_categorize_batch(self, time_entries: List[TimeEntry]) -> Dict...
method _categorize_text (line 163) | def _categorize_text(self, text: str) -> List[tuple]:
method _calculate_match_score (line 189) | def _calculate_match_score(self, description: str, entity) -> float:
method learn_from_user_patterns (line 208) | def learn_from_user_patterns(self, user_id: int) -> Dict[str, Any]:
FILE: app/services/ai_suggestion_service.py
class AISuggestionService (line 18) | class AISuggestionService:
method get_time_entry_suggestions (line 21) | def get_time_entry_suggestions(self, user_id: int, context: str = None...
method _analyze_recent_patterns (line 47) | def _analyze_recent_patterns(self, user_id: int) -> List[Dict]:
method _suggest_from_active_tasks (line 94) | def _suggest_from_active_tasks(self, user_id: int) -> List[Dict]:
method _suggest_by_time_pattern (line 130) | def _suggest_by_time_pattern(self, user_id: int) -> List[Dict]:
method _suggest_by_deadlines (line 172) | def _suggest_by_deadlines(self, user_id: int) -> List[Dict]:
method _estimate_duration (line 209) | def _estimate_duration(self, entries: List[TimeEntry], project_id: int...
method _deduplicate_suggestions (line 224) | def _deduplicate_suggestions(self, suggestions: List[Dict]) -> List[Di...
method _rank_suggestions (line 237) | def _rank_suggestions(self, suggestions: List[Dict], user_id: int) -> ...
method get_project_suggestion (line 251) | def get_project_suggestion(self, description: str, user_id: int) -> Op...
FILE: app/services/analytics_service.py
class AnalyticsService (line 17) | class AnalyticsService:
method __init__ (line 20) | def __init__(self):
method get_dashboard_stats (line 26) | def get_dashboard_stats(self, user_id: Optional[int] = None) -> Dict[s...
method get_dashboard_top_projects (line 75) | def get_dashboard_top_projects(self, user_id: int, days: int = 30, lim...
method get_time_by_project_chart (line 124) | def get_time_by_project_chart(self, user_id: int, days: int = 7, limit...
method get_trends (line 153) | def get_trends(self, user_id: Optional[int] = None, days: int = 30) ->...
FILE: app/services/api_token_service.py
class ApiTokenService (line 15) | class ApiTokenService:
method create_token (line 44) | def create_token(
method rotate_token (line 114) | def rotate_token(self, token_id: int, user_id: int) -> Dict[str, Any]:
method revoke_token (line 178) | def revoke_token(self, token_id: int, user_id: int) -> Dict[str, Any]:
method get_expiring_tokens (line 217) | def get_expiring_tokens(self, days_ahead: int = 7) -> List[ApiToken]:
method validate_scopes (line 236) | def validate_scopes(self, scopes: str) -> Dict[str, Any]:
method check_token_rate_limit (line 285) | def check_token_rate_limit(self, token_id: int, max_requests_per_hour:...
FILE: app/services/backup_service.py
class BackupService (line 17) | class BackupService:
method __init__ (line 20) | def __init__(self):
method create_database_backup (line 24) | def create_database_backup(self, backup_name: Optional[str] = None) ->...
method list_backups (line 97) | def list_backups(self) -> List[Dict[str, Any]]:
method delete_backup (line 128) | def delete_backup(self, backup_name: str) -> Dict[str, Any]:
FILE: app/services/base_crud_service.py
class BaseCRUDService (line 20) | class BaseCRUDService(Generic[ModelType, RepositoryType]):
method __init__ (line 29) | def __init__(self, repository: RepositoryType, model_name: str = "Reco...
method get_by_id (line 40) | def get_by_id(self, record_id: int) -> Dict[str, Any]:
method create (line 57) | def create(self, **kwargs) -> Dict[str, Any]:
method update (line 85) | def update(self, record_id: int, **kwargs) -> Dict[str, Any]:
method delete (line 119) | def delete(self, record_id: int) -> Dict[str, Any]:
method list_all (line 157) | def list_all(self, page: int = 1, per_page: int = 20, **filters) -> Di...
FILE: app/services/calendar_integration_service.py
class CalendarIntegrationService (line 18) | class CalendarIntegrationService:
method create_integration (line 23) | def create_integration(
method get_integration (line 89) | def get_integration(self, integration_id: int) -> Optional[CalendarInt...
method get_user_integrations (line 93) | def get_user_integrations(self, user_id: int, provider: Optional[str] ...
method sync_time_entry_to_calendar (line 100) | def sync_time_entry_to_calendar(
method update_sync_status (line 138) | def update_sync_status(
method deactivate_integration (line 190) | def deactivate_integration(self, integration_id: int, user_id: int) ->...
FILE: app/services/client_activity_feed_service.py
function get_client_activity_feed (line 14) | def get_client_activity_feed(
function _activity_to_feed_item (line 115) | def _activity_to_feed_item(
FILE: app/services/client_approval_service.py
class ClientApprovalService (line 17) | class ClientApprovalService:
method request_approval (line 20) | def request_approval(self, time_entry_id: int, requested_by: int, comm...
method approve (line 79) | def approve(self, approval_id: int, contact_id: int, comment: str = No...
method reject (line 94) | def reject(self, approval_id: int, contact_id: int, reason: str) -> Di...
method get_pending_approvals_for_client (line 109) | def get_pending_approvals_for_client(self, client_id: int) -> List[Cli...
method _emit_approval_update (line 127) | def _emit_approval_update(self, approval: ClientTimeApproval, event: s...
method _notify_client_contacts (line 141) | def _notify_client_contacts(self, client: Client, approval: ClientTime...
method _notify_requester (line 167) | def _notify_requester(self, approval: ClientTimeApproval, status: str,...
FILE: app/services/client_notification_service.py
class ClientNotificationService (line 18) | class ClientNotificationService:
method create_notification (line 21) | def create_notification(
method notify_invoice_created (line 72) | def notify_invoice_created(self, invoice_id: int, client_id: int):
method notify_invoice_paid (line 91) | def notify_invoice_paid(self, invoice_id: int, client_id: int, amount:...
method notify_invoice_overdue (line 110) | def notify_invoice_overdue(self, invoice_id: int, client_id: int, days...
method notify_time_entry_approval (line 129) | def notify_time_entry_approval(self, approval_id: int, client_id: int):
method notify_quote_available (line 148) | def notify_quote_available(self, quote_id: int, client_id: int):
method notify_project_milestone (line 167) | def notify_project_milestone(self, project_id: int, client_id: int, mi...
method notify_budget_alert (line 186) | def notify_budget_alert(self, project_id: int, client_id: int, budget_...
method _send_email_notification (line 205) | def _send_email_notification(self, notification: ClientNotification):
method mark_as_read (line 240) | def mark_as_read(self, notification_id: int, client_id: int) -> bool:
method mark_all_as_read (line 249) | def mark_all_as_read(self, client_id: int) -> int:
method get_unread_count (line 257) | def get_unread_count(self, client_id: int) -> int:
method get_notifications (line 261) | def get_notifications(self, client_id: int, limit: int = 50, unread_on...
FILE: app/services/client_report_service.py
function build_report_data (line 16) | def build_report_data(
function _task_summary_for_projects (line 98) | def _task_summary_for_projects(project_ids: List[int]) -> Dict[str, Any]:
FILE: app/services/client_service.py
class ClientService (line 14) | class ClientService:
method __init__ (line 17) | def __init__(self):
method get_by_id (line 20) | def get_by_id(self, client_id: int) -> Optional[Client]:
method get_by_name (line 29) | def get_by_name(self, name: str) -> Optional[Client]:
method create_client (line 38) | def create_client(
method update_client (line 80) | def update_client(self, client_id: int, user_id: int, **kwargs) -> Dic...
method get_active_clients (line 104) | def get_active_clients(self) -> List[Client]:
FILE: app/services/comment_service.py
class CommentService (line 14) | class CommentService:
method __init__ (line 17) | def __init__(self):
method create_comment (line 22) | def create_comment(
method get_project_comments (line 113) | def get_project_comments(self, project_id: int, include_replies: bool ...
method get_task_comments (line 119) | def get_task_comments(self, task_id: int, include_replies: bool = True...
method delete_comment (line 123) | def delete_comment(self, comment_id: int, user_id: int) -> Dict[str, A...
FILE: app/services/currency_service.py
class CurrencyService (line 18) | class CurrencyService:
method convert (line 25) | def convert(amount: Decimal, from_currency: str, to_currency: str, con...
method get_exchange_rate (line 42) | def get_exchange_rate(base_currency: str, quote_currency: str, rate_da...
method fetch_exchange_rate (line 70) | def fetch_exchange_rate(base_currency: str, quote_currency: str, rate_...
method store_exchange_rate (line 104) | def store_exchange_rate(base_currency: str, quote_currency: str, rate_...
method update_exchange_rates (line 121) | def update_exchange_rates(base_currency: str = "EUR", currencies: list...
method get_historical_rates (line 145) | def get_historical_rates(base_currency: str, quote_currency: str, star...
method auto_convert_invoice (line 161) | def auto_convert_invoice(invoice) -> Dict[str, Decimal]:
FILE: app/services/custom_report_service.py
class CustomReportService (line 21) | class CustomReportService:
method build_report (line 24) | def build_report(self, config_id: int, filters: Dict = None) -> Dict[s...
method _build_time_report (line 45) | def _build_time_report(self, config: CustomReportConfig, filters: Dict...
method _build_project_report (line 80) | def _build_project_report(self, config: CustomReportConfig, filters: D...
method _build_invoice_report (line 91) | def _build_invoice_report(self, config: CustomReportConfig, filters: D...
method _build_expense_report (line 107) | def _build_expense_report(self, config: CustomReportConfig, filters: D...
method _build_combined_report (line 123) | def _build_combined_report(self, config: CustomReportConfig, filters: ...
method _apply_groupings (line 131) | def _apply_groupings(self, entries: List, groupings: List[str]) -> Dict:
method _format_columns (line 154) | def _format_columns(self, data: Dict, columns: List[str]) -> List[Dict]:
method _calculate_summary (line 180) | def _calculate_summary(self, entries: List[TimeEntry]) -> Dict:
FILE: app/services/email_service.py
class EmailService (line 14) | class EmailService:
method __init__ (line 17) | def __init__(self):
method send_invoice_email (line 20) | def send_invoice_email(
method send_notification_email (line 76) | def send_notification_email(
FILE: app/services/enhanced_ocr_service.py
class EnhancedOCRService (line 16) | class EnhancedOCRService:
method scan_receipt_enhanced (line 19) | def scan_receipt_enhanced(self, image_path: str, lang: str = "eng") ->...
method _extract_merchant (line 50) | def _extract_merchant(self, text: str) -> Optional[str]:
method _extract_date (line 66) | def _extract_date(self, text: str) -> Optional[str]:
method _extract_total (line 88) | def _extract_total(self, text: str) -> Optional[Decimal]:
method _extract_tax (line 115) | def _extract_tax(self, text: str) -> Optional[Decimal]:
method _extract_items (line 134) | def _extract_items(self, text: str) -> List[Dict[str, Any]]:
method _extract_currency (line 164) | def _extract_currency(self, text: str) -> Optional[str]:
method _extract_receipt_number (line 186) | def _extract_receipt_number(self, text: str) -> Optional[str]:
method _calculate_confidence (line 202) | def _calculate_confidence(self, text: str) -> float:
FILE: app/services/expense_service.py
class ExpenseService (line 15) | class ExpenseService:
method __init__ (line 18) | def __init__(self):
method create_expense (line 22) | def create_expense(
method get_project_expenses (line 93) | def get_project_expenses(
method get_total_expenses (line 101) | def get_total_expenses(
method list_expenses (line 113) | def list_expenses(
method update_expense (line 164) | def update_expense(self, expense_id: int, user_id: int, is_admin: bool...
method delete_expense (line 202) | def delete_expense(self, expense_id: int, user_id: int, is_admin: bool...
FILE: app/services/export_service.py
class ExportService (line 14) | class ExportService:
method __init__ (line 17) | def __init__(self):
method export_time_entries_csv (line 23) | def export_time_entries_csv(
method export_projects_csv (line 91) | def export_projects_csv(self, status: Optional[str] = None, client_id:...
method export_invoices_csv (line 141) | def export_invoices_csv(self, status: Optional[str] = None, client_id:...
FILE: app/services/gamification_service.py
class GamificationService (line 18) | class GamificationService:
method check_and_award_badges (line 21) | def check_and_award_badges(self, user_id: int, event_type: str, event_...
method _check_badge_criteria (line 47) | def _check_badge_criteria(self, user_id: int, badge: Badge, event_type...
method _get_total_hours (line 84) | def _get_total_hours(self, user_id: int, criteria: Dict) -> float:
method _get_completed_tasks (line 96) | def _get_completed_tasks(self, user_id: int, criteria: Dict) -> int:
method _get_streak (line 105) | def _get_streak(self, user_id: int, criteria: Dict) -> int:
method _get_completed_projects (line 123) | def _get_completed_projects(self, user_id: int, criteria: Dict) -> int:
method get_user_badges (line 133) | def get_user_badges(self, user_id: int) -> List[Dict]:
method get_user_points (line 139) | def get_user_points(self, user_id: int) -> int:
method calculate_leaderboard (line 145) | def calculate_leaderboard(
method _get_period_dates (line 183) | def _get_period_dates(self, period: str) -> tuple:
method _calculate_scores (line 206) | def _calculate_scores(self, leaderboard: Leaderboard, start: datetime,...
method get_leaderboard (line 248) | def get_leaderboard(self, leaderboard_id: int, limit: int = 100) -> Li...
FILE: app/services/gantt_service.py
function calculate_project_progress (line 14) | def calculate_project_progress(project: Project, tasks: Optional[List[Ta...
function calculate_task_progress (line 24) | def calculate_task_progress(task: Task) -> int:
class GanttService (line 35) | class GanttService:
method get_gantt_data (line 38) | def get_gantt_data(
FILE: app/services/global_search_service.py
function _parse_search_types (line 15) | def _parse_search_types(types_filter: str) -> Set[str]:
function run_global_search (line 24) | def run_global_search(
FILE: app/services/gps_tracking_service.py
class GPSTrackingService (line 16) | class GPSTrackingService:
method start_tracking (line 19) | def start_tracking(
method add_track_point (line 32) | def add_track_point(
method stop_tracking (line 50) | def stop_tracking(
method create_expense_from_track (line 84) | def create_expense_from_track(
method calculate_route_distance (line 120) | def calculate_route_distance(
method get_user_tracks (line 151) | def get_user_tracks(
FILE: app/services/health_service.py
class HealthService (line 14) | class HealthService:
method get_health_status (line 17) | def get_health_status(self) -> Dict[str, Any]:
method get_readiness_status (line 55) | def get_readiness_status(self) -> Dict[str, Any]:
FILE: app/services/import_service.py
class ImportService (line 15) | class ImportService:
method __init__ (line 18) | def __init__(self):
method import_time_entries_csv (line 28) | def import_time_entries_csv(self, file, user_id: int, default_project_...
method import_projects_csv (line 109) | def import_projects_csv(self, file, created_by: int) -> Dict[str, Any]:
FILE: app/services/integration_service.py
class IntegrationService (line 18) | class IntegrationService:
method register_connector (line 33) | def register_connector(cls, provider: str, connector_class):
method get_connector (line 38) | def get_connector(cls, integration: Integration) -> Optional[Any]:
method create_integration (line 56) | def create_integration(
method get_integration (line 122) | def get_integration(
method list_integrations (line 154) | def list_integrations(self, user_id: Optional[int] = None) -> Li
Copy disabled (too large)
Download .json
Condensed preview — 1899 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (43,146K chars).
[
{
"path": ".bandit",
"chars": 79,
"preview": "[bandit]\nexclude_dirs = tests,migrations,venv,.venv,htmlcov\nskips = B101,B601\n\n"
},
{
"path": ".coveragerc",
"chars": 378,
"preview": "[run]\nsource = app\nomit =\n */tests/*\n */test_*.py\n */__pycache__/*\n */venv/*\n */env/*\n # Exclude infra"
},
{
"path": ".editorconfig",
"chars": 205,
"preview": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\ncharset = utf-8\n\n[*.py]\nindent_style = space\nindent_size ="
},
{
"path": ".flake8",
"chars": 485,
"preview": "[flake8]\nmax-line-length = 120\nmax-complexity = 10\n# C901=complexity, E501=line length, F401=unused import, E402=import "
},
{
"path": ".gitattributes",
"chars": 169,
"preview": "# Enforce LF endings for executable scripts to avoid /usr/bin/env CRLF issues\n*.sh text eol=lf\n*.py text eol=lf\n\n# Optio"
},
{
"path": ".github/FUNDING.yml",
"chars": 880,
"preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 1122,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the b"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 595,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your fea"
},
{
"path": ".github/ISSUE_TEMPLATE/translation_fix.yml",
"chars": 1890,
"preview": "name: Translation improvement\ndescription: Suggest a correction or better wording for interface text in a specific langu"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 856,
"preview": "## Description\n\nBrief description of the change and why it's needed.\n\n## Type of change\n\n- [ ] Bug fix (non-breaking cha"
},
{
"path": ".github/workflows/build-desktop.yml",
"chars": 2719,
"preview": "name: Build Desktop Apps\n\nenv:\n NODE_VERSION: '24'\n\non:\n push:\n branches: [ main, develop ]\n paths:\n - 'des"
},
{
"path": ".github/workflows/build-mobile.yml",
"chars": 4218,
"preview": "name: Build Mobile Apps\n\non:\n push:\n branches: [ main, develop ]\n paths:\n - 'mobile/**'\n - '.github/wor"
},
{
"path": ".github/workflows/cd-development.yml",
"chars": 13178,
"preview": "name: CD - Development Build\n\non:\n pull_request:\n branches: [ 'rc', 'rc/**' ]\n # Only trigger builds when actual "
},
{
"path": ".github/workflows/cd-release.yml",
"chars": 54054,
"preview": "name: CD - Release Build\n\n# This workflow builds and publishes official releases\n# \n# Testing Strategy:\n# - Full test su"
},
{
"path": ".github/workflows/ci-comprehensive.yml",
"chars": 19696,
"preview": "name: Comprehensive CI Pipeline\n\n# This workflow runs comprehensive tests on pull requests\n# \n# Test Strategy:\n# - Smoke"
},
{
"path": ".github/workflows/ci.yml",
"chars": 5401,
"preview": "name: CI/CD Pipeline\n\n# DISABLED: This workflow is disabled in favor of the Comprehensive CI Pipeline (ci-comprehensive."
},
{
"path": ".github/workflows/crowdin-sync.yml",
"chars": 1260,
"preview": "# Manual Crowdin sync: uploads English source .po, downloads translations, opens a PR.\n# Prerequisites: repo secrets CRO"
},
{
"path": ".github/workflows/migration-check.yml",
"chars": 15324,
"preview": "name: Database Migration Validation\n\non:\n pull_request:\n paths:\n - 'app/models/**'\n - 'migrations/**'\n "
},
{
"path": ".github/workflows/static.yml",
"chars": 1111,
"preview": "name: Deploy to GitHub Pages\n\non:\n release:\n types: [published]\n\n# Sets permissions of the GITHUB_TOKEN to allow dep"
},
{
"path": ".github/workflows-archive/ci.yml.backup",
"chars": 5934,
"preview": "name: Continuous Integration\n\non:\n push:\n branches: [ main, develop ]\n pull_request:\n branches: [ main, develop "
},
{
"path": ".github/workflows-archive/docker-publish.yml.backup",
"chars": 8336,
"preview": "name: Build and Publish TimeTracker Docker Image\n\non:\n push:\n branches: [ main ]\n tags: [ 'v*' ]\n pull_request:\n"
},
{
"path": ".gitignore",
"chars": 4477,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": ".pre-commit-config.yaml",
"chars": 2433,
"preview": "# Pre-commit hooks configuration for TimeTracker\n# Install with: pre-commit install\n# Run manually: pre-commit run --all"
},
{
"path": "CHANGELOG.md",
"chars": 30971,
"preview": "# Changelog\n\nAll notable changes to TimeTracker will be documented in this file.\n\nThe format is based on [Keep a Changel"
},
{
"path": "CONTRIBUTING.md",
"chars": 1976,
"preview": "# Contributing to TimeTracker\n\nThank you for your interest in contributing to TimeTracker. This page gives you a quick o"
},
{
"path": "Dockerfile",
"chars": 5562,
"preview": "# syntax=docker/dockerfile:1.4\n\n# --- Stage 1: Frontend Build ---\nFROM node:18-slim as frontend\nWORKDIR /app\nCOPY packag"
},
{
"path": "INSTALLATION.md",
"chars": 3545,
"preview": "# TimeTracker Installation\n\nThis guide walks you through installing and running TimeTracker. For a quick overview, see t"
},
{
"path": "LICENSE",
"chars": 35219,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "Makefile",
"chars": 5807,
"preview": "# TimeTracker Makefile\n# Common development and testing tasks\n\n.PHONY: help install test test-smoke test-unit test-integ"
},
{
"path": "README.md",
"chars": 50657,
"preview": "# TimeTracker\n\n<div align=\"center\">\n\n### Professional Time Tracking & Project Management for Teams\n\n**Track time. Manage"
},
{
"path": "app/__init__.py",
"chars": 61143,
"preview": "import logging\nimport os\nimport re\nimport tempfile\nimport time\nimport uuid\nfrom datetime import timedelta\nfrom urllib.pa"
},
{
"path": "app/blueprint_registry.py",
"chars": 10394,
"preview": "\"\"\"\nCentralized blueprint registration for the Flask app.\nExtracted from app/__init__.py to reduce bootstrap module size"
},
{
"path": "app/config/__init__.py",
"chars": 1850,
"preview": "\"\"\"\nConfiguration module for TimeTracker.\n\nThis module contains:\n- Flask application configuration (Config, ProductionCo"
},
{
"path": "app/config/analytics_defaults.py",
"chars": 6308,
"preview": "\"\"\"\nAnalytics configuration for TimeTracker.\n\nThese values are embedded at build time and cannot be overridden by users."
},
{
"path": "app/config/support_ui.py",
"chars": 1800,
"preview": "\"\"\"Non-translated support/checkout configuration (URLs, numeric defaults).\"\"\"\n\nfrom __future__ import annotations\n\nimpor"
},
{
"path": "app/config.py",
"chars": 19906,
"preview": "import os\nfrom datetime import timedelta\n\n\nclass Config:\n \"\"\"Base configuration class\"\"\"\n\n # Flask settings\n # "
},
{
"path": "app/constants.py",
"chars": 4695,
"preview": "\"\"\"\nApplication-wide constants and enums.\nThis module centralizes magic strings and numbers used throughout the applicat"
},
{
"path": "app/integrations/__init__.py",
"chars": 102,
"preview": "\"\"\"\nIntegration connectors package.\n\"\"\"\n\nfrom .base import BaseConnector\n\n__all__ = [\"BaseConnector\"]\n"
},
{
"path": "app/integrations/activitywatch.py",
"chars": 16655,
"preview": "\"\"\"\nActivityWatch integration connector.\n\nImports window and web activity events from a local ActivityWatch aw-server\n(h"
},
{
"path": "app/integrations/asana.py",
"chars": 17066,
"preview": "\"\"\"\nAsana integration connector.\nSync tasks and projects with Asana.\n\"\"\"\n\nimport os\nfrom datetime import datetime, timed"
},
{
"path": "app/integrations/base.py",
"chars": 10791,
"preview": "\"\"\"\nBase connector interface for integrations.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom datetime import datetime\nfr"
},
{
"path": "app/integrations/caldav_calendar.py",
"chars": 76250,
"preview": "\"\"\"\nCalDAV Calendar integration connector.\n\nSupports CalDAV servers such as Zimbra by using WebDAV PROPFIND/REPORT reque"
},
{
"path": "app/integrations/github.py",
"chars": 27211,
"preview": "\"\"\"\nGitHub integration connector.\n\"\"\"\n\nimport logging\nimport os\nfrom datetime import datetime, timedelta\nfrom typing imp"
},
{
"path": "app/integrations/gitlab.py",
"chars": 18934,
"preview": "\"\"\"\nGitLab integration connector.\nSync issues and track time from GitLab.\n\"\"\"\n\nimport os\nfrom datetime import datetime, "
},
{
"path": "app/integrations/google_calendar.py",
"chars": 44561,
"preview": "\"\"\"\nGoogle Calendar integration connector.\nProvides two-way sync between TimeTracker and Google Calendar.\n\"\"\"\n\nimport os"
},
{
"path": "app/integrations/jira.py",
"chars": 26200,
"preview": "\"\"\"\nJira integration connector.\n\"\"\"\n\nimport hashlib\nimport hmac\nimport json\nimport logging\nimport os\nimport re\nfrom date"
},
{
"path": "app/integrations/linear.py",
"chars": 9498,
"preview": "\"\"\"\nLinear integration: import issues as tasks using a Personal API Key.\n\nhttps://developers.linear.app/docs/graphql/wor"
},
{
"path": "app/integrations/microsoft_teams.py",
"chars": 15183,
"preview": "\"\"\"\nMicrosoft Teams integration connector.\nSend notifications and sync with Microsoft Teams.\n\"\"\"\n\nimport os\nfrom datetim"
},
{
"path": "app/integrations/outlook_calendar.py",
"chars": 18577,
"preview": "\"\"\"\nOutlook Calendar integration connector.\nProvides two-way sync between TimeTracker and Outlook Calendar.\n\"\"\"\n\nimport "
},
{
"path": "app/integrations/peppol.py",
"chars": 13978,
"preview": "\"\"\"\nPeppol e-invoicing integration (BIS Billing 3.0 / UBL Invoice 2.1).\n\nThis module provides:\n- UBL XML generation for "
},
{
"path": "app/integrations/peppol_as4.py",
"chars": 6537,
"preview": "\"\"\"\nPEPPOL AS4 message packaging and transmission.\n\nEXPERIMENTAL: This native AS4 implementation provides basic message\n"
},
{
"path": "app/integrations/peppol_identifiers.py",
"chars": 5570,
"preview": "\"\"\"\nPEPPOL participant identifier validation (scheme + endpoint ID).\n\nValidates and normalizes sender/recipient identifi"
},
{
"path": "app/integrations/peppol_smp.py",
"chars": 5995,
"preview": "\"\"\"\nPEPPOL SML/SMP participant discovery.\n\nEXPERIMENTAL: Resolves recipient access point URL from the Service Metadata\nL"
},
{
"path": "app/integrations/peppol_transport.py",
"chars": 5649,
"preview": "\"\"\"\nPEPPOL transport provider interface and implementations.\n\n- GenericTransport: HTTP JSON adapter (access point URL). "
},
{
"path": "app/integrations/quickbooks.py",
"chars": 37015,
"preview": "\"\"\"\nQuickBooks integration connector.\nSync invoices, expenses, and payments with QuickBooks Online.\n\"\"\"\n\nimport base64\ni"
},
{
"path": "app/integrations/registry.py",
"chars": 2147,
"preview": "\"\"\"\nIntegration connector registry.\nRegisters all available connectors with the IntegrationService.\n\"\"\"\n\nfrom app.integr"
},
{
"path": "app/integrations/slack.py",
"chars": 14748,
"preview": "\"\"\"\nSlack integration connector.\n\"\"\"\n\nimport os\nfrom datetime import datetime, timedelta\nfrom typing import Any, Dict, O"
},
{
"path": "app/integrations/trello.py",
"chars": 24691,
"preview": "\"\"\"\nTrello integration connector.\nSync boards, lists, and cards with Trello.\n\"\"\"\n\nimport base64\nimport hashlib\nimport hm"
},
{
"path": "app/integrations/xero.py",
"chars": 21573,
"preview": "\"\"\"\nXero integration connector.\nSync invoices, expenses, and payments with Xero.\n\"\"\"\n\nimport base64\nimport logging\nimpor"
},
{
"path": "app/models/__init__.py",
"chars": 7391,
"preview": "from .activity import Activity\nfrom .api_idempotency_key import ApiIdempotencyKey\nfrom .api_token import ApiToken\nfrom ."
},
{
"path": "app/models/activity.py",
"chars": 6814,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass Activity(db.Model):\n \"\"\"Activity log for tracking user acti"
},
{
"path": "app/models/api_idempotency_key.py",
"chars": 916,
"preview": "\"\"\"Idempotency keys for API write deduplication (e.g. mobile retries).\"\"\"\n\nfrom datetime import datetime\n\nfrom app impor"
},
{
"path": "app/models/api_token.py",
"chars": 5184,
"preview": "\"\"\"API Token model for REST API authentication\"\"\"\n\nimport secrets\nfrom datetime import datetime, timedelta\n\nfrom sqlalch"
},
{
"path": "app/models/audit_log.py",
"chars": 11878,
"preview": "import json\nfrom datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\nclass"
},
{
"path": "app/models/budget_alert.py",
"chars": 6037,
"preview": "from datetime import datetime, timedelta\n\nfrom app import db\n\n\nclass BudgetAlert(db.Model):\n \"\"\"Budget alert model fo"
},
{
"path": "app/models/calendar_event.py",
"chars": 11810,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef _isoformat_ca"
},
{
"path": "app/models/calendar_integration.py",
"chars": 3266,
"preview": "\"\"\"Calendar integration models\"\"\"\n\nfrom datetime import datetime\n\nfrom app import db\n\n\nclass CalendarIntegration(db.Mode"
},
{
"path": "app/models/client.py",
"chars": 15043,
"preview": "import json\nimport secrets\nfrom datetime import datetime, timedelta\nfrom decimal import Decimal\n\nfrom sqlalchemy.orm.att"
},
{
"path": "app/models/client_attachment.py",
"chars": 4861,
"preview": "import os\nfrom datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef loc"
},
{
"path": "app/models/client_note.py",
"chars": 5445,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\nclass ClientNote("
},
{
"path": "app/models/client_notification.py",
"chars": 6731,
"preview": "\"\"\"\nClient Notification models for client portal notifications\n\"\"\"\n\nimport enum\nfrom datetime import datetime\n\nfrom app "
},
{
"path": "app/models/client_portal_customization.py",
"chars": 4172,
"preview": "\"\"\"\nClient Portal Customization model\nAllows branding and customization of the client portal\n\"\"\"\n\nfrom datetime import d"
},
{
"path": "app/models/client_portal_dashboard_preference.py",
"chars": 2179,
"preview": "\"\"\"\nClient Portal Dashboard Preference model.\nStores per-client (and optionally per-user) widget visibility and order fo"
},
{
"path": "app/models/client_prepaid_consumption.py",
"chars": 1789,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\n\n\nclass ClientPrepaidConsumption(db.Model)"
},
{
"path": "app/models/client_time_approval.py",
"chars": 6518,
"preview": "\"\"\"\nClient Time Entry Approval models\nSimilar to manager approval but for client-side approval\n\"\"\"\n\nimport enum\nfrom dat"
},
{
"path": "app/models/comment.py",
"chars": 9580,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\nclass Comment(db."
},
{
"path": "app/models/comment_attachment.py",
"chars": 4291,
"preview": "import os\nfrom datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef loc"
},
{
"path": "app/models/contact.py",
"chars": 5657,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef local_now():\n"
},
{
"path": "app/models/contact_communication.py",
"chars": 4545,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef local_now():\n"
},
{
"path": "app/models/currency.py",
"chars": 1741,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass Currency(db.Model):\n \"\"\"Supported currencies and display me"
},
{
"path": "app/models/custom_field_definition.py",
"chars": 8180,
"preview": "\"\"\"Custom Field Definition model for global custom field management\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy.e"
},
{
"path": "app/models/custom_report.py",
"chars": 2047,
"preview": "\"\"\"\nCustom Report Builder models\n\"\"\"\n\nfrom datetime import datetime\n\nfrom app import db\n\n\nclass CustomReportConfig(db.Mo"
},
{
"path": "app/models/deal.py",
"chars": 7556,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_"
},
{
"path": "app/models/deal_activity.py",
"chars": 2786,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef local_now():\n"
},
{
"path": "app/models/donation_interaction.py",
"chars": 4481,
"preview": "\"\"\"Model to track donation banner interactions and user engagement metrics.\n\nCanonical interaction_type values (funnel):"
},
{
"path": "app/models/expense.py",
"chars": 14295,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom sqlalchemy import Index\n\nfrom app import db\n\n\nclass Expe"
},
{
"path": "app/models/expense_category.py",
"chars": 6152,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom sqlalchemy import Index\n\nfrom app import db\n\n\nclass Expe"
},
{
"path": "app/models/expense_gps.py",
"chars": 5330,
"preview": "\"\"\"\nGPS tracking models for mileage expenses\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import Optional\n\nfrom sqlalc"
},
{
"path": "app/models/extra_good.py",
"chars": 6600,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\n\n\nclass ExtraGood(db.Model):\n \"\"\"Extra "
},
{
"path": "app/models/focus_session.py",
"chars": 2582,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass FocusSession(db.Model):\n \"\"\"Pomodoro-style focus session me"
},
{
"path": "app/models/gamification.py",
"chars": 6677,
"preview": "\"\"\"\nGamification models for badges and leaderboards\n\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy import Index\n\nfr"
},
{
"path": "app/models/import_export.py",
"chars": 8840,
"preview": "\"\"\"\nImport/Export tracking models for data import/export operations\n\"\"\"\n\nfrom datetime import datetime\n\nfrom app import "
},
{
"path": "app/models/integration.py",
"chars": 4552,
"preview": "\"\"\"\nIntegration models for third-party service connections.\n\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy import J"
},
{
"path": "app/models/integration_external_event_link.py",
"chars": 1427,
"preview": "\"\"\"\nExternal event link table for integration-driven sync.\n\nUsed for idempotency when importing calendar events (e.g., C"
},
{
"path": "app/models/invoice.py",
"chars": 16422,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\nfrom app.utils.invoice_numbering import ge"
},
{
"path": "app/models/invoice_approval.py",
"chars": 3432,
"preview": "\"\"\"Invoice approval workflow models\"\"\"\n\nfrom datetime import datetime\n\nfrom app import db\n\n\nclass InvoiceApproval(db.Mod"
},
{
"path": "app/models/invoice_email.py",
"chars": 3920,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef local_now():\n"
},
{
"path": "app/models/invoice_image.py",
"chars": 4887,
"preview": "import os\nfrom datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef loc"
},
{
"path": "app/models/invoice_pdf_template.py",
"chars": 8552,
"preview": "\"\"\"Invoice PDF Template Model\n\nStores PDF templates for different page sizes (A4, Letter, A3, etc.)\n\"\"\"\n\nfrom datetime i"
},
{
"path": "app/models/invoice_peppol.py",
"chars": 2761,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass InvoicePeppolTransmission(db.Model):\n \"\"\"Track Peppol sends"
},
{
"path": "app/models/invoice_template.py",
"chars": 810,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass InvoiceTemplate(db.Model):\n \"\"\"Reusable invoice templates/t"
},
{
"path": "app/models/issue.py",
"chars": 11294,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\nclass Issue(db.Mo"
},
{
"path": "app/models/kanban_column.py",
"chars": 8389,
"preview": "from app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\nclass KanbanColumn(db.Model):\n \"\"\"Model for c"
},
{
"path": "app/models/lead.py",
"chars": 7449,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_"
},
{
"path": "app/models/lead_activity.py",
"chars": 2786,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef local_now():\n"
},
{
"path": "app/models/link_template.py",
"chars": 4937,
"preview": "\"\"\"Link Template model for storing URL templates with field placeholders\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalch"
},
{
"path": "app/models/mileage.py",
"chars": 11272,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom sqlalchemy import Index\n\nfrom app import db\n\n\nclass Mile"
},
{
"path": "app/models/payment_gateway.py",
"chars": 3846,
"preview": "\"\"\"Payment gateway integration models\"\"\"\n\nfrom datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\n"
},
{
"path": "app/models/payments.py",
"chars": 4817,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\n\n\nclass Payment(db.Model):\n \"\"\"Partial/"
},
{
"path": "app/models/per_diem.py",
"chars": 18181,
"preview": "from datetime import datetime, timedelta\nfrom decimal import Decimal\n\nfrom sqlalchemy import Index\n\nfrom app import db\n\n"
},
{
"path": "app/models/permission.py",
"chars": 4812,
"preview": "\"\"\"Permission model for granular access control\"\"\"\n\nfrom datetime import datetime\n\nfrom app import db\n\n\nclass Permission"
},
{
"path": "app/models/project.py",
"chars": 15858,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\n\n\nclass Project(db.Model):\n \"\"\"Project "
},
{
"path": "app/models/project_attachment.py",
"chars": 4888,
"preview": "import os\nfrom datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef loc"
},
{
"path": "app/models/project_cost.py",
"chars": 6217,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\n\n\nclass ProjectCost(db.Model):\n \"\"\"Proj"
},
{
"path": "app/models/project_stock_allocation.py",
"chars": 3112,
"preview": "\"\"\"ProjectStockAllocation model for tracking stock allocated to projects\"\"\"\n\nfrom datetime import datetime\nfrom decimal "
},
{
"path": "app/models/project_template.py",
"chars": 2451,
"preview": "\"\"\"Project template model for reusable project configurations\"\"\"\n\nfrom datetime import datetime\nfrom decimal import Deci"
},
{
"path": "app/models/purchase_order.py",
"chars": 10592,
"preview": "\"\"\"Purchase Order models for inventory management\"\"\"\n\nfrom datetime import datetime\nfrom decimal import Decimal\n\nfrom ap"
},
{
"path": "app/models/push_subscription.py",
"chars": 2779,
"preview": "\"\"\"\nPush Subscription model for storing browser push notification subscriptions.\n\"\"\"\n\nimport json\nfrom datetime import d"
},
{
"path": "app/models/quote.py",
"chars": 27252,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom sqlalchemy import and_\n\nfrom app import db\nfrom app.util"
},
{
"path": "app/models/quote_attachment.py",
"chars": 4817,
"preview": "import os\nfrom datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef loc"
},
{
"path": "app/models/quote_image.py",
"chars": 4844,
"preview": "import os\nfrom datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef loc"
},
{
"path": "app/models/quote_template.py",
"chars": 7459,
"preview": "import json\nfrom datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef l"
},
{
"path": "app/models/quote_version.py",
"chars": 5405,
"preview": "import json\nfrom datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\ndef l"
},
{
"path": "app/models/rate_override.py",
"chars": 2782,
"preview": "from datetime import datetime\nfrom decimal import Decimal\n\nfrom app import db\n\n\nclass RateOverride(db.Model):\n \"\"\"Bil"
},
{
"path": "app/models/recurring_block.py",
"chars": 2952,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass RecurringBlock(db.Model):\n \"\"\"Recurring time block template"
},
{
"path": "app/models/recurring_invoice.py",
"chars": 6738,
"preview": "from datetime import datetime, timedelta\nfrom decimal import Decimal\n\nfrom dateutil.relativedelta import relativedelta\n\n"
},
{
"path": "app/models/recurring_task.py",
"chars": 6394,
"preview": "\"\"\"\nRecurring Task model for automated task creation\nSimilar to recurring invoices but for tasks\n\"\"\"\n\nfrom datetime impo"
},
{
"path": "app/models/reporting.py",
"chars": 3048,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass SavedReportView(db.Model):\n \"\"\"Saved configurations for the"
},
{
"path": "app/models/salesman_email_mapping.py",
"chars": 3350,
"preview": "\"\"\"\nSalesman Email Mapping Model\n\nMaps salesman initials (from client custom fields) to email addresses\nfor automated re"
},
{
"path": "app/models/saved_filter.py",
"chars": 1472,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass SavedFilter(db.Model):\n \"\"\"User-defined saved filters for r"
},
{
"path": "app/models/settings.py",
"chars": 49925,
"preview": "import os\nimport threading\nfrom datetime import datetime\n\nfrom app import db\nfrom app.config import Config\nfrom app.util"
},
{
"path": "app/models/stock_item.py",
"chars": 7874,
"preview": "\"\"\"StockItem model for inventory management\"\"\"\n\nfrom datetime import datetime\nfrom decimal import Decimal\n\nfrom app impo"
},
{
"path": "app/models/stock_lot.py",
"chars": 3595,
"preview": "\"\"\"Stock lot (valuation layer) models for inventory valuation and devaluation.\n\nThis project historically valued invento"
},
{
"path": "app/models/stock_movement.py",
"chars": 21177,
"preview": "\"\"\"StockMovement model for tracking inventory movements\"\"\"\n\nfrom datetime import datetime\nfrom decimal import Decimal\n\nf"
},
{
"path": "app/models/stock_reservation.py",
"chars": 6770,
"preview": "\"\"\"StockReservation model for reserving stock\"\"\"\n\nfrom datetime import datetime, timedelta\nfrom decimal import Decimal\n\n"
},
{
"path": "app/models/supplier.py",
"chars": 3524,
"preview": "\"\"\"Supplier model for inventory management\"\"\"\n\nfrom datetime import datetime\n\nfrom app import db\n\n\nclass Supplier(db.Mod"
},
{
"path": "app/models/supplier_stock_item.py",
"chars": 3777,
"preview": "\"\"\"SupplierStockItem model for many-to-many relationship between suppliers and stock items\"\"\"\n\nfrom datetime import date"
},
{
"path": "app/models/task.py",
"chars": 12031,
"preview": "from datetime import datetime\n\nfrom app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\nclass Task(db.Mod"
},
{
"path": "app/models/task_activity.py",
"chars": 1080,
"preview": "from app import db\nfrom app.utils.timezone import now_in_app_timezone\n\n\nclass TaskActivity(db.Model):\n \"\"\"Lightweight"
},
{
"path": "app/models/tax_rule.py",
"chars": 1369,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass TaxRule(db.Model):\n \"\"\"Flexible tax rules per country/regio"
},
{
"path": "app/models/team_chat.py",
"chars": 7426,
"preview": "\"\"\"\nTeam Chat models for real-time messaging\n\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy import Index\n\nfrom app "
},
{
"path": "app/models/time_entry.py",
"chars": 15523,
"preview": "from datetime import datetime, timedelta, timezone\n\nfrom app import db\nfrom app.config import Config\nfrom app.utils.time"
},
{
"path": "app/models/time_entry_approval.py",
"chars": 7788,
"preview": "\"\"\"\nTime Entry Approval models for manager approval workflow\n\"\"\"\n\nimport enum\nfrom datetime import datetime\n\nfrom sqlalc"
},
{
"path": "app/models/time_entry_template.py",
"chars": 4295,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass TimeEntryTemplate(db.Model):\n \"\"\"Quick-start templates for "
},
{
"path": "app/models/time_off.py",
"chars": 5504,
"preview": "import enum\nfrom datetime import datetime\n\nfrom sqlalchemy import Enum as SQLEnum\nfrom sqlalchemy import Index\n\nfrom ap"
},
{
"path": "app/models/timesheet_period.py",
"chars": 4212,
"preview": "import enum\nfrom datetime import date, datetime\n\nfrom sqlalchemy import Enum as SQLEnum\nfrom sqlalchemy import Index, U"
},
{
"path": "app/models/timesheet_policy.py",
"chars": 1915,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass TimesheetPolicy(db.Model):\n \"\"\"Configurable lock and appro"
},
{
"path": "app/models/user.py",
"chars": 26317,
"preview": "import os\nfrom datetime import datetime\n\nfrom flask_login import UserMixin\nfrom werkzeug.security import check_password_"
},
{
"path": "app/models/user_client.py",
"chars": 844,
"preview": "\"\"\"User-Client association for subcontractor scope (restrict user to assigned clients).\"\"\"\n\nfrom datetime import datetim"
},
{
"path": "app/models/user_favorite_project.py",
"chars": 1120,
"preview": "from datetime import datetime\n\nfrom app import db\n\n\nclass UserFavoriteProject(db.Model):\n \"\"\"Association table for us"
},
{
"path": "app/models/user_smart_notification_dismissal.py",
"chars": 752,
"preview": "\"\"\"Per-user dismissals for smart in-app notifications (by local calendar date and kind).\"\"\"\n\nfrom datetime import dateti"
},
{
"path": "app/models/warehouse.py",
"chars": 2777,
"preview": "\"\"\"Warehouse model for inventory management\"\"\"\n\nfrom datetime import datetime\n\nfrom app import db\n\n\nclass Warehouse(db.M"
},
{
"path": "app/models/warehouse_stock.py",
"chars": 4255,
"preview": "\"\"\"WarehouseStock model for tracking stock levels per warehouse\"\"\"\n\nfrom datetime import datetime\nfrom decimal import De"
},
{
"path": "app/models/webhook.py",
"chars": 10999,
"preview": "\"\"\"Webhook models for enabling integrations\"\"\"\n\nimport hashlib\nimport hmac\nimport json\nimport secrets\nfrom datetime impo"
},
{
"path": "app/models/weekly_time_goal.py",
"chars": 9745,
"preview": "from datetime import datetime, timedelta\n\nfrom sqlalchemy import func\n\nfrom app import db\n\n\ndef local_now():\n \"\"\"Get "
},
{
"path": "app/models/workflow.py",
"chars": 3874,
"preview": "\"\"\"\nWorkflow automation models for rule-based automation\n\"\"\"\n\nfrom datetime import datetime\n\nfrom sqlalchemy import JSON"
},
{
"path": "app/repositories/__init__.py",
"chars": 936,
"preview": "\"\"\"\nRepository layer for data access abstraction.\nThis layer provides a clean interface for database operations,\nmaking "
},
{
"path": "app/repositories/base_repository.py",
"chars": 4668,
"preview": "\"\"\"\nBase repository class providing common database operations.\n\nThis module provides the base repository pattern implem"
},
{
"path": "app/repositories/client_repository.py",
"chars": 917,
"preview": "\"\"\"\nRepository for client data access operations.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom sqlalchemy.orm import joi"
},
{
"path": "app/repositories/comment_repository.py",
"chars": 2509,
"preview": "\"\"\"\nRepository for comment data access operations.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom sqlalchemy.orm import jo"
},
{
"path": "app/repositories/expense_repository.py",
"chars": 2485,
"preview": "\"\"\"\nRepository for expense data access operations.\n\"\"\"\n\nfrom datetime import date, datetime\nfrom typing import List, Opt"
},
{
"path": "app/repositories/invoice_repository.py",
"chars": 3643,
"preview": "\"\"\"\nRepository for invoice data access operations.\n\"\"\"\n\nfrom datetime import date, datetime\nfrom typing import List, Opt"
},
{
"path": "app/repositories/payment_repository.py",
"chars": 2685,
"preview": "\"\"\"\nRepository for payment data access operations.\n\"\"\"\n\nfrom datetime import date\nfrom decimal import Decimal\nfrom typin"
},
{
"path": "app/repositories/project_repository.py",
"chars": 3410,
"preview": "\"\"\"\nRepository for project data access operations.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom sqlalchemy.orm import jo"
},
{
"path": "app/repositories/recurring_invoice_repository.py",
"chars": 1044,
"preview": "\"\"\"\nRepository for recurring invoice data access.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom app.models import Recurri"
},
{
"path": "app/repositories/task_repository.py",
"chars": 2413,
"preview": "\"\"\"\nRepository for task data access operations.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom sqlalchemy.orm import joine"
},
{
"path": "app/repositories/time_entry_repository.py",
"chars": 10103,
"preview": "\"\"\"\nRepository for time entry data access operations.\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import List, Option"
},
{
"path": "app/repositories/user_repository.py",
"chars": 998,
"preview": "\"\"\"\nRepository for user data access operations.\n\"\"\"\n\nfrom typing import List, Optional\n\nfrom app import db\nfrom app.cons"
},
{
"path": "app/resources/icc/LICENSE.txt",
"chars": 177,
"preview": "sRGB-v2-nano.icc is from Compact-ICC-Profiles by Liam R. E. Quin / saucecontrol,\ndistributed under the MIT License.\nSour"
},
{
"path": "app/routes/activity_feed.py",
"chars": 5102,
"preview": "\"\"\"\nActivity Feed routes\n\"\"\"\n\nfrom datetime import datetime, timedelta\n\nfrom flask import Blueprint, current_app, jsonif"
},
{
"path": "app/routes/admin.py",
"chars": 236134,
"preview": "import os\nimport shutil\nimport threading\nimport time\nimport uuid\nfrom datetime import datetime\n\nfrom flask import (\n "
},
{
"path": "app/routes/analytics.py",
"chars": 48275,
"preview": "import calendar\nfrom datetime import datetime, timedelta\n\nfrom flask import Blueprint, jsonify, render_template, request"
},
{
"path": "app/routes/api/__init__.py",
"chars": 1454,
"preview": "\"\"\"\nAPI Routes Package\n\nThis package contains versioned API routes.\nCurrent structure:\n- v1: Current stable API (migrate"
},
{
"path": "app/routes/api/v1/__init__.py",
"chars": 599,
"preview": "\"\"\"\nAPI v1 Routes\n\nThis module contains the v1 API endpoints.\nv1 is the current stable API version.\n\nAPI Versioning Poli"
},
{
"path": "app/routes/api.py",
"chars": 78875,
"preview": "import json\nimport os\nimport uuid\nfrom datetime import datetime, time, timedelta\n\nfrom flask import Blueprint, current_a"
},
{
"path": "app/routes/api_docs.py",
"chars": 27905,
"preview": "\"\"\"API Documentation with Swagger UI\"\"\"\n\nfrom flask import Blueprint, current_app, jsonify, render_template_string\n\nfrom"
},
{
"path": "app/routes/api_v1.py",
"chars": 185106,
"preview": "\"\"\"REST API v1 - Comprehensive API endpoints with token authentication\"\"\"\n\nfrom datetime import date, datetime, timedelt"
},
{
"path": "app/routes/api_v1_ai.py",
"chars": 1567,
"preview": "\"\"\"API v1 AI helper endpoints.\"\"\"\n\nfrom flask import Blueprint, g, jsonify, request\n\nfrom app.services.llm_service impor"
},
{
"path": "app/routes/api_v1_clients.py",
"chars": 8426,
"preview": "\"\"\"\nAPI v1 - Clients sub-blueprint.\nRoutes under /api/v1/clients.\n\"\"\"\n\nfrom flask import Blueprint, current_app, g, json"
},
{
"path": "app/routes/api_v1_common.py",
"chars": 3027,
"preview": "\"\"\"\nShared helpers for API v1 routes.\nUsed by api_v1.py and by domain-specific sub-blueprints (e.g. api_v1_time_entries)"
},
{
"path": "app/routes/api_v1_contacts.py",
"chars": 5253,
"preview": "\"\"\"\nAPI v1 - Contacts (CRM) sub-blueprint.\nRoutes under /api/v1/clients/<id>/contacts and /api/v1/contacts.\n\"\"\"\n\nfrom fl"
},
{
"path": "app/routes/api_v1_deals.py",
"chars": 6404,
"preview": "\"\"\"\nAPI v1 - Deals (CRM) sub-blueprint.\nRoutes under /api/v1/deals.\n\"\"\"\n\nfrom decimal import Decimal\n\nfrom flask import "
},
{
"path": "app/routes/api_v1_expenses.py",
"chars": 7110,
"preview": "\"\"\"\nAPI v1 - Expenses sub-blueprint.\nRoutes under /api/v1/expenses.\n\"\"\"\n\nfrom decimal import Decimal\n\nfrom flask import "
},
{
"path": "app/routes/api_v1_invoices.py",
"chars": 6405,
"preview": "\"\"\"\nAPI v1 - Invoices sub-blueprint.\nRoutes under /api/v1/invoices.\n\"\"\"\n\nfrom flask import Blueprint, current_app, g, js"
},
{
"path": "app/routes/api_v1_leads.py",
"chars": 6349,
"preview": "\"\"\"\nAPI v1 - Leads (CRM) sub-blueprint.\nRoutes under /api/v1/leads.\n\"\"\"\n\nfrom decimal import Decimal\n\nfrom flask import "
},
{
"path": "app/routes/api_v1_mileage.py",
"chars": 7124,
"preview": "\"\"\"\nAPI v1 - Mileage sub-blueprint.\nRoutes under /api/v1/mileage.\n\"\"\"\n\nfrom decimal import Decimal\n\nfrom flask import Bl"
},
{
"path": "app/routes/api_v1_payments.py",
"chars": 5176,
"preview": "\"\"\"\nAPI v1 - Payments sub-blueprint.\nRoutes under /api/v1/payments.\n\"\"\"\n\nfrom decimal import Decimal\n\nfrom flask import "
},
{
"path": "app/routes/api_v1_projects.py",
"chars": 5803,
"preview": "\"\"\"\nAPI v1 - Projects sub-blueprint.\nRoutes under /api/v1/projects.\n\"\"\"\n\nfrom flask import Blueprint, g, jsonify, reques"
},
{
"path": "app/routes/api_v1_tasks.py",
"chars": 4884,
"preview": "\"\"\"\nAPI v1 - Tasks sub-blueprint.\nRoutes under /api/v1/tasks.\n\"\"\"\n\nfrom flask import Blueprint, g, jsonify, request\n\nfro"
},
{
"path": "app/routes/api_v1_time_entries.py",
"chars": 16900,
"preview": "\"\"\"\nAPI v1 - Time Entries and Timer endpoints.\nSub-blueprint for /api/v1/time-entries and /api/v1/timer/*.\n\"\"\"\n\nfrom fla"
},
{
"path": "app/routes/audit_logs.py",
"chars": 8503,
"preview": "from datetime import datetime, timedelta\n\nfrom flask import Blueprint, abort, current_app, jsonify, render_template, req"
},
{
"path": "app/routes/auth.py",
"chars": 68253,
"preview": "from flask import (\n Blueprint,\n current_app,\n flash,\n redirect,\n render_template,\n request,\n send_"
},
{
"path": "app/routes/budget_alerts.py",
"chars": 14899,
"preview": "\"\"\"\nBudget Alerts Routes\n\nThis module provides API endpoints for managing budget alerts and forecasting.\n\"\"\"\n\nfrom datet"
},
{
"path": "app/routes/calendar.py",
"chars": 19018,
"preview": "import os\nfrom datetime import datetime, timedelta\n\nfrom flask import Blueprint, flash, jsonify, redirect, render_templa"
},
{
"path": "app/routes/client_notes.py",
"chars": 10821,
"preview": "from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for\nfrom flask_babel import gettext"
},
{
"path": "app/routes/client_portal.py",
"chars": 53673,
"preview": "\"\"\"Client Portal Routes\n\nProvides a simplified interface for clients to view their projects,\ninvoices, and time entries."
},
{
"path": "app/routes/client_portal_customization.py",
"chars": 5710,
"preview": "\"\"\"\nClient Portal Customization routes\n\"\"\"\n\nimport os\nimport uuid\n\nfrom flask import (\n Blueprint,\n current_app,\n "
},
{
"path": "app/routes/clients.py",
"chars": 58116,
"preview": "import csv\nimport io\nimport json\nfrom datetime import datetime, timedelta\nfrom decimal import Decimal, InvalidOperation\n"
},
{
"path": "app/routes/comments.py",
"chars": 16070,
"preview": "import os\nfrom datetime import datetime\n\nfrom flask import Blueprint, current_app, flash, jsonify, redirect, render_temp"
},
{
"path": "app/routes/contacts.py",
"chars": 8596,
"preview": "\"\"\"Routes for contact management\"\"\"\n\nfrom datetime import datetime\n\nfrom flask import Blueprint, flash, jsonify, redirec"
}
]
// ... and 1699 more files (download for full content)
About this extraction
This page contains the full source code of the DRYTRIX/TimeTracker GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1899 files (38.5 MB), approximately 10.2M tokens, and a symbol index with 9292 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.