Skip to content
8 changes: 3 additions & 5 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,8 @@ export const Header: React.FC = () => {

setRepositories(mergedRepositories);

// 3. 获取Release信息
console.log('Fetching releases...');
const releases = await githubApi.getMultipleRepositoryReleases(mergedRepositories.slice(0, 20));
setReleases(releases);
// Note: Release fetching is now handled by the Refresh button in Release Timeline
// Header sync only syncs the starred repos list

setLastSync(new Date().toISOString());
console.log('Sync completed successfully');
Expand Down Expand Up @@ -367,7 +365,7 @@ export const Header: React.FC = () => {
onClick={handleSync}
disabled={isLoading}
className="p-1 rounded hover:bg-light-surface dark:hover:bg-white/5 transition-colors disabled:opacity-50"
title={t('同步仓库', 'Sync repositories')}
title={t('同步星标仓库列表(不包含Release)', 'Sync starred repos list (excludes Release)')}
>
<RefreshCw className={`w-4 h-4 ${isLoading ? 'animate-spin' : ''}`} />
</button>
Expand Down
101 changes: 66 additions & 35 deletions src/components/ReleaseTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const ReleaseTimeline: React.FC = () => {
setReleaseSearchQuery,
toggleReleaseExpandedRepository,
setReleaseIsRefreshing,
includePreRelease,
setIncludePreRelease,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} = useAppStore();

const { toast, confirm } = useDialog();
Expand Down Expand Up @@ -301,56 +303,54 @@ export const ReleaseTimeline: React.FC = () => {
setReleaseIsRefreshing(true);
try {
const githubApi = new GitHubApiService(githubToken);
// Only fetch releases for repos that are subscribed to releases
const subscribedRepos = repositories.filter(repo => releaseSubscriptions.has(repo.id));

if (subscribedRepos.length === 0) {
toast(language === 'zh' ? '没有订阅的仓库。' : 'No subscribed repositories.', 'error');
return;
}

const allNewReleases: Release[] = [];

const latestReleaseTime = releases.length > 0
? releases.reduce((max, r) => Math.max(max, new Date(r.published_at).getTime()), 0)
: 0;
const sinceTimestamp = latestReleaseTime > 0 ? new Date(latestReleaseTime).toISOString() : undefined;
// Use the new getMultipleRepositoryReleases with options
const { releases: newReleases, failedRepos } = await githubApi.getMultipleRepositoryReleases(
subscribedRepos,
{ includePreRelease }
);

// Update repository sync metadata
const now = new Date().toISOString();
for (const repo of subscribedRepos) {
const [owner, name] = repo.full_name.split('/');

const hasExistingReleases = releases.some(r => r.repository.id === repo.id);

let repoReleases: Release[];
if (!hasExistingReleases) {
repoReleases = await githubApi.getRepositoryReleases(owner, name, 1, 10);
} else {
repoReleases = await githubApi.getIncrementalRepositoryReleases(owner, name, sinceTimestamp, 10);
}

repoReleases.forEach(release => {
release.repository.id = repo.id;
updateRepository({
...repo,
has_fetched_releases: true,
last_release_fetch_time: now,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not advance sync markers for failed repositories.

failedRepos is surfaced in the toast, but Lines 320-327 still stamp has_fetched_releases and last_release_fetch_time onto every subscribed repo. Any repo that failed here will be treated as incrementally synced on the next refresh, which can hide missed releases.

Suggested fix
       // Update repository sync metadata
       const now = new Date().toISOString();
+      const failedRepoIds = new Set(failedRepos.map(repo => repo.repoId));
       for (const repo of subscribedRepos) {
+        if (failedRepoIds.has(repo.id)) {
+          continue;
+        }
         updateRepository({
           ...repo,
           has_fetched_releases: true,
           last_release_fetch_time: now,
         });

Also applies to: 341-353

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

In `@src/components/ReleaseTimeline.tsx` around lines 320 - 327, The code
currently updates every repo in subscribedRepos via updateRepository(...) which
advances has_fetched_releases/last_release_fetch_time even for failed repos;
change this to only update repositories that succeeded by filtering
subscribedRepos against failedRepos (e.g., by repo id or full_name) and call
updateRepository only for those not in failedRepos; apply the same filtering fix
to the other update block referenced (the later block around where similar
metadata is set).


allNewReleases.push(...repoReleases);

await new Promise(resolve => setTimeout(resolve, 200));
}

// Filter out existing releases and add new ones
const existingIds = new Set(useAppStore.getState().releases.map(r => r.id));
const actuallyNewCount = allNewReleases.filter(r => !existingIds.has(r.id)).length;
const actuallyNewReleases = newReleases.filter(r => !existingIds.has(r.id));
const actuallyNewCount = actuallyNewReleases.length;

if (allNewReleases.length > 0) {
addReleases(allNewReleases);
if (actuallyNewReleases.length > 0) {
addReleases(actuallyNewReleases);
}

const now = new Date().toISOString();
setLastRefreshTime(now);

const message = language === 'zh'
? `刷新完成!发现 ${actuallyNewCount} 个新Release。`
: `Refresh completed! Found ${actuallyNewCount} new releases.`;
// Build success message with failed repos info
let message: string;
if (failedRepos.length > 0) {
message = language === 'zh'
? `刷新完成!发现 ${actuallyNewCount} 个新Release,${failedRepos.length} 个仓库刷新失败。`
: `Refresh completed! Found ${actuallyNewCount} new releases, ${failedRepos.length} repos failed.`;
} else {
message = language === 'zh'
? `刷新完成!发现 ${actuallyNewCount} 个新Release。`
: `Refresh completed! Found ${actuallyNewCount} new releases.`;
}

toast(message, 'success');
toast(message, actuallyNewCount > 0 ? 'success' : 'info');
} catch (error) {
console.error('Refresh failed:', error);
const errorMessage = language === 'zh'
Expand Down Expand Up @@ -527,19 +527,35 @@ export const ReleaseTimeline: React.FC = () => {
}
</p>

{/* 刷新按钮 - 在有订阅仓库时显示 */}
{/* Pre-release toggle + Refresh button */}
{subscribedRepoCount > 0 && (
<div className="mb-6">
<div className="mb-6 flex flex-col items-center gap-3">
{/* Pre-release toggle */}
<label className="flex items-center gap-2 cursor-pointer select-none">
<div
onClick={() => setIncludePreRelease(!includePreRelease)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${includePreRelease ? 'bg-brand-indigo' : 'bg-gray-300 dark:bg-gray-600'}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${includePreRelease ? 'translate-x-[18px]' : 'translate-x-[2px]'}`}
/>
</div>
<span className="text-sm text-gray-600 dark:text-text-secondary">
{t('包含 Pre-release', 'Include Pre-release')}
</span>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</label>

{/* Refresh button */}
<button
onClick={handleRefresh}
disabled={releaseIsRefreshing}
className="flex items-center space-x-2 px-6 py-3 bg-brand-indigo text-white rounded-lg hover:bg-brand-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed mx-auto"
className="flex items-center space-x-2 px-6 py-3 bg-brand-indigo text-white rounded-lg hover:bg-brand-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw className={`w-5 h-5 ${releaseIsRefreshing ? 'animate-spin' : ''}`} />
<span>{releaseIsRefreshing ? t('刷新中...', 'Refreshing...') : t('刷新Release', 'Refresh Releases')}</span>
</button>
{lastRefreshTime && (
<p className="text-sm text-gray-500 dark:text-text-tertiary mt-2">
<p className="text-sm text-gray-500 dark:text-text-tertiary">
{t('上次刷新:', 'Last refresh:')} {formatDistanceToNow(new Date(lastRefreshTime), { addSuffix: true })}
</p>
)}
Expand Down Expand Up @@ -598,6 +614,21 @@ export const ReleaseTimeline: React.FC = () => {
</span>
)}

{/* Pre-release toggle */}
<label className="flex items-center gap-1.5 cursor-pointer select-none">
<div
onClick={() => setIncludePreRelease(!includePreRelease)}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${includePreRelease ? 'bg-brand-indigo' : 'bg-gray-300 dark:bg-gray-600'}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${includePreRelease ? 'translate-x-[18px]' : 'translate-x-[2px]'}`}
/>
</div>
<span className="text-xs text-gray-600 dark:text-text-secondary hidden sm:inline">
{t('Pre', 'Pre')}
</span>
</label>

{/* Refresh Button */}
<button
onClick={handleRefresh}
Expand Down
Loading
Loading