feat: refresh a single repository from its own button (#2)

* feat(refresh): refresh a single repository with POST /refresh?repo=

The endpoint refreshes one configured repository and returns the full state plus the repo name.
Each repository has a 60-second cooldown that is separate from the full-refresh limit.
A running full refresh or a refresh of the same repository returns 409.
An unknown repository returns 404. Cross-site and disabled-config behaviour stay the same.
The collector reuses its fetch path and replaces only that repository.

* feat(ui): add a refresh button beside each repository

The button appears in the project list and in each attention group header.
It stays hidden when the server disables manual refresh.
The button shows a spinner while its repository refreshes and reports refusals in the notice line.
The store swaps in the returned state, so no extra fetch follows.
This commit is contained in:
2026-10-02 23:16:22 +01:00
committed by GitHub
parent 4c2c9d3341
commit 8cbc495dc7
16 changed files with 760 additions and 19 deletions
+19 -1
View File
@@ -1,4 +1,4 @@
import type { TrackerState } from './types'
import type { RepoRefreshResult, TrackerState } from './types'
export async function fetchState(signal?: AbortSignal): Promise<TrackerState> {
const res = await fetch('/api/state', { signal, headers: { Accept: 'application/json' } })
@@ -14,3 +14,21 @@ export async function requestRefresh(): Promise<void> {
if (res.status === 429) throw new RescanLimitedError('Rescan is limited to once every 5 minutes')
if (!res.ok) throw new Error(`rescan request failed: HTTP ${res.status}`)
}
/** Thrown when one repository was refreshed too recently or a scan is running. */
export class RepoRefreshRefusedError extends Error {}
const REPO_COOLDOWN_FALLBACK_SECONDS = 60
/** Refreshes one repository and returns the full state after it, plus the repo name. */
export async function requestRepoRefresh(fullName: string): Promise<RepoRefreshResult> {
const res = await fetch(`/refresh?repo=${encodeURIComponent(fullName)}`, { method: 'POST' })
if (res.status === 429) {
const wait = Number(res.headers.get('Retry-After')) || REPO_COOLDOWN_FALLBACK_SECONDS
throw new RepoRefreshRefusedError(`${fullName} was refreshed recently. Try again in ${wait}s`)
}
if (res.status === 409)
throw new RepoRefreshRefusedError('A refresh is already running. Try again shortly')
if (!res.ok) throw new Error(`refresh of ${fullName} failed: HTTP ${res.status}`)
return (await res.json()) as RepoRefreshResult
}