← projects
$ Contribution Radar
Turns a GitHub username or a programming language into a curated list of active, contributable open source issues.
- →Built with a hexagonal Spring Boot backend and a Vite/React frontend. Given a GitHub username, it analyzes the account's most used languages via the GitHub GraphQL API and surfaces open issues weighted toward those languages; alternatively, a language can be searched directly without any username.
- →Fixed a race condition in per search statistics tracking by replacing a read then write JPA flow with a single atomic PostgreSQL INSERT ... ON CONFLICT DO UPDATE upsert, eliminating duplicate key failures under concurrent requests.
- →Discovered that GitHub's own stars: search qualifier silently over filtered results server side; replaced it with client side post filtering against the real stargazerCount, combined with an activity window filter so only issues updated in the last 90 days or with meaningful discussion are suggested.
- →Resolved a hidden N+1 style delete pattern in a scheduled cache cleanup job deleting entities one by one triggered a separate DELETE per entity plus its @ElementCollection child table. Replaced it with two bulk native SQL DELETE statements, cutting the job to a fixed number of queries regardless of batch size.
- →Implemented IP based abuse protection as a single Servlet Filter chain instead of per controller checks: a ClientIpResolver that scans X-Forwarded-For right to left to resist IP spoofing behind the reverse proxy, automatic temporary bans after repeated invalid requests, and separate Caffeine backed rate limits per endpoint.
- →Deployed with Docker Compose (PostgreSQL, Spring Boot, Vite served via nginx) joined to an existing shared nginx proxy + Let's Encrypt network, with an nginx SPA fallback so client side routes resolve correctly on direct access and for search engine crawlers.
- →Hardened the frontend's nginx layer against exploit scanning bots with a categorized blocklist (403 on credential files, CMS admin paths, backup/dump extensions, editor artifacts) and per IP leaky bucket rate limiting (limit_req, tolerating real traffic bursts while rejecting scripted rapid fire requests), validating both end to end in an isolated, disposable Docker environment before touching the real reverse proxy a process that also surfaced a real config bug, an error_page rule pointing at a nonexistent file that would have shown users a misleading 404 instead of a proper 503 when the rate limit triggered.
- →Prototyped a fail2ban based IP banning layer on top of the rate limiter, since nginx's limiter has no memory across requests; verified real ban and auto expiry behavior against fake log data inside an isolated container network, de risking the approach with zero exposure to the production server before deciding whether to deploy it.
- →Diagnosed, from real production log analysis, that the frontend sits behind a two-hop chain (Cloudflare, then a shared reverse proxy) that left the rate limiter blind to real visitor IPs, silently throttling all traffic as a single shared address; fixed it by trusting both hops (Cloudflare's official IP ranges plus the internal Docker subnet) and recursively resolving the X-Forwarded-For chain, verified against live production traffic.
- →Closed a GitHub search-qualifier injection surface (e.g. is:closed, -label:...) in the free text keyword filter by restricting it to a safe character whitelist at the domain layer, covered by 20 unit tests; confirmed the username input was already immune since it's validated against GitHub's own username format and passed as a GraphQL variable rather than concatenated into the query document.
- →Implemented author-association badges end to end (GraphQL to cache to UI), surfacing whether an issue was opened by a project maintainer/owner or a past contributor, verified via live schema introspection and real production data across every observed association type.
- →Set up an automated nightly database backup pipeline (pg_dump, gzip, checksum verification) uploaded to Cloudflare R2 with retention cleanup and an HTML email report from a domain-authenticated sender, and separately fixed the shared reverse proxy's unbounded, multi-hundred-megabyte log file with host-level log rotation, without touching the running container.
- →Added a non-personalized RSS feed for the language search by reusing the existing per-search cache table under a reserved global sentinel key rather than building new infrastructure, keeping GitHub API cost bounded regardless of visitor volume; RSS 2.0 XML is hand-rendered without an external library, with manual entity escaping and RFC-822 date formatting.
- →Added a second, non-personalized discovery mode, Popular Projects, surfacing recent issues from only well-known, high-star repositories (5k/10k presets) with a cache shared across all visitors; when the same stars: reliability issue resurfaced for this feature's own repo-scoped search, fixed it architecturally instead of just re-filtering, by first discovering a pool of qualifying repositories through GitHub's repository-type search (where stars: is verified accurate) with a much longer, weekly cache, then scoping the issue search to that pool via repo: qualifiers confirmed live that a single query accepts up to 100 chained repo: qualifiers without truncation.
- →Tuned Popular Projects after live testing showed a recency-sorted single page produced too few results once correctly filtered: added GraphQL cursor pagination across three pages, raised the per-repository result cap specifically for this mode to preserve diversity without starving well-known repos, and extended the existing per-IP Caffeine rate limiter to cover both this endpoint and the RSS feed at a shared, explicitly chosen 20 requests/minute; added a lightweight in-memory counter that increments only on an actual GitHub refresh, never on a cache hit, to track real usage separately from the click-based counters already in place.
- →Discovered that GitHub's type: ISSUE search silently mixes in pull requests alongside real issues, wasting up to 90% of every page's budget on results our own code was already discarding; fixed by adding an is:issue qualifier everywhere issues are searched, verified live against the real API (3 of 30 real issues before the fix, 30 of 30 after) for a roughly 10x increase in usable results at zero additional API cost.
- →Diagnosed a silent, 100%-reproducible bug from real production log analysis: cross-origin POST requests to a search-statistics endpoint were being rejected with a CORS 403 because the CORS policy only allowlisted GET, while the frontend's own call failed silently by design so no user ever saw an error; reproduced the exact failure with a single curl request (with vs without an Origin header) before shipping the one-line fix, then confirmed the corrected response live in production.
- →Found and fixed a subtler bug in the IP-ban cache: its Caffeine expiry was a fixed multi-day duration that reset every time the cache reloaded from Postgres on startup, so an application restart near the end of a real ban would silently extend it; replaced the fixed expiry with a custom per-entry policy driven by each ban's actual expiration timestamp, verified with dedicated tests that simulate the exact restart-reload scenario rather than assuming the fix worked.
- →Took the fail2ban IP-ban layer from prototype to a deployed dry-run: re-verified the scanner-detection regex to full certainty against real server access logs rather than synthetic data, rewrote the frontend nginx to emit structured JSON access logs, tightened the blocklist patterns (bare wp-, .git and .env had matched any path merely containing those substrings, so a legitimate slug like wp-migration-tool would 403) to path-segment anchors, and confirmed Cloudflare's IP Access Rules API actually enforces an edge block before building on it; the layer now records which IPs it would ban to a file for a review window, making no API calls or firewall changes until the rules are proven against live traffic.
- →Deployed a self-contained observability stack alongside the app (Loki, Promtail, Grafana, Prometheus): the frontend nginx ships JSON access logs, the backend exposes Prometheus metrics on a separate management port deliberately kept off the public API host and writes structured logs to its own volume (the image pre-creates the log directory so the non-root container can write to a fresh named volume). Grafana carries two provisioned dashboards, one for edge/security traffic and one for backend JVM/HTTP metrics, and is reachable only through the shared nginx proxy behind a Cloudflare Access email gate, with no published ports on any service.
- →Found that GitHub's REST /rate_limit endpoint silently does not reflect GraphQL API point consumption it reports zero used no matter how many GraphQL calls are made so built a scheduled monitor that reads the REST and search limits from that endpoint but the GraphQL budget from the GraphQL API's own rateLimit field, exposed as metrics so the dashboard shows real quota burn instead of a flat, misleading full bar.
- →Replaced a bare RuntimeException for unhandled GitHub API failures logged twice with full stack traces and surfaced to the caller as a 500 with a dedicated exception mapped to a single WARN line and HTTP 502, keeping the catch-all stack-trace path only for genuinely unexpected errors.
- →Found two places logging one identical WARN line per occurrence instead of a single summary a null GitHub search-result node, and every request from an already-banned IP which could flood the log with dozens of duplicate lines per second under real scanner traffic; replaced the first with a per-batch skipped-count summary and the second with a Caffeine-backed per-IP throttle capping it to once a minute.
- →Fixed a real gap in the nginx scanner blocklist: an anchored regex only matched Symfony's dev-profiler probe at the URL root, so a scanner nesting it under an arbitrary prefix (as observed live in production, e.g. app_dev.php/_profiler/open) passed straight through to the SPA fallback; un-anchored the pattern and added blocks for Next.js/Vercel-internal paths this app never legitimately serves, verified against both the real attack paths and legitimate routes before deploying.
- →Took the fail2ban layer from dry-run to a live Cloudflare block: wrote a bounded-retry ban/unban action against Cloudflare's IP Access Rules API with its own audit log of every call's response, then verified the full ban-then-unban round trip against the real API using an RFC 5737 test address (reserved, never a live host) before switching the jail over for real, only after confirming zero false positives across roughly two days of dry-run observation.
- →Diagnosed a Grafana panel whose rate-limit reset countdown never seemed to move: it tracked a REST resource the app never actually calls (100% of traffic is GraphQL), so GitHub had no real consumption window to anchor a reset to and just returned a rolling now-plus-one-hour on every poll; repointed the query at the resource actually consumed and switched the display from a relative countdown to an absolute reset time.
JavaSpring BootReactTypeScriptPostgreSQLDockerHexagonal ArchitectureGitHub GraphQL APIResilience4j