From d2d877f75de0d1697a2b69d92c89b811e3c3c7da Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Thu, 27 Aug 2026 20:04:10 +0200 Subject: [PATCH 1/3] fix(android): sign release APKs with a stable CI keystore Stop using the per-runner debug keystore so sideloaded updates no longer fail with signature mismatch (#740). --- .github/workflows/build-mobile.yml | 16 ++++++++++++++++ .github/workflows/cd-release.yml | 16 ++++++++++++++++ .gitignore | 5 +++++ mobile/android/app/build.gradle | 19 ++++++++++++++++++- 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index d68838a2..483ca1ac 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -48,6 +48,22 @@ jobs: - name: Run tests working-directory: mobile run: flutter test + + - name: Decode and configure Android signing + if: env.ANDROID_KEYSTORE_BASE64 != '' + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > mobile/android/release-key.jks + { + echo "storeFile=../release-key.jks" + echo "storePassword=$ANDROID_STORE_PASSWORD" + echo "keyAlias=$ANDROID_KEY_ALIAS" + echo "keyPassword=$ANDROID_KEY_PASSWORD" + } > mobile/android/key.properties - name: Build APK working-directory: mobile diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index 5966dc7b..bdcfe382 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -1012,6 +1012,22 @@ jobs: - name: Run mobile tests working-directory: mobile run: flutter test + + - name: Decode and configure Android signing + if: env.ANDROID_KEYSTORE_BASE64 != '' + env: + ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > mobile/android/release-key.jks + { + echo "storeFile=../release-key.jks" + echo "storePassword=$ANDROID_STORE_PASSWORD" + echo "keyAlias=$ANDROID_KEY_ALIAS" + echo "keyPassword=$ANDROID_KEY_PASSWORD" + } > mobile/android/key.properties - name: Build APK working-directory: mobile diff --git a/.gitignore b/.gitignore index dd6cafaf..63ec6e44 100644 --- a/.gitignore +++ b/.gitignore @@ -201,6 +201,11 @@ desktop/*.p12 desktop/*.spc desktop/*.pvk +# Android release signing (never commit) +mobile/android/key.properties +mobile/android/*.jks +mobile/android/*.keystore + # Docker Compose overrides (do not ignore auto files, only manually generated) # docker-compose.https.yml is now tracked diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index f9f4972c..20e4e234 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -22,6 +22,13 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } +// Load key.properties if present (written by CI; absent in local dev → falls back to debug) +def keyPropertiesFile = rootProject.file('key.properties') +def keyProperties = new Properties() +if (keyPropertiesFile.exists()) { + keyPropertiesFile.withReader('UTF-8') { reader -> keyProperties.load(reader) } +} + android { namespace "com.timetracker.mobile" compileSdk 36 @@ -49,9 +56,19 @@ android { versionName flutterVersionName } + signingConfigs { + release { + keyAlias keyProperties['keyAlias'] + keyPassword keyProperties['keyPassword'] + storeFile keyProperties['storeFile'] ? file(keyProperties['storeFile']) : null + storePassword keyProperties['storePassword'] + } + } + buildTypes { release { - signingConfig signingConfigs.debug + // Use release signing if key.properties is present, otherwise fall back to debug + signingConfig keyPropertiesFile.exists() ? signingConfigs.release : signingConfigs.debug minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } From 81c0b8d486d4a268be74dd6e38f6aba3ab5603ea Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Thu, 27 Aug 2026 20:04:33 +0200 Subject: [PATCH 2/3] feat(integrations): export all connectors and add Linear setup wizard Re-export the full connector registry from the package init and add a guided Linear API-key wizard so setup matches the other providers. --- app/integrations/__init__.py | 28 ++- app/routes/integrations.py | 26 ++- app/templates/integrations/wizard_linear.html | 165 ++++++++++++++++++ 3 files changed, 215 insertions(+), 4 deletions(-) create mode 100644 app/templates/integrations/wizard_linear.html diff --git a/app/integrations/__init__.py b/app/integrations/__init__.py index bed84de7..b674a98d 100644 --- a/app/integrations/__init__.py +++ b/app/integrations/__init__.py @@ -2,14 +2,36 @@ Integration connectors package. """ +from .activitywatch import ActivityWatchConnector +from .asana import AsanaConnector from .base import BaseConnector -from .github_connector import GitHubConnector -from .google_calendar_connector import GoogleCalendarConnector -from .slack_connector import SlackConnector +from .caldav_calendar import CalDAVCalendarConnector +from .github import GitHubConnector +from .gitlab import GitLabConnector +from .google_calendar import GoogleCalendarConnector +from .jira import JiraConnector +from .linear import LinearConnector +from .microsoft_teams import MicrosoftTeamsConnector +from .outlook_calendar import OutlookCalendarConnector +from .quickbooks import QuickBooksConnector +from .slack import SlackConnector +from .trello import TrelloConnector +from .xero import XeroConnector __all__ = [ "BaseConnector", + "ActivityWatchConnector", + "AsanaConnector", + "CalDAVCalendarConnector", "GitHubConnector", + "GitLabConnector", "GoogleCalendarConnector", + "JiraConnector", + "LinearConnector", + "MicrosoftTeamsConnector", + "OutlookCalendarConnector", + "QuickBooksConnector", "SlackConnector", + "TrelloConnector", + "XeroConnector", ] diff --git a/app/routes/integrations.py b/app/routes/integrations.py index cb4f7e91..c2e93135 100644 --- a/app/routes/integrations.py +++ b/app/routes/integrations.py @@ -1547,6 +1547,29 @@ def setup_wizard(provider): flash(result["message"], "error") return redirect(url_for("integrations.setup_wizard", provider=provider)) + # Linear uses a Personal API Key stored as integration credentials + if provider == "linear" and integration: + linear_api_key = request.form.get("linear_api_key", "").strip() + if linear_api_key: + key_result = service.save_credentials( + integration_id=integration.id, + access_token=linear_api_key, + refresh_token=None, + expires_at=None, + token_type="Bearer", + scope="read", + extra_data={"auth_type": "api_key"}, + ) + if key_result.get("success"): + integration.is_active = True + else: + return jsonify( + { + "success": False, + "message": key_result.get("message", _("Could not save API key.")), + } + ) + # Update integration config integration.config = current_config from sqlalchemy.orm.attributes import flag_modified @@ -1588,6 +1611,7 @@ def setup_wizard(provider): "github": [_("OAuth"), _("Repositories"), _("Sync Config"), _("Webhooks"), _("Review")], "asana": [_("OAuth"), _("Workspace"), _("Projects"), _("Sync Config"), _("Review")], "trello": [_("API Keys"), _("Connection Test"), _("Review")], + "linear": [_("API Key"), _("Sync Config"), _("Test & Finish")], "outlook_calendar": [_("Tenant ID"), _("OAuth"), _("Review")], "microsoft_teams": [_("Tenant ID"), _("OAuth"), _("Review")], } @@ -1597,7 +1621,7 @@ def setup_wizard(provider): # Get test connection URL if available test_connection_url = None - if provider in ["jira", "gitlab", "trello"]: + if provider in ["jira", "gitlab", "trello", "linear"]: test_connection_url = url_for("integrations.test_connection_wizard", provider=provider) wizard_title = _("%(name)s Setup Wizard", name=display_name) diff --git a/app/templates/integrations/wizard_linear.html b/app/templates/integrations/wizard_linear.html new file mode 100644 index 00000000..b625a62c --- /dev/null +++ b/app/templates/integrations/wizard_linear.html @@ -0,0 +1,165 @@ +{% extends "integrations/wizard_base.html" %} + +{% block wizard_steps %} + +
+

{{ _('Step 1: API Key') }}

+
+ {% if current_user.is_admin %} +
+

+ + {{ _('Linear uses a Personal API Key (not OAuth). Create one at') }} + linear.app/settings/api. +

+
+ +
+ + +

+ {{ _('Leave empty only if a key is already saved and you are updating sync settings.') }} +

+
+ {% else %} +
+

+ + {{ _('Only administrators can configure the Linear API key.') }} +

+
+ {% endif %} +
+
+ + + + + + +{% endblock %} + +{% block wizard_js %} + +{% endblock %} From c5321a5fb721415c5e47ce4c719c63963d2ca4b1 Mon Sep 17 00:00:00 2001 From: Dries Peeters Date: Thu, 27 Aug 2026 20:06:18 +0200 Subject: [PATCH 3/3] chore(release): bump version to 5.13.2 and update docs Syncs setup.py, desktop/package.json, mobile/pubspec.yaml, and browser-extension manifests to 5.13.2. Adds the corresponding CHANGELOG entry and README highlights for the release. --- CHANGELOG.md | 16 +++++++++++++--- README.md | 4 ++++ browser-extension/manifest.json | 2 +- browser-extension/package.json | 2 +- desktop/package.json | 2 +- mobile/pubspec.yaml | 2 +- setup.py | 2 +- 7 files changed, 22 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ba48b3a..e5c24e0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed +## [5.13.2] - 2026-08-27 + +### Added + +- **Linear setup wizard** — Guided API-key wizard for Linear so integration setup matches the other providers; the connector registry is re-exported from the integrations package init. -- **Web manual entry cascade & inline create (#728)** — Manual time logging and the dashboard start-timer modal now follow the mobile start-timer flow: pick a client first (the project picker unlocks only then), then one of that client's projects, then a task of that project. The client field stays a real searchable dropdown even when only one client exists, so another client can be picked or created inline; a newly created client is selected automatically. Inline project creation attaches automatically to the selected client — the client dropdown is replaced by a read-only display with an editable hourly rate pre-filled from the client default — instead of asking for a client again. Inline task creation creates the task immediately from the combobox "Create …" row (no confirmation modal) for the preselected project. -- **Searchable combobox dark mode & typing (#728)** — Combobox dropdowns build their DOM in JS with utility classes Tailwind purged from the stylesheet, leaving white-on-white rows in dark mode; the dropdown now uses always-emitted component classes (`tt-searchable-*`) with proper dark hover/selected states. Focusing a combobox selects its text so typing replaces the committed label instead of appending to it. Source-controlled assets (`searchable-select.js`, `inline-create.js`, `dist/output.css`) are served with mtime-based version queries via a new `static_url()` helper so edits bust the browser cache without a release bump. ### Changed - **Mobile start timer flow** — Client, project, and task selection in the start timer sheet now use one consistent searchable picker; picking a client filters the project list to that client and picking a project filters tasks accordingly. Projects can also be picked directly without choosing a client first, which then auto-fills the client. Typing an unknown name offers inline "Create …" for clients, projects, and tasks via the existing v1 endpoints. The projects screen is sorted by most recently used (`last_used_at`) so recent work can be restarted with one tap. +- **Client versions** — Synced Electron (`desktop/package.json`), Flutter (`mobile/pubspec.yaml`), and Chromium extension (`browser-extension/manifest.json` / `package.json`) to **5.13.2** with the webapp (`setup.py`). ### Fixed +- **Web manual entry cascade & inline create (#728)** — Manual time logging and the dashboard start-timer modal now follow the mobile start-timer flow: pick a client first (the project picker unlocks only then), then one of that client's projects, then a task of that project. The client field stays a real searchable dropdown even when only one client exists, so another client can be picked or created inline; a newly created client is selected automatically. Inline project creation attaches automatically to the selected client — the client dropdown is replaced by a read-only display with an editable hourly rate pre-filled from the client default — instead of asking for a client again. Inline task creation creates the task immediately from the combobox "Create …" row (no confirmation modal) for the preselected project. +- **Searchable combobox dark mode & typing (#728)** — Combobox dropdowns build their DOM in JS with utility classes Tailwind purged from the stylesheet, leaving white-on-white rows in dark mode; the dropdown now uses always-emitted component classes (`tt-searchable-*`) with proper dark hover/selected states. Focusing a combobox selects its text so typing replaces the committed label instead of appending to it. Source-controlled assets (`searchable-select.js`, `inline-create.js`, `dist/output.css`) are served with mtime-based version queries via a new `static_url()` helper so edits bust the browser cache without a release bump. - **Mobile start timer crash** — Two `FloatingActionButton`s with the default Hero tag coexisted in the home screen's `IndexedStack`, turning the screen red whenever a route was pushed on top (e.g. the start timer sheet); the Entries tab FAB now has a unique `heroTag`. - **Mobile inline creation selection** — The v1 create endpoints wrap the payload (`{"message": …, "": {…}}`); unwrapping it ensures a newly created client/project/task is selected immediately in the start timer sheet. - **Mobile stop/pause flicker** — Background timer polling no longer toggles `isLoading`, which disabled the Stop and Pause buttons for the duration of each 5-second sync; the flag is now reserved for user-initiated actions. +- **Android release signing** — CI signs release APKs with a stable keystore so updates install over previous builds. + +### Documentation + +- **Version** — Bumped `setup.py` to **5.13.2** (single source of truth for the application version). ## [5.13.1] - 2026-08-26 diff --git a/README.md b/README.md index 74daf947..d5d63187 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,10 @@ TimeTracker has been continuously enhanced with powerful new features! Here's wh **Current version** is defined in `setup.py` (single source of truth). See [CHANGELOG.md](CHANGELOG.md) for versioned release history. +### ✨ Highlights of v5.13.2 + +**Patch (5.13.2):** **Linear setup wizard** — guided API-key setup matching other integrations. **Web manual entry (#728)** — cascading client → project → task pickers with inline create and dark-mode comboboxes. **Mobile start timer** — unified searchable pickers, project-first with client auto-fill, crash/selection/flicker fixes. **Android signing** — stable CI keystore for release APKs. See [CHANGELOG.md](CHANGELOG.md#5132---2026-08-27). + ### ✨ Highlights of v5.13.1 **Patch (5.13.1):** **Projects by last used (#738)** — pickers and API lists surface recent work first. **Idle timeout (#722)** — auto-stop records the configured duration and surfaces Still working? on web, extension, and mobile. **Picker fixes (#728)** — blur resets typed-but-unselected text, Create project requires a client, translated Create labels. **Extension release zip** — Chromium extension package attached to GitHub Releases. See [CHANGELOG.md](CHANGELOG.md#5131---2026-08-26). diff --git a/browser-extension/manifest.json b/browser-extension/manifest.json index 3937a1ad..34f60ffe 100644 --- a/browser-extension/manifest.json +++ b/browser-extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "TimeTracker Timer", - "version": "5.13.1", + "version": "5.13.2", "description": "Start and stop TimeTracker timers from your browser toolbar.", "permissions": ["storage", "alarms", "idle", "notifications"], "optional_host_permissions": ["http://*/*", "https://*/*"], diff --git a/browser-extension/package.json b/browser-extension/package.json index 94d982c2..6acb3213 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -1,7 +1,7 @@ { "name": "timetracker-browser-extension", "private": true, - "version": "5.13.1", + "version": "5.13.2", "description": "TimeTracker Chromium extension (Manifest V3)", "type": "module", "scripts": { diff --git a/desktop/package.json b/desktop/package.json index 85365160..e837c5f5 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "timetracker-desktop", - "version": "5.13.1", + "version": "5.13.2", "description": "TimeTracker desktop app for Windows, Linux, and macOS", "main": "src/main/main.js", "scripts": { diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 70cd0216..d93c5d9e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -1,7 +1,7 @@ name: timetracker_mobile description: TimeTracker mobile app for Android and iOS publish_to: 'none' -version: 5.13.1+1 +version: 5.13.2+1 environment: sdk: '>=3.0.0 <4.0.0' diff --git a/setup.py b/setup.py index 6d6503e3..c7936d6d 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='timetracker', - version='5.13.1', + version='5.13.2', packages=find_packages(), include_package_data=True, package_data={