Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5be0054
feat(telemetry): add install_id UUID and consent-aware telemetry service
evilguy4000 Mar 16, 2026
287020d
feat(telemetry): gate product analytics on opt-in and send base first…
evilguy4000 Mar 16, 2026
cd0ccd6
feat(telemetry): add daily base heartbeat and trigger opt-in ping on …
evilguy4000 Mar 16, 2026
94ab81c
feat(telemetry): clarify two-layer telemetry in settings and admin da…
evilguy4000 Mar 16, 2026
2733284
docs(telemetry): two-layer architecture, privacy, and PostHog dashboa…
evilguy4000 Mar 16, 2026
76b3d48
test(telemetry): consent gate, base first_seen/heartbeat, install_id
evilguy4000 Mar 16, 2026
4a5a5ce
chore: stop tracking gitignored files
evilguy4000 Mar 16, 2026
5629254
feat(db): add migrations for keyboard shortcuts overrides and client …
evilguy4000 Mar 16, 2026
404647e
feat(models): add ClientPortalDashboardPreference and update user/aud…
evilguy4000 Mar 16, 2026
16edb71
feat(client-portal): activity feed, report service, dashboard widgets…
evilguy4000 Mar 16, 2026
0a1f551
feat(settings): keyboard shortcut overrides and developer documentation
evilguy4000 Mar 16, 2026
c0fe92b
chore(integrations): update Jira and GitHub integration modules
evilguy4000 Mar 16, 2026
db1b882
chore(app): routes, utils, and bootstrap updates
evilguy4000 Mar 16, 2026
624a434
chore(ui): update static JS and base template
evilguy4000 Mar 16, 2026
7cad0c6
docs: align documentation with current implementation
evilguy4000 Mar 16, 2026
c35a12c
test: add and update tests for client portal, shortcuts, Jira, invent…
evilguy4000 Mar 16, 2026
084e0b3
i18n: remove orphaned bulk-task translation strings across locales
evilguy4000 Mar 16, 2026
b67428a
chore: update CHANGELOG for unreleased documentation and i18n audit
evilguy4000 Mar 16, 2026
7e059a0
feat(jira): add optional webhook signature verification (HMAC-SHA256)
evilguy4000 Mar 16, 2026
8c2714b
fix(activity-feed): validate date params and return 400 for invalid A…
evilguy4000 Mar 16, 2026
f05d772
feat(api): add read:inventory and write:inventory scopes for inventor…
evilguy4000 Mar 16, 2026
346d716
feat(client-portal): add report date range and CSV export
evilguy4000 Mar 16, 2026
2808140
fix(invoices): handle and surface PEPPOL compliance check exceptions
evilguy4000 Mar 16, 2026
0ade07e
fix(settings): redirect /settings and /settings/preferences to user.s…
evilguy4000 Mar 16, 2026
3654a6a
feat(offline): store method, headers, and body in queue for correct P…
evilguy4000 Mar 16, 2026
a50a4eb
docs: sync CHANGELOG and implementation status; add CODEBASE_AUDIT
evilguy4000 Mar 16, 2026
3d0ad83
Version Bump v5.0.0
evilguy4000 Mar 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
609 changes: 0 additions & 609 deletions .cursor/plans/mobile_and_desktop_apps_5c5af1fb.plan.md

This file was deleted.

12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **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).

### Changed
- **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).
Expand Down
106 changes: 23 additions & 83 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,125 +64,58 @@ def log_event(name: str, **kwargs):
try:
extra = {"request_id": getattr(g, "request_id", None), "event": name, **kwargs}
json_logger.info(name, extra=extra)
except Exception:
# Don't let logging errors break the application
pass
except Exception as e:
logging.getLogger(__name__).debug("Structured log_event failed: %s", e)


def identify_user(user_id, properties=None):
"""
Identify a user in PostHog with person properties.

Sets properties on the user for better segmentation, cohort analysis,
and personalization in PostHog.

Args:
user_id: The user ID (internal ID, not PII)
properties: Dict of properties to set (use $set and $set_once)
Identify a user in the analytics backend (consent-aware).
Delegates to telemetry service; only sent when detailed analytics is opted in.
"""
try:
posthog_api_key = os.getenv("POSTHOG_API_KEY", "")
if not posthog_api_key:
return
from app.telemetry.service import identify_user as _identify

posthog.identify(distinct_id=str(user_id), properties=properties or {})
except Exception:
# Don't let analytics errors break the application
pass
_identify(user_id, properties)
except Exception as e:
logging.getLogger(__name__).debug("Telemetry identify_user failed: %s", e)


def track_event(user_id, event_name, properties=None):
"""
Track a product analytics event via PostHog.

Enhanced to include contextual properties like user agent, referrer,
and deployment info for better analysis.

Args:
user_id: The user ID (internal ID, not PII)
event_name: Name of the event (use resource.action format)
properties: Dict of event properties (no PII)
Track a product analytics event (consent-aware).
Delegates to telemetry service; only sent when detailed analytics is opted in.
"""
try:
# Get PostHog API key - must be explicitly set to enable tracking
posthog_api_key = os.getenv("POSTHOG_API_KEY", "")
if not posthog_api_key:
return

# Enhance properties with context
enhanced_properties = properties or {}

# Add request context if available
try:
if request:
enhanced_properties.update(
{
"$current_url": request.url,
"$host": request.host,
"$pathname": request.path,
"$browser": request.user_agent.browser,
"$device_type": "mobile" if request.user_agent.platform in ["android", "iphone"] else "desktop",
"$os": request.user_agent.platform,
}
)
except Exception:
pass
from app.telemetry.service import send_analytics_event

# Add deployment context
# Get app version from analytics config
from app.config.analytics_defaults import get_analytics_config

analytics_config = get_analytics_config()

enhanced_properties.update(
{
"environment": os.getenv("FLASK_ENV", "production"),
"app_version": analytics_config.get("app_version"),
"deployment_method": "docker" if os.path.exists("/.dockerenv") else "native",
}
)

posthog.capture(distinct_id=str(user_id), event=event_name, properties=enhanced_properties)
send_analytics_event(user_id, event_name, properties)
except Exception:
# Don't let analytics errors break the application
pass


def track_page_view(page_name, user_id=None, properties=None):
"""
Track a page view event.

Args:
page_name: Name of the page (e.g., 'dashboard', 'projects_list')
user_id: User ID (optional, will use current_user if not provided)
properties: Additional properties for the page view
Track a page view event (consent-aware). Only sent when detailed analytics is opted in.
"""
try:
# Get user ID if not provided
if user_id is None:
from flask_login import current_user

if current_user.is_authenticated:
user_id = current_user.id
else:
return # Don't track anonymous page views

# Build page view properties
return
page_properties = {
"page_name": page_name,
"$pathname": request.path if request else None,
"$current_url": request.url if request else None,
}

# Add custom properties if provided
if properties:
page_properties.update(properties)

# Track the page view
track_event(user_id, "$pageview", page_properties)
except Exception:
# Don't let analytics errors break the application
pass
except Exception as e:
logging.getLogger(__name__).debug("Telemetry track_page_view failed: %s", e)


def create_app(config=None):
Expand Down Expand Up @@ -395,6 +328,13 @@ def create_app(config=None):
# Register tasks after app context is available, passing app instance
with app.app_context():
register_scheduled_tasks(scheduler, app=app)
# Base telemetry: send first_seen once per install (idempotent)
try:
from app.telemetry.service import send_base_first_seen

send_base_first_seen()
except Exception:
pass

# Only initialize CSRF protection if enabled
if app.config.get("WTF_CSRF_ENABLED"):
Expand Down
8 changes: 4 additions & 4 deletions app/integrations/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ def exchange_code_for_tokens(self, code: str, redirect_uri: str) -> Dict[str, An
)
if user_response.status_code == 200:
user_info = user_response.json()
except Exception:
pass
except Exception as e:
logger.debug("GitHub user fetch failed: %s", e)

return {
"access_token": access_token,
Expand Down Expand Up @@ -330,8 +330,8 @@ def sync_data(self, sync_type: str = "full") -> Dict[str, Any]:
logger.error(f"GitHub sync failed: {e}", exc_info=True)
try:
db.session.rollback()
except Exception:
pass
except Exception as rollback_err:
logger.debug("Rollback after GitHub sync failure: %s", rollback_err)
return {"success": False, "message": f"Sync failed: {str(e)}", "errors": errors}

def handle_webhook(
Expand Down
Loading
Loading