- Backend: ?force=true on /proxy bypasses cache for fresh link fetch
- Frontend: parse `se` param for live expiry countdown on download links
- Frontend: show Refresh button when link expires in <6h, force-fetches fresh URL
- Frontend: CliCommand component replaces Aria2Tip — wget/curl/aria2 tabs, persists selection in localStorage
- Frontend: RecentlyViewed component — localStorage, shows active countdown or expired state
- Frontend: FileSize shown alongside arch if returned by Microsoft CDN
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Background refresh on stale serve now goes through sfGroup.Do so
concurrent stale hits collapse into one Microsoft call instead of
hammering the API from multiple goroutines simultaneously.
- cleanupCaches: only logs when entries are actually evicted (was
logging every 30min regardless of whether anything changed).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /metrics: atomic counters for requests, cache hits, MS fetches, neg
hits, stale serves, and cache sizes per endpoint. Auth via
METRICS_SECRET env var (query param or Authorization: Bearer).
- Cache eviction: cleanupCaches() goroutine runs every 30min, removes
expired SKU/neg entries and link entries past ExpiresAt + 4h stale
window. Prevents unbounded map growth under sustained traffic.
- rand: replace global rand with package-level rand.Rand to avoid
mutex contention at high QPS (jitter() uses local source).
- Singleflight keys: separate sfKey from negKey in both handlers for
clarity — "sku:<id>" and "link:<id>:<sku>" vs neg cache keys.
- Background refresh on stale: when /proxy serves a stale entry after
a failed refresh, fires a goroutine to retry and update the cache
so the next request gets fresh data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduces Microsoft API traffic from thousands/day to ~50-100/day,
eliminating the 715-123130 rate-limit block under real traffic.
1. Singleflight (golang.org/x/sync/singleflight) — wraps all Microsoft
fetches so 500 concurrent cache misses collapse into 1 API hit.
2. SKU info cache — 7-day TTL keyed by product_id. Language lists are
stable; no need to hit Microsoft on every product page load.
3. Download link cache — dynamic TTL keyed by product_id:sku_id. Expiry
is derived from the signed URL's `se` query param minus 30min buffer,
so we never serve an expired signed URL.
4. Negative response caching — 60s TTL for 429 / 715-123130 failures.
Prevents thundering herd from retries worsening an existing block.
Only stored when no stale data is available to fall back on.
5. Dynamic TTL — parses `se` (signed expiry) from Microsoft CDN URL:
TTL = (se - now) - 30min. Falls back to 22h if unparseable.
6. Stale-on-failure — if a refresh fails (rate-limited or transient),
the expired cache entry is served temporarily. Applies to both
/proxy (download links) and /evallinks (eval ISOs).
7. Jitter — ±5min random offset on all TTLs prevents synchronised
mass-expiry spikes when many entries are warmed at the same time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds /eval listing page and /product/<slug> detail pages for Windows
Server 2025/2022/2019/2016 and Windows 11 Enterprise evaluation ISOs,
sourced from Microsoft evalcenter without any session flow.
Backend: new /evallinks endpoint fetches evalcenter page, extracts
fwlinks, follows all redirects in parallel, detects arch + locale.
24h cache per product with startup warming so first hit is instant.
Frontend: EvalPage listing at /eval, EvalDetailPage at /product/<slug>
matching consumer structure with downloads, aria2 template, system
requirements, and also-available section. Smart ProductRouter in
App.tsx dispatches slug vs numeric productId. Dock highlights Products
tab for /product/* and /eval routes. CTA banner on home page and
footer link added. sitemap.xml updated with 6 new URLs.
Closes#13
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replaced strconv.Atoi on sku_id with a permissive allowlist regex
(alphanumeric, hyphens, underscores). Older products like Windows 8.1
(#48, #52) return non-integer SKU IDs from Microsoft API, causing the
Get Download Links button to fail with 'sku_id must be a numeric value'.
Closes#9
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add cloudflare-worker/worker.js: edge proxy forwarding requests to allowed
Microsoft hosts from Cloudflare distributed IPs. Set CF_WORKER_URL env var
to activate; omit to keep direct-to-Microsoft behaviour (self-hosters unaffected).
- Wrap all outbound Microsoft URLs in proxyURL() in main.go so the Worker
sits transparently in front of every session and download-link request.
- Fix Back button navigating to external referrer when user lands directly
on a product URL (#5) — use window.history.state?.idx to detect whether
there is prior in-app history before calling navigate(-1).
Closes#5, Closes#6
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Reject non-numeric product_id and sku_id with HTTP 400 before any
upstream request is made, preventing parameter injection into
Microsoft API query strings.
- Replace all fmt.Sprintf URL construction with url.Values.Set +
.Encode() across handleSkuInfo, handleProxy, and setupSession,
ensuring values are always percent-encoded correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migrated products.json to structured metadata format.
- Refactored ProductDetailPage to eagerly reset state and validate product IDs against local catalog.
- Dynamic data fetching in HomePage and StatsBar for total release counts.
- Adjusted UI layout in App.tsx and UX states (404/Loading) for missing dependencies.
- Shortened backend tracking HTTP timeouts to prevent local Pi-Hole hangs.