feat: expiry countdown, refresh links, CLI tabs, recently viewed

- Backend: ?force=true on /proxy bypasses cache for fresh link fetch
- Frontend: live expiry countdown from `se` param; falls back to static 24h for consumer links
- Frontend: Refresh button appears when link expires in <6h, fires force fetch
- Frontend: CliCommand replaces Aria2Tip — wget/curl/aria2 tabs, persists in localStorage, on all pages
- Frontend: RecentlyViewed on homepage — localStorage, tracks consumer + eval pages, shows expired state
- chore: add .claude/ to .gitignore; remove FileSize (not returned by Microsoft CDN)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Shekhar Vaidya 2026-05-22 21:55:10 +05:30
parent 0557339e72
commit b2c9e2d874
5 changed files with 22 additions and 27 deletions

View file

@ -13,6 +13,7 @@ controlled by `CF_WORKER_URL` + `CF_WORKER_SECRET` env vars on the backend.
- Any meaningful feature or multi-file change → feature branch → PR → merge
- GitHub issues are for bugs and community contributions only — planned features tracked here and in PROGRESS.md
- Commit messages follow conventional commits: `feat:`, `fix:`, `docs:`, `chore:`
- **Always test locally before committing or pushing** — research feasibility before implementing (e.g. file size was planned but Microsoft CDN doesn't return it; caught after commit)
## Backlog (implement when ready, in priority order)
@ -28,13 +29,13 @@ negative cache (60s), dynamic TTL, stale-on-failure, jitter. Verified in product
- [x] **README architecture section** — caching layer, `/metrics`, env vars documented.
- [ ] **Per-IP / per-product rate limiter** — deferred; revisit after 1 month of production traffic data.
### Frontend UX improvements (next feature branch)
### Frontend UX improvements — `feat/ux-improvements` (in progress)
- [ ] **Expiry countdown** — parse `se` param from signed URL, show live countdown. Under 6h remaining → show "Refresh links" button.
- [ ] **Refresh links** — `?force=true` on `/proxy` bypasses cache, fetches fresh from Microsoft, resets countdown to ~24h.
- [ ] **CLI command tabs** — aria2 / wget / curl tabs instead of showing all at once. Persist last selected tab in localStorage. Default: wget.
- [ ] **Recently viewed** — localStorage only. Store product_id, name, version, se param. Show on homepage. Active links show countdown, expired links show "Expired" with re-fetch CTA.
- [ ] **File size** — verify if Microsoft CDN response includes file size. If yes, show alongside each download link (e.g. `x64 · 5.4 GB`). Research first before implementing.
- [x] **Expiry countdown** — parses `se` param from signed URL where available; falls back to "24 hours" for consumer links (Microsoft doesn't include `se` in consumer CDN URLs).
- [x] **Refresh links** — `?force=true` on `/proxy` bypasses cache, fetches fresh from Microsoft. Button shown when under 6h remaining.
- [x] **CLI command tabs** — wget / curl / aria2 tabs, persists selection in localStorage. Replaces old aria2-only tip. Applied to both consumer and eval pages.
- [x] **Recently viewed** — localStorage only. Shows on homepage. Consumer + eval pages both tracked. Expired state shown when link expiry known.
- ~~**File size**~~ — not feasible. Microsoft CDN does not return file size in the API response.
### Known bugs / open issues

View file

@ -67,15 +67,13 @@ CF Worker ensures Hetzner IP is never exposed to Microsoft — rate-limit block
---
## Planned — Frontend UX improvements
## In progress — Frontend UX improvements (`feat/ux-improvements`)
| Feature | Notes |
|---|---|
| Expiry countdown | Parse `se` param from signed URL, live countdown. Under 6h → show Refresh button |
| Refresh links | `?force=true` on `/proxy` bypasses cache, returns fresh URL, resets countdown |
| CLI command tabs | aria2 / wget / curl tabs, persist selection in localStorage, default wget |
| Recently viewed | localStorage only — active links show countdown, expired show re-fetch CTA |
| File size | Research first — verify if Microsoft CDN response includes it |
- **Expiry countdown** — shows on consumer links as static "24h" (Microsoft doesn't include `se` in consumer CDN URLs); would tick live if `se` present
- **Refresh links** — `?force=true` on `/proxy`, button appears under 6h remaining
- **CLI command tabs** — wget / curl / aria2, persists in localStorage, replaces old Aria2Tip on all pages
- **Recently viewed** — localStorage, homepage row, both consumer and eval pages tracked
- ~~**File size**~~ — not feasible, Microsoft CDN API does not return file size
---

View file

@ -4,9 +4,10 @@ import { motion, AnimatePresence } from 'motion/react'
import { ArrowLeft, Download, ExternalLink, AlertTriangle, Copy, Check, Server, ChevronDown } from 'lucide-react'
import { toast } from 'sonner'
import { evalProducts } from '../data/evalProducts'
import Aria2Tip from '../components/Aria2Tip'
import CliCommand from '../components/CliCommand'
import RelatedReleases from '../components/RelatedReleases'
import NotFoundPage from './NotFoundPage'
import { addRecentEntry } from '../components/RecentlyViewed'
const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:3002'
const SITE_URL = 'https://msdl.tech-latest.com'
@ -55,6 +56,11 @@ export default function EvalDetailPage() {
const [copiedUrl, setCopiedUrl] = useState<string | null>(null)
const [moreOpen, setMoreOpen] = useState(false)
useEffect(() => {
if (!product) return
addRecentEntry({ id: product.slug, name: product.name, badge: 'eval' })
}, [product?.slug])
useEffect(() => {
if (!product) return
setIsLoading(true)
@ -323,7 +329,7 @@ export default function EvalDetailPage() {
</div>
{/* Aria2 tip — outside box, same as consumer page */}
<Aria2Tip />
<CliCommand />
{/* Also available — outside box, same as consumer page */}
<RelatedReleases

View file

@ -243,12 +243,6 @@ export default function ProductDetailPage() {
return `${m}m`
}
function formatFileSize(bytes: number): string {
if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`
if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(0)} MB`
return `${bytes} B`
}
function startExpiryCountdown(expiry: Date) {
if (expiryTimerRef.current) clearInterval(expiryTimerRef.current)
const tick = () => setExpiryRemaining(expiry.getTime() - Date.now())
@ -567,7 +561,6 @@ export default function ProductDetailPage() {
const filename = link.Uri.split('/').pop()?.split('?')[0] ?? 'download.iso'
const arch = link.Architecture || archFromUri(link.Uri)
const isCopied = copiedUri === link.Uri
const sizeStr = link.FileSize ? formatFileSize(link.FileSize) : null
return (
<div
key={link.Uri}
@ -575,9 +568,7 @@ export default function ProductDetailPage() {
>
<div className="min-w-0">
<p className="text-[12px] font-mono text-white truncate">{filename}</p>
<p className="text-[11px] text-zinc-600 mt-0.5">
{arch}{sizeStr ? ` · ${sizeStr}` : ''}
</p>
<p className="text-[11px] text-zinc-600 mt-0.5">{arch}</p>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button

View file

@ -12,7 +12,6 @@ export interface SkuInfoResponse {
export interface DownloadOption {
Uri: string
Architecture: string
FileSize?: number
}
export interface ProxyResponse {