Skip to content

fix(taskserver): resolve bugs and improve CCSync v3 integration#618

Closed
inderjeet20 wants to merge 1 commit intoCCExtractor:mainfrom
inderjeet20:tw3Bugs
Closed

fix(taskserver): resolve bugs and improve CCSync v3 integration#618
inderjeet20 wants to merge 1 commit intoCCExtractor:mainfrom
inderjeet20:tw3Bugs

Conversation

@inderjeet20
Copy link
Contributor

@inderjeet20 inderjeet20 commented Mar 8, 2026

Summary

This PR resolves critical bugs, adds missing features, and improves the user experience for TaskServer 3 (CCSync v3) in the Flutter app.
fixes #617

Changes

🐛 Bug Fixes

  • Replaced hardcoded origin URL with dynamic fetching.
  • Fixed ScaffoldMessenger crash in complete.dart.
  • Reduced timeout in manage_task_champion_creds_controller.dart.

✨ Feature Enhancements

  • Added annotations parsing in TaskForC.fromJson.
  • Improved mode selector visibility in the profile view.

🧪 Tests

  • Expanded tests for:
    • Annotations parsing
    • completeTask exception handling
    • Timeout regression

Files Changed

🐛 Bug Fixes

  • origin.dart
  • fetch.dart
  • complete.dart
  • manage_task_champion_creds_controller.dart

✨ Feature Enhancements

  • task.dart
  • profiles_list.dart
  • profile_view.dart

🧪 Tests

  • api_service_test.dart

Verification

  • Flutter Analyze: No new errors or warnings.
  • Tests: All new tests passed. Four pre-existing failures remain unchanged.

How to Test

1️⃣ Mode Selector

  1. Open the profile view.
  2. Verify the current mode is displayed as a subtitle in the "Change Mode" option.

2️⃣ Annotations Parsing

  1. Sync tasks with annotations.
  2. Verify annotations are displayed correctly.

3️⃣ Error Handling

  1. Trigger a task completion failure.
  2. Verify the app handles the error gracefully without crashing.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Added error handling when marking tasks complete—failures now display a user-friendly message instead of crashing the app
    • Fixed task annotation deserialization from server responses
    • Corrected HTTP request timeout from 10000s to 10s for faster operation
  • New Features

    • Added sync mode labels to profile display, showing which backend is active (TaskChampion, CCSync, or TaskServer)
  • Chores

    • Updated plugin registrations for Linux, macOS, and Windows platforms

@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

📝 Walkthrough

Walkthrough

The PR addresses five key issues in TaskServer 3 integration: replaces a hardcoded origin URL with dynamic retrieval from credentials storage, removes unsafe ScaffoldMessenger calls from network error handling, reduces HTTP timeout from 10000 to 10 seconds, adds annotation parsing to task deserialization, displays profile mode labels in the UI, and registers GTK and AppLinks plugins on multiple platforms.

Changes

Cohort / File(s) Summary
Network Error Handling
lib/app/modules/home/views/show_tasks.dart, lib/app/v3/net/complete.dart, lib/app/v3/db/update.dart
Wraps task completion calls in try/catch blocks; replaces UI-level ScaffoldMessenger error handling with exception throwing and logging; allows callers to handle errors appropriately.
Origin URL Management
lib/app/v3/net/origin.dart, lib/app/v3/net/fetch.dart, lib/app/modules/manage_task_champion_creds/controllers/manage_task_champion_creds_controller.dart
Replaces hardcoded localhost origin with dynamic getOrigin() function that retrieves API URL from credentials storage; updates URL construction to use baseUrl parameter; reduces HTTP timeout from 10000 to 10 seconds.
Task Model Updates
lib/app/v3/models/task.dart
Adds proper deserialization of annotations field in TaskForC.fromJson, mapping JSON array elements to Annotation objects instead of defaulting to empty list.
Profile UI Improvements
lib/app/modules/profile/views/profiles_list.dart, lib/app/modules/profile/views/profile_view.dart
Introduces getModeLabel callback to ProfilesList to display human-friendly mode labels (e.g., 'Taskchampion (v3)', 'CCSync (v3)', 'TaskServer') as subtitles in the Change Mode option.
Platform Plugin Registration
linux/flutter/generated_plugin_registrant.cc, linux/flutter/generated_plugins.cmake, macos/Flutter/GeneratedPluginRegistrant.swift, windows/flutter/generated_plugin_registrant.cc, windows/flutter/generated_plugins.cmake
Registers GTK plugin on Linux and AppLinks plugin on macOS and Windows platforms via generated plugin files.
Test Coverage
test/api_service_test.dart
Adds test groups for annotation parsing, completeTask exception handling, and timeout configuration validation; removes origin.dart dependency from test expectations.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Hops with glee through code so clean,
Dynamic URLs now convene,
Errors caught with graceful care,
Annotations parsed everywhere!
Modes displayed for all to see,
TaskServer's bugs are history! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Changes to plugin registration files (GTK on Linux, app_links on macOS/Windows) appear unrelated to the core TaskServer 3 integration objectives in issue #617 and may represent scope creep. Clarify whether plugin registration changes are intentional dependencies or should be separated into a distinct PR focused on plugin configuration updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing bugs and improving CCSync v3 integration in the TaskServer context.
Description check ✅ Passed The description comprehensively covers the changes, includes a link to the fixed issue (#617), lists files changed, verification steps, and testing instructions, though it lacks a formal checklist completion.
Linked Issues check ✅ Passed The PR successfully addresses all five objectives from issue #617: dynamic origin URL retrieval, ScaffoldMessenger crash removal, timeout reduction, annotation parsing, and mode selector visibility improvement with corresponding test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/app/modules/home/views/show_tasks.dart (1)

246-251: ⚠️ Potential issue | 🟡 Minor

Inconsistent error handling: deleteTask lacks await and try-catch.

Similar to _markTaskAsCompleted, the _markTaskAsDeleted method should await deleteTask and handle potential errors to provide consistent user feedback and prevent silent failures.

🛡️ Proposed fix for consistency
   void _markTaskAsDeleted(String uuid) async {
     TaskDatabase taskDatabase = TaskDatabase();
     await taskDatabase.open();
     taskDatabase.markTaskAsDeleted(uuid);
-    deleteTask('email', uuid);
+    try {
+      await deleteTask('email', uuid);
+    } catch (e) {
+      debugPrint('Error deleting task on server: $e');
+      Get.snackbar(
+        'Sync Error',
+        'Failed to delete task on server',
+        snackPosition: SnackPosition.BOTTOM,
+        duration: const Duration(seconds: 3),
+      );
+    }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/app/modules/home/views/show_tasks.dart` around lines 246 - 251, The
_markTaskAsDeleted method currently calls deleteTask without awaiting it and
lacks error handling; update _markTaskAsDeleted to await deleteTask('email',
uuid) and wrap the async work in a try/catch (similar to _markTaskAsCompleted)
so you open TaskDatabase, await taskDatabase.markTaskAsDeleted(uuid) if it
returns a Future, then await deleteTask(...), and on error catch and surface
user feedback/logging via the same mechanism used elsewhere (e.g., show a
Snackbar or log the error) to avoid silent failures.
🧹 Nitpick comments (4)
lib/app/v3/net/fetch.dart (1)

26-30: Consider propagating errors instead of silently returning an empty list.

The current error handling returns an empty list on failure, which makes it difficult for callers to distinguish between "no tasks exist" and "server error occurred". This could lead to data loss scenarios where users think they have no tasks when the server is actually unreachable.

Consider throwing the exception and letting callers handle it, similar to how completeTask was refactored in this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/app/v3/net/fetch.dart` around lines 26 - 30, The current catch block in
fetchTasks (the one logging "Error fetching tasks: $e") swallows errors and
returns an empty list; change it to rethrow the caught exception (or throw a
wrapped exception) instead of returning [], so callers can distinguish server
errors from legitimately empty results—follow the same pattern used by
completeTask by logging the error then rethrowing the exception (preserve stack
trace using the caught stack variable).
lib/app/v3/models/task.dart (1)

47-47: Consider removing debug print in production code.

This debug statement logs on every task deserialization. If many tasks are fetched, this could create log noise. Consider removing it or using a more selective logging approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/app/v3/models/task.dart` at line 47, Remove the unconditional debug print
in the Task.fromJson deserialization (the debugPrint("Annotation fromJson: ...")
call) to avoid noisy logs in production; either delete the line or guard it
behind a debug-only check (e.g., use Flutter's kDebugMode) or switch to a
configurable logger so only debug builds emit this message while production
builds remain silent.
test/api_service_test.dart (2)

299-299: Variable mockClient shadows outer scope declaration.

Line 299 declares a new mockClient that shadows the MockClient mockClient at line 27. While this works due to Dart's scoping rules, it can cause confusion. Consider using a distinct name like localMockClient or renaming the outer variable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/api_service_test.dart` at line 299, The local declaration "final
mockClient = MockClient();" shadows the outer "MockClient mockClient" variable;
rename the inner variable (for example to "localMockClient" or "testMockClient")
or rename the outer variable so they are distinct across scope, and update all
uses in the test block to reference the new name (search for "mockClient" inside
the test around line 299 and replace with the chosen new identifier).

313-319: Timeout regression test doesn't verify actual code behavior.

This test only verifies that Duration(seconds: 10) equals 10 seconds, which is always true. It doesn't actually verify that the timeout value in manage_task_champion_creds_controller.dart is configured correctly.

Consider testing the actual behavior by mocking a slow server response, or at minimum, importing and referencing the actual timeout constant from the controller if one exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/api_service_test.dart` around lines 313 - 319, The test currently
asserts Duration(seconds: 10) instead of the app constant—update the test in
test/api_service_test.dart to reference the real timeout constant or behavior
from manage_task_champion_creds_controller.dart: import the exported timeout
constant (e.g., CREDENTIAL_CHECK_TIMEOUT or the named constant used in that
controller) and assert its value, or better mock a slow server (using your HTTP
client test utility or a fake server) and assert that the credential-check call
times out as expected (throws a timeout or returns an error) when the
controller's timeout is applied; locate references to the controller's
credential-check function or HTTP call (e.g., the method that performs the
credential check) and write the test against that symbol rather than a locally
created Duration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/app/v3/db/update.dart`:
- Around line 85-87: The call to deleteTask when localTask.status == 'deleted'
is not awaited or error-handled like completeTask, so wrap the
deleteTask('email', localTask.uuid!) call in an await and surround it with
try-catch (similar to the completeTask handling) to catch and log errors;
reference the deleteTask function call and localTask.status check and ensure you
use await deleteTask(...) and a try { await deleteTask(...) } catch (err) { /*
log error via existing logger/processLogger */ } pattern to keep behavior
consistent.

In `@lib/app/v3/net/complete.dart`:
- Around line 6-8: The function completeTask creates an http.Client when no
client is injected but never closes it, leaking sockets; change completeTask to
track whether the client was created (e.g., createdClient = client == null) and
ensure httpClient.close() is called in a finally block (or use a try/finally) so
only internally created clients are closed while injected clients are left open;
reference the httpClient local and completeTask function to locate where to add
the finally/close logic.

In `@lib/app/v3/net/origin.dart`:
- Around line 1-3: Remove the dead helper getOrigin() and its containing file:
delete the getOrigin function and the file defining it (the function name
getOrigin and its single use of CredentialsStorage.getApiUrl() are unused), and
ensure no other code imports or references getOrigin; replace any remaining
calls (if any) with direct calls to CredentialsStorage.getApiUrl() or remove the
import entirely so the file can be safely deleted.

---

Outside diff comments:
In `@lib/app/modules/home/views/show_tasks.dart`:
- Around line 246-251: The _markTaskAsDeleted method currently calls deleteTask
without awaiting it and lacks error handling; update _markTaskAsDeleted to await
deleteTask('email', uuid) and wrap the async work in a try/catch (similar to
_markTaskAsCompleted) so you open TaskDatabase, await
taskDatabase.markTaskAsDeleted(uuid) if it returns a Future, then await
deleteTask(...), and on error catch and surface user feedback/logging via the
same mechanism used elsewhere (e.g., show a Snackbar or log the error) to avoid
silent failures.

---

Nitpick comments:
In `@lib/app/v3/models/task.dart`:
- Line 47: Remove the unconditional debug print in the Task.fromJson
deserialization (the debugPrint("Annotation fromJson: ...") call) to avoid noisy
logs in production; either delete the line or guard it behind a debug-only check
(e.g., use Flutter's kDebugMode) or switch to a configurable logger so only
debug builds emit this message while production builds remain silent.

In `@lib/app/v3/net/fetch.dart`:
- Around line 26-30: The current catch block in fetchTasks (the one logging
"Error fetching tasks: $e") swallows errors and returns an empty list; change it
to rethrow the caught exception (or throw a wrapped exception) instead of
returning [], so callers can distinguish server errors from legitimately empty
results—follow the same pattern used by completeTask by logging the error then
rethrowing the exception (preserve stack trace using the caught stack variable).

In `@test/api_service_test.dart`:
- Line 299: The local declaration "final mockClient = MockClient();" shadows the
outer "MockClient mockClient" variable; rename the inner variable (for example
to "localMockClient" or "testMockClient") or rename the outer variable so they
are distinct across scope, and update all uses in the test block to reference
the new name (search for "mockClient" inside the test around line 299 and
replace with the chosen new identifier).
- Around line 313-319: The test currently asserts Duration(seconds: 10) instead
of the app constant—update the test in test/api_service_test.dart to reference
the real timeout constant or behavior from
manage_task_champion_creds_controller.dart: import the exported timeout constant
(e.g., CREDENTIAL_CHECK_TIMEOUT or the named constant used in that controller)
and assert its value, or better mock a slow server (using your HTTP client test
utility or a fake server) and assert that the credential-check call times out as
expected (throws a timeout or returns an error) when the controller's timeout is
applied; locate references to the controller's credential-check function or HTTP
call (e.g., the method that performs the credential check) and write the test
against that symbol rather than a locally created Duration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 814a2e35-5889-45ea-80c3-088af564807f

📥 Commits

Reviewing files that changed from the base of the PR and between 66aef29 and d1a9e0c.

📒 Files selected for processing (15)
  • lib/app/modules/home/views/show_tasks.dart
  • lib/app/modules/manage_task_champion_creds/controllers/manage_task_champion_creds_controller.dart
  • lib/app/modules/profile/views/profile_view.dart
  • lib/app/modules/profile/views/profiles_list.dart
  • lib/app/v3/db/update.dart
  • lib/app/v3/models/task.dart
  • lib/app/v3/net/complete.dart
  • lib/app/v3/net/fetch.dart
  • lib/app/v3/net/origin.dart
  • linux/flutter/generated_plugin_registrant.cc
  • linux/flutter/generated_plugins.cmake
  • macos/Flutter/GeneratedPluginRegistrant.swift
  • test/api_service_test.dart
  • windows/flutter/generated_plugin_registrant.cc
  • windows/flutter/generated_plugins.cmake

Comment on lines 85 to 87
} else if (localTask.status == 'deleted') {
deleteTask('email', localTask.uuid!);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent error handling: deleteTask is not awaited or wrapped in try-catch.

completeTask is now properly awaited with error handling, but deleteTask on line 86 is called without await and has no try-catch. This inconsistency could lead to unhandled errors if deleteTask throws, potentially causing silent failures or unhandled promise rejections.

🛡️ Proposed fix for consistency
         if (localTask.status == 'completed') {
           try {
             await completeTask('email', localTask.uuid!);
           } catch (e) {
             debugPrint('Failed to complete task on server: $e');
           }
         } else if (localTask.status == 'deleted') {
-          deleteTask('email', localTask.uuid!);
+          try {
+            await deleteTask('email', localTask.uuid!);
+          } catch (e) {
+            debugPrint('Failed to delete task on server: $e');
+          }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (localTask.status == 'deleted') {
deleteTask('email', localTask.uuid!);
}
} else if (localTask.status == 'deleted') {
try {
await deleteTask('email', localTask.uuid!);
} catch (e) {
debugPrint('Failed to delete task on server: $e');
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/app/v3/db/update.dart` around lines 85 - 87, The call to deleteTask when
localTask.status == 'deleted' is not awaited or error-handled like completeTask,
so wrap the deleteTask('email', localTask.uuid!) call in an await and surround
it with try-catch (similar to the completeTask handling) to catch and log
errors; reference the deleteTask function call and localTask.status check and
ensure you use await deleteTask(...) and a try { await deleteTask(...) } catch
(err) { /* log error via existing logger/processLogger */ } pattern to keep
behavior consistent.

Comment on lines +6 to +8
Future<void> completeTask(String email, String taskUuid,
{http.Client? client}) async {
final httpClient = client ?? http.Client();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Resource leak: internally created http.Client is never closed.

When no client is injected, a new http.Client() is created but never closed. The http.Client should be closed after use to release underlying resources (socket connections).

🛡️ Proposed fix
 Future<void> completeTask(String email, String taskUuid,
     {http.Client? client}) async {
-  final httpClient = client ?? http.Client();
+  final httpClient = client;
+  final shouldCloseClient = httpClient == null;
+  final effectiveClient = httpClient ?? http.Client();
   var c = await CredentialsStorage.getClientId();
   var e = await CredentialsStorage.getEncryptionSecret();
   var baseUrl = await CredentialsStorage.getApiUrl();
   // ... rest of the code ...
   try {
-    final response = await httpClient.post(
+    final response = await effectiveClient.post(
       // ...
     );
     // ...
   } catch (e, s) {
     debugPrint('Error completing task: $e\n$s');
     rethrow;
+  } finally {
+    if (shouldCloseClient) {
+      effectiveClient.close();
+    }
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Future<void> completeTask(String email, String taskUuid,
{http.Client? client}) async {
final httpClient = client ?? http.Client();
Future<void> completeTask(String email, String taskUuid,
{http.Client? client}) async {
final httpClient = client;
final shouldCloseClient = httpClient == null;
final effectiveClient = httpClient ?? http.Client();
var c = await CredentialsStorage.getClientId();
var e = await CredentialsStorage.getEncryptionSecret();
var baseUrl = await CredentialsStorage.getApiUrl();
// ... rest of the code ...
try {
final response = await effectiveClient.post(
// ...
);
// ...
} catch (e, s) {
debugPrint('Error completing task: $e\n$s');
rethrow;
} finally {
if (shouldCloseClient) {
effectiveClient.close();
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/app/v3/net/complete.dart` around lines 6 - 8, The function completeTask
creates an http.Client when no client is injected but never closes it, leaking
sockets; change completeTask to track whether the client was created (e.g.,
createdClient = client == null) and ensure httpClient.close() is called in a
finally block (or use a try/finally) so only internally created clients are
closed while injected clients are left open; reference the httpClient local and
completeTask function to locate where to add the finally/close logic.

Comment on lines +1 to +3
import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart';

Future<String> getOrigin() async => await CredentialsStorage.getApiUrl() ?? '';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for usages of getOrigin() across the codebase

# Search for imports of origin.dart
echo "=== Imports of origin.dart ==="
rg -l "import.*origin\.dart" --type dart

# Search for getOrigin function calls
echo -e "\n=== Usages of getOrigin() ==="
rg -n "getOrigin\(" --type dart

Repository: CCExtractor/taskwarrior-flutter

Length of output: 245


Remove unused getOrigin() function and file.

The getOrigin() function is not imported or called anywhere in the codebase. It is dead code left over from refactoring where direct calls to CredentialsStorage.getApiUrl() replaced its usage. Delete lib/app/v3/net/origin.dart entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/app/v3/net/origin.dart` around lines 1 - 3, Remove the dead helper
getOrigin() and its containing file: delete the getOrigin function and the file
defining it (the function name getOrigin and its single use of
CredentialsStorage.getApiUrl() are unused), and ensure no other code imports or
references getOrigin; replace any remaining calls (if any) with direct calls to
CredentialsStorage.getApiUrl() or remove the import entirely so the file can be
safely deleted.

@SGI-CAPP-AT2
Copy link
Collaborator

@inderjeet20 , We're no longer using CCSync APIs to sync.
Instead we use TC.

Closing this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TaskServer 3 (CCSync v3) Bugs and Improvements

2 participants