Bound each provider's startFetch()/resolveFetch() to an optional
since/until window, wire a SQLite-backed ContributionStore into
ContributionAggregator (fetch only the trailing window past the last
stored date, merge into the store, prune by CONTRIBUTIONS_RETENTION_DAYS),
and add a graph:contributions:refetch command to force a full or ranged
re-fetch in <=365-day chunks. This is the root fix for the full
365-day-refetch timeout that used to hit on every cache miss.
- Fix add()'s upsert SQL (was invalid SQL from typos) and use it for
the new merge() as well.
- Fix remove()'s missing FROM keyword and drop the unindexed LIKE scan
in favor of an exact match.
- Add latestDate(), all() with optional sinceDays filtering, and
prune() with a retention-day cutoff.
- Add Contribution (immutable value object) and ContributionCollection
(IteratorAggregate + Countable) so all() returns something typed
instead of a raw date => count array.
- Cover all of the above with unit tests against an in-memory SQLite
database.
The eightpoints/guzzle-bundle and idci/graphql-client-bundle only
existed to build one near-static query string, and the bundle's own
HTTP transport was already bypassed in favor of Symfony HttpClient.
Build the query with sprintf/json_encode instead, drop both bundles
from config/bundles.php, and update the test double accordingly.
Promoted readonly properties can't be reassigned in the constructor
body, so GitLabProvider and GiteaProvider threw on every instantiation.
Normalize the plain parameter first, then assign to a separately
declared readonly property.
Providers fetch in parallel now (cost ~= max, not sum), so the old
90s stopgap for the 3-sequential-providers-plus-pagination worst case
can come back down toward the 30s default, with headroom for
pagination and per-request HTTP timeouts.
aggregate() now fires every configured provider's startFetch() in one
pass before resolving any of them, so wall-clock cost is roughly
max(providers) instead of sum(providers). Partial-failure isolation
and per-date summation behavior are unchanged.
startFetch() resolves the user id and fires the first events page,
returning a handle without reading it; resolveFetch() continues the
existing do/while pagination starting from that first response.
Pagination itself stays sequential within this provider since each
page depends on the previous one - parallelism here only spans across
providers, not within GitLab's own pages.
startFetch() fires the GraphQL POST and returns the response without
reading it; resolveFetch() does the .toArray()/parse/log work that
used to happen inline right after the request.
Splits the provider contract into a non-blocking startFetch() that
fires the HTTP request(s) and a resolveFetch() that reads the result,
so callers can fire every provider's request before blocking on any
of them. Symfony HttpClient is already async under the hood -
request() doesn't block until you read the response - so this needs
no new dependency.
Move all provider-related classes, enums, interface and trait into
App\Service\Provider; move SvgRenderer into App\Service\Renderer.
ContributionAggregator stays at the Service root as the orchestrator.
Test namespaces and use statements updated to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduce ProviderHealthChecker which probes each configured provider
via AutowireIterator. Wire it into GraphController so /health returns
detailed per-provider status and responds 503 when any provider is
in a degraded state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add getName() and ping() to each provider and wire ProbeTrait. Make
all classes final, add strict_types declarations, and replace generic
RuntimeException with typed HTTP exceptions so probe() can classify
auth failures and unreachable endpoints correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add getName(), ping(), and probe() to ProviderInterface. Introduce
ProbeTrait to classify HTTP/transport exceptions into typed error codes,
ProviderStatus as the result value object, and ProviderStatusType /
ProviderErrorCode as backing enums.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move fetchAllContributions and merge logic from GraphController into a new
ContributionAggregator service. Replace deprecated TaggedIterator with
AutowireIterator throughout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- modify('Sunday') → modify('last Sunday') so the grid start is
reliably the prior Sunday when today is already Sunday
- Remove unused $totalW from renderLegend signature
- Remove unused GAP constant (value preserved inline as a comment on STEP)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use GraphQLApiClientRegistryInterface to build the query via the
bundle's query builder. Keep Symfony HttpClient for the actual POST
since GitHub requires application/json (the bundle's transport sends
form_params instead).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>