Skip to content

feat: add manifest refresh with 2h TTL caching#4476

Open
kodjima33 wants to merge 3 commits intomainfrom
feat/manifest-refresh-caching
Open

feat: add manifest refresh with 2h TTL caching#4476
kodjima33 wants to merge 3 commits intomainfrom
feat/manifest-refresh-caching

Conversation

@kodjima33
Copy link
Collaborator

Summary

Implements chat tools manifest refresh functionality with 2-hour TTL caching and a manual refresh button in the app management screen.

Changes

Backend

  • Caching: Modified fetch_app_chat_tools_from_manifest() to cache manifests in Redis with 2-hour TTL
  • Force Refresh: Added force_refresh parameter to bypass cache when needed
  • New API Endpoint: Created POST /v1/apps/{app_id}/refresh-manifest for manual refresh

Frontend (Flutter)

  • Refresh Button: Added refresh icon button in UpdateAppPage AppBar
  • Provider Method: Added refreshManifest() method to AddAppProvider
  • Loading State: Added isRefreshingManifest state with loading indicator
  • Conditional Display: Button only shows when app has external integration with manifest URL

Manual Refresh

  • Successfully refreshed GitHub app manifest (7 chat tools including AI coding feature)

Benefits

  • Efficiency: 2-hour caching reduces external API calls
  • User Control: App owners can manually refresh when they deploy changes
  • Better UX: Loading states and success/error feedback
  • Production Ready: Refreshed production apps to ensure latest tooling

Test Plan

  • Backend caching works with 2h TTL
  • Manual refresh API endpoint works
  • Refresh button appears in UI when appropriate
  • Loading indicator shows during refresh
  • Success/error messages display correctly
  • GitHub app manifest refreshed in production

🤖 Generated with Claude Code

kodjima33 and others added 3 commits January 30, 2026 14:36
- Add 2-hour TTL caching for chat tools manifest fetching
- Add manual refresh button in manage app screen
- Add /v1/apps/{app_id}/refresh-manifest API endpoint
- Manually refreshed GitHub app manifest (7 tools)

Backend changes:
- Modified fetch_app_chat_tools_from_manifest() to use Redis cache with 2h TTL
- Added force_refresh parameter to bypass cache
- Created new POST endpoint for manual manifest refresh

Frontend changes:
- Added refresh icon button in UpdateAppPage AppBar
- Added refreshManifest() method to AddAppProvider
- Added isRefreshingManifest state with loading indicator
- Button only shows when app has external integration with manifest URL

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces manifest refresh with caching, which is a great improvement for efficiency. However, it also includes significant, unrelated changes to the Action Items page for a new "Goals" feature and refactors the bottom navigation bar. Bundling multiple features into a single pull request makes it harder to review and increases the risk of introducing bugs. In the future, please try to keep pull requests focused on a single, cohesive change. This aligns with the repository's general rule about separating large-scale changes into dedicated pull requests.

Comment on lines +106 to +142
Future<void> _loadGoals() async {
try {
final goals = await getAllGoals();
if (!mounted) return;
if (goals.isNotEmpty) {
setState(() {
_goals = goals;
_isLoadingGoals = false;
});
_pruneTaskGoalLinks();
return;
}
} catch (_) {}

// Fallback to locally cached goals from the goals widget
try {
final prefs = await SharedPreferences.getInstance();
final goalsJson = prefs.getString(_goalsStorageKey);
if (goalsJson != null) {
final List<dynamic> decoded = jsonDecode(goalsJson);
final localGoals = decoded.map((e) => Goal.fromJson(e)).toList();
if (mounted) {
setState(() {
_goals = localGoals;
_isLoadingGoals = false;
});
_pruneTaskGoalLinks();
return;
}
}
} catch (_) {}

if (!mounted) return;
setState(() {
_isLoadingGoals = false;
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The catch blocks in this function are empty, which silently swallows any exceptions that occur when fetching or parsing goals. This can make debugging difficult and lead to unexpected behavior if an operation fails. At a minimum, errors should be logged.

  Future<void> _loadGoals() async {
    try {
      final goals = await getAllGoals();
      if (!mounted) return;
      if (goals.isNotEmpty) {
        setState(() {
          _goals = goals;
          _isLoadingGoals = false;
        });
        _pruneTaskGoalLinks();
        return;
      }
    } catch (e, stackTrace) {
      Logger.debug('Failed to load goals from API: $e, $stackTrace');
    }

    // Fallback to locally cached goals from the goals widget
    try {
      final prefs = await SharedPreferences.getInstance();
      final goalsJson = prefs.getString(_goalsStorageKey);
      if (goalsJson != null) {
        final List<dynamic> decoded = jsonDecode(goalsJson);
        final localGoals = decoded.map((e) => Goal.fromJson(e)).toList();
        if (mounted) {
          setState(() {
            _goals = localGoals;
            _isLoadingGoals = false;
          });
          _pruneTaskGoalLinks();
          return;
        }
      }
    } catch (e, stackTrace) {
      Logger.debug('Failed to load goals from SharedPreferences: $e, $stackTrace');
    }

    if (!mounted) return;
    setState(() {
      _isLoadingGoals = false;
    });
  }

Comment on lines +585 to +605
Future<bool> refreshManifest() async {
if (updateAppId == null) {
AppSnackbar.showSnackbarError('App ID not found');
return false;
}

setIsRefreshingManifest(true);
var success = await refreshAppManifestServer(updateAppId!);
if (success) {
// Reload app details to get updated chat tools
var app = await getAppDetailsServer(updateAppId!);
if (app != null) {
appProvider!.updateLocalApp(App.fromJson(app));
AppSnackbar.showSnackbarSuccess('Manifest refreshed successfully');
}
} else {
AppSnackbar.showSnackbarError('Failed to refresh manifest');
}
setIsRefreshingManifest(false);
return success;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The loading state isRefreshingManifest is not guaranteed to be reset to false if an error occurs after it's set to true (e.g., if App.fromJson or appProvider!.updateLocalApp throws an exception). This could leave the UI in a permanent loading state. Using a try...finally block will ensure the loading state is always reset.

  Future<bool> refreshManifest() async {
    if (updateAppId == null) {
      AppSnackbar.showSnackbarError('App ID not found');
      return false;
    }

    setIsRefreshingManifest(true);
    var success = false;
    try {
      success = await refreshAppManifestServer(updateAppId!);
      if (success) {
        // Reload app details to get updated chat tools
        var app = await getAppDetailsServer(updateAppId!);
        if (app != null) {
          appProvider?.updateLocalApp(App.fromJson(app));
          AppSnackbar.showSnackbarSuccess('Manifest refreshed successfully');
        }
      } else {
        AppSnackbar.showSnackbarError('Failed to refresh manifest');
      }
    } catch (e, stackTrace) {
      Logger.debug('Failed to refresh manifest: $e, $stackTrace');
      AppSnackbar.showSnackbarError('Failed to refresh manifest');
      success = false;
    } finally {
      setIsRefreshingManifest(false);
    }
    return success;
  }

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.

1 participant