From 3a2f9fab12ce0ccbe9bed7829830e6f4c65a7ddf Mon Sep 17 00:00:00 2001 From: Chinmay Chaudhari Date: Thu, 1 Oct 2026 22:43:39 +0530 Subject: [PATCH 1/6] fix(sync): stop logging the TaskChampion client id and encryption secret Replica.sync() printed both credentials in plain text on every sync. debugPrint is persisted to the debug-log database by main(), so the secret was visible on the in-app Logs page and in logcat. Log only the server URL and whether each credential is set. Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/app/v3/champion/replica.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/app/v3/champion/replica.dart b/lib/app/v3/champion/replica.dart index 0e7ece48f..ae538f054 100644 --- a/lib/app/v3/champion/replica.dart +++ b/lib/app/v3/champion/replica.dart @@ -25,6 +25,7 @@ class Replica { // interprets. The desktop CLI is what acts on it. "recur", ]; + /// Create a task, returning null on success or the reason it was refused. /// /// This used to answer with a success/failure sentinel that no caller read — @@ -194,8 +195,11 @@ class Replica { var url = await CredentialsStorage.getApiUrl(); var clientId = await CredentialsStorage.getClientId(); var encryptionSecret = await CredentialsStorage.getEncryptionSecret(); - debugPrint( - "Syncing Replica with url=$url clientId=$clientId encryptionSecret=$encryptionSecret"); + // Never log the client id or encryption secret: debugPrint output is + // persisted to the debug-log database shown on the Logs page. + debugPrint("Syncing Replica with url=$url " + "(clientId set: ${clientId?.isNotEmpty ?? false}, " + "encryptionSecret set: ${encryptionSecret?.isNotEmpty ?? false})"); await sync_( taskdbDirPath: taskdbDirPath, url: url ?? "", From 54872855b6fc30fc97b65f42e074f2354aeea141 Mon Sep 17 00:00:00 2001 From: Chinmay Chaudhari Date: Thu, 1 Oct 2026 22:45:02 +0530 Subject: [PATCH 2/6] fix(home): "Hide Waiting" hid every task except waiting ones The waiting filter is on by default and its switch reads "Hide Waiting" while on, but _refreshTasks kept only tasks with a future wait date. On a new profile every ordinary task was therefore hidden, so newly added tasks seemed to vanish. Leave waiting tasks out instead, matching the label and Taskwarrior's default. Extracts isWaiting() so the rule is unit-tested. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../home/controllers/home_controller.dart | 12 ++++---- lib/app/utils/taskfunctions/waiting.dart | 5 ++++ test/utils/taskfunctions/waiting_test.dart | 28 +++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 lib/app/utils/taskfunctions/waiting.dart create mode 100644 test/utils/taskfunctions/waiting_test.dart diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 78575731e..27fefcbbc 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -29,6 +29,7 @@ import 'package:taskwarrior/app/utils/taskfunctions/comparator.dart'; import 'package:taskwarrior/app/utils/taskfunctions/projects.dart'; import 'package:taskwarrior/app/utils/taskfunctions/query.dart'; import 'package:taskwarrior/app/utils/taskfunctions/tags.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/waiting.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; import 'package:taskwarrior/app/v3/champion/replica.dart'; import 'package:taskwarrior/app/v3/champion/models/task_for_replica.dart'; @@ -43,6 +44,7 @@ class HomeController extends GetxController { final SplashController splashController = Get.find(); late Storage storage; final RxBool pendingFilter = false.obs; + /// Which status the list is filtered to: pending / completed / deleted / /// recurring. /// Supersedes [pendingFilter], which can only express the first two; that @@ -263,11 +265,12 @@ class HomeController extends GetxController { queriedTasks.value = storage.data.completedData(); } + // The switch is labelled "Hide Waiting" while on, so on means waiting + // tasks are left out (Taskwarrior's default), not that only they show. if (waitingFilter.value) { var currentTime = DateTime.now(); - queriedTasks.value = queriedTasks - .where((task) => task.wait != null && task.wait!.isAfter(currentTime)) - .toList(); + queriedTasks.value = + queriedTasks.where((task) => !isWaiting(task, currentTime)).toList(); } if (projectFilter.value.isNotEmpty) { @@ -795,8 +798,7 @@ class HomeController extends GetxController { /// Which projects column the filter drawer is currently showing. The two sit /// behind mutually exclusive `Visibility` widgets, so only one of /// [projectsKey] / [projectsKeyTaskc] is ever laid out. - bool get usesTaskchampionProjects => - taskchampion.value || taskReplica.value; + bool get usesTaskchampionProjects => taskchampion.value || taskReplica.value; void initFilterDrawerTour() { tutorialCoachMark = TutorialCoachMark( diff --git a/lib/app/utils/taskfunctions/waiting.dart b/lib/app/utils/taskfunctions/waiting.dart new file mode 100644 index 000000000..9bd187d94 --- /dev/null +++ b/lib/app/utils/taskfunctions/waiting.dart @@ -0,0 +1,5 @@ +import 'package:taskwarrior/app/models/models.dart'; + +/// Whether [task] is waiting: hidden until its wait date, as in Taskwarrior. +bool isWaiting(Task task, DateTime now) => + task.wait != null && task.wait!.isAfter(now); diff --git a/test/utils/taskfunctions/waiting_test.dart b/test/utils/taskfunctions/waiting_test.dart new file mode 100644 index 000000000..9bd93e169 --- /dev/null +++ b/test/utils/taskfunctions/waiting_test.dart @@ -0,0 +1,28 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/models/models.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/waiting.dart'; + +void main() { + final now = DateTime.utc(2026, 10, 1, 12); + + Task task({DateTime? wait}) => Task((b) => b + ..uuid = 'u' + ..description = 'd' + ..status = 'pending' + ..entry = now + ..wait = wait); + + test('a task without a wait date is not waiting', () { + expect(isWaiting(task(), now), isFalse); + }); + + test('a task whose wait date is in the future is waiting', () { + expect( + isWaiting(task(wait: now.add(const Duration(days: 1))), now), isTrue); + }); + + test('a task whose wait date has passed is no longer waiting', () { + expect(isWaiting(task(wait: now.subtract(const Duration(days: 1))), now), + isFalse); + }); +} From c16b0de824b8eb212fae2ce15e5cf7a83c750731 Mon Sep 17 00:00:00 2001 From: Chinmay Chaudhari Date: Thu, 1 Oct 2026 22:45:41 +0530 Subject: [PATCH 3/6] fix(home): show task times in local time, not UTC age() and when() formatted the time of day straight from the stored DateTime, which is UTC, so the home list showed e.g. "0s ago (03:47 PM)" at 9:17 PM IST. The relative part ("0s ago", "2w") was already right. Convert to local time before formatting. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../taskfunctions/datetime_differences.dart | 6 ++++-- .../taskfunctions/datetime_differences_test.dart | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/app/utils/taskfunctions/datetime_differences.dart b/lib/app/utils/taskfunctions/datetime_differences.dart index 41586ced6..bc29a3597 100644 --- a/lib/app/utils/taskfunctions/datetime_differences.dart +++ b/lib/app/utils/taskfunctions/datetime_differences.dart @@ -25,7 +25,8 @@ String age(DateTime dt, {bool? use24HourFormat}) { // Format the time part according to the format preference String timeFormat = format ? 'HH:mm' : 'hh:mm a'; - String formattedTime = DateFormat(timeFormat).format(dt); + // Task dates are stored in UTC; show the clock time the user expects. + String formattedTime = DateFormat(timeFormat).format(dt.toLocal()); return '$result ago ($formattedTime)'; } @@ -54,7 +55,8 @@ String when(DateTime dt, {bool? use24HourFormat}) { // Format the time part according to the format preference String timeFormat = format ? 'HH:mm' : 'hh:mm a'; - String formattedTime = DateFormat(timeFormat).format(dt); + // Task dates are stored in UTC; show the clock time the user expects. + String formattedTime = DateFormat(timeFormat).format(dt.toLocal()); return '$result ($formattedTime)'; } diff --git a/test/utils/taskfunctions/datetime_differences_test.dart b/test/utils/taskfunctions/datetime_differences_test.dart index d0a5b8e40..e3a5db076 100644 --- a/test/utils/taskfunctions/datetime_differences_test.dart +++ b/test/utils/taskfunctions/datetime_differences_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:get/get.dart'; +import 'package:intl/intl.dart'; import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; import 'package:taskwarrior/app/utils/taskfunctions/datetime_differences.dart'; @@ -89,4 +90,19 @@ void main() { expect(when(dt), matches(r'.*\d{1,2}:\d{2}\)')); }); }); + + group('time of day is shown in local time', () { + // Task dates are stored in UTC. These only distinguish UTC from local + // time when the test machine is not itself on UTC. + final utc = DateTime.now().toUtc().subtract(const Duration(hours: 3)); + final local = DateFormat('hh:mm a').format(utc.toLocal()); + + test('age', () { + expect(age(utc), endsWith('($local)')); + }); + + test('when', () { + expect(when(utc), endsWith('($local)')); + }); + }); } From 79503abf2d8cf58704b109ed18e0a1c6d4e1de06 Mon Sep 17 00:00:00 2001 From: Chinmay Chaudhari Date: Thu, 1 Oct 2026 22:46:25 +0530 Subject: [PATCH 4/6] fix(widget): find the home-screen widget provider in the nightly flavor home_widget resolves the provider as "." unless a qualified name is given. The nightly flavor's application id ends in .nightly while TaskWarriorWidgetProvider lives in the base package, so every widget update threw ClassNotFoundException. Pass the qualified class name at both call sites. The update in WidgetController was also returned without await inside try, so the PlatformException escaped as an unhandled error; await it, and catch errors from the fire-and-forget update in HomeController too. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../home/controllers/home_controller.dart | 8 ++++++-- .../home/controllers/widget.controller.dart | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 27fefcbbc..cb8b8ded9 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -739,8 +739,12 @@ class HomeController extends GetxController { HomeWidget.saveWidgetData( "themeMode", AppSettings.isDarkMode ? "dark" : "light"); HomeWidget.updateWidget( - androidName: "TaskWarriorWidgetProvider", - iOSName: "TaskWarriorWidgets"); + qualifiedAndroidName: kAndroidWidgetProvider, + iOSName: "TaskWarriorWidgets") + .catchError((Object e) { + debugPrint('Error updating widget theme: $e'); + return null; + }); // print("called and value is${isDarkModeOn.value}"); } diff --git a/lib/app/modules/home/controllers/widget.controller.dart b/lib/app/modules/home/controllers/widget.controller.dart index 4137f137f..2c3cc1c74 100644 --- a/lib/app/modules/home/controllers/widget.controller.dart +++ b/lib/app/modules/home/controllers/widget.controller.dart @@ -16,6 +16,15 @@ import 'package:taskwarrior/app/modules/splash/controllers/splash_controller.dar import 'package:taskwarrior/app/utils/taskfunctions/urgency.dart'; // import 'package:taskwarrior/widgets/taskfunctions/datetime_differences.dart'; +/// Fully qualified home-screen widget provider class. +/// +/// home_widget otherwise prefixes the provider name with the application id, +/// which is `com.ccextractor.taskwarriorflutter.nightly` for the nightly +/// flavor, while the Kotlin class always lives in the base package; the +/// lookup then fails with ClassNotFoundException. +const String kAndroidWidgetProvider = + 'com.ccextractor.taskwarriorflutter.TaskWarriorWidgetProvider'; + class WidgetController extends GetxController { final HomeController storageWidget = Get.find(); late Storage storage; @@ -181,8 +190,11 @@ class WidgetController extends GetxController { Future updateWidget() async { try { - return HomeWidget.updateWidget( - name: 'TaskWarriorWidgetProvider', iOSName: 'TaskWarriorWidgets'); + // Awaited so a failure is caught here instead of surfacing as an + // unhandled exception. + return await HomeWidget.updateWidget( + qualifiedAndroidName: kAndroidWidgetProvider, + iOSName: 'TaskWarriorWidgets'); } on PlatformException catch (exception) { debugPrint('Error Updating Widget. $exception'); } From f020e4eacd948b803417bebd9e17bbfbc2c94dd0 Mon Sep 17 00:00:00 2001 From: Chinmay Chaudhari Date: Thu, 1 Oct 2026 22:47:35 +0530 Subject: [PATCH 5/6] fix(add-task): don't save an empty project or drop project: from the description The local add-task path always set project to the Project field's text, after taskParser had run. An empty field therefore saved project as '' (shown as a blank project), and a project typed in the description ("buy milk project:home") was overwritten. The TaskChampion and Replica paths already treated an empty field as no project. Use the field when it has text, otherwise keep the parsed project. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../home/views/add_task_bottom_sheet_new.dart | 4 +++- .../taskfunctions/add_task_dialog_utils.dart | 7 +++++++ .../taskfunctions/resolve_project_test.dart | 20 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 test/utils/taskfunctions/resolve_project_test.dart diff --git a/lib/app/modules/home/views/add_task_bottom_sheet_new.dart b/lib/app/modules/home/views/add_task_bottom_sheet_new.dart index e5fef026b..f133af6f1 100644 --- a/lib/app/modules/home/views/add_task_bottom_sheet_new.dart +++ b/lib/app/modules/home/views/add_task_bottom_sheet_new.dart @@ -391,7 +391,9 @@ class AddTaskBottomSheet extends StatelessWidget { ..priority = homeController.priority.value == 'X' ? null : homeController.priority.value) - .rebuild((t) => t..project = homeController.projectcontroller.text) + .rebuild((t) => t + ..project = resolveProject( + homeController.projectcontroller.text, t.project)) .rebuild((t) => t..wait = getWaitDate(homeController.selectedDates)?.toUtc()) .rebuild((t) => diff --git a/lib/app/utils/taskfunctions/add_task_dialog_utils.dart b/lib/app/utils/taskfunctions/add_task_dialog_utils.dart index b79242ce0..75392b37b 100644 --- a/lib/app/utils/taskfunctions/add_task_dialog_utils.dart +++ b/lib/app/utils/taskfunctions/add_task_dialog_utils.dart @@ -40,3 +40,10 @@ DateTime? getSchedDate(List dates) { DateTime? getUntilDate(List dates) { return dates[3]; } + +/// The project for a new task: the Project field when it has text, otherwise +/// whatever the description set (e.g. `project:home`), otherwise none. +String? resolveProject(String fieldText, String? parsedProject) { + final String project = fieldText.trim(); + return project.isEmpty ? parsedProject : project; +} diff --git a/test/utils/taskfunctions/resolve_project_test.dart b/test/utils/taskfunctions/resolve_project_test.dart new file mode 100644 index 000000000..34ad4dc14 --- /dev/null +++ b/test/utils/taskfunctions/resolve_project_test.dart @@ -0,0 +1,20 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/add_task_dialog_utils.dart'; +import 'package:taskwarrior/app/utils/taskfunctions/taskparser.dart'; + +void main() { + test('an empty Project field leaves the project unset', () { + expect(resolveProject('', null), isNull); + expect(resolveProject(' ', null), isNull); + }); + + test('the Project field is used when filled in', () { + expect(resolveProject('home', null), 'home'); + expect(resolveProject(' home ', 'work'), 'home'); + }); + + test('an empty field keeps a project typed in the description', () { + final parsed = taskParser('buy milk project:home'); + expect(resolveProject('', parsed.project), 'home'); + }); +} From bafae83ed65e39e3b1860ef1ca7597988fd17a69 Mon Sep 17 00:00:00 2001 From: Chinmay Chaudhari Date: Thu, 1 Oct 2026 22:55:49 +0530 Subject: [PATCH 6/6] fix(sync): stop logging TaskChampion credentials on refresh The home refresh button printed the client id and encryption secret ("Replica Credentials: c=... e=...") on every press, the same leak as Replica.sync(). Missed in the first pass because the values were in one-letter variables. Log only whether each credential is set. Co-Authored-By: Claude Opus 5.5 (1M context) --- lib/app/modules/home/views/home_page_app_bar.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/app/modules/home/views/home_page_app_bar.dart b/lib/app/modules/home/views/home_page_app_bar.dart index 0e3023dda..a77218f20 100644 --- a/lib/app/modules/home/views/home_page_app_bar.dart +++ b/lib/app/modules/home/views/home_page_app_bar.dart @@ -136,8 +136,11 @@ class HomePageAppBar extends StatelessWidget implements PreferredSizeWidget { var c = await CredentialsStorage.getClientId(); var e = await CredentialsStorage.getEncryptionSecret(); - debugPrint( - "controller.taskReplica.value ${controller.taskReplica.value} Replica Credentials: c=$c e=$e"); + // Never log the credentials themselves; debugPrint + // is persisted to the in-app Logs page. + debugPrint("Replica refresh " + "(clientId set: ${c != null}, " + "encryptionSecret set: ${e != null})"); if (c == null || e == null) { _showResultSnackBar( context,