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:
parent
0557339e72
commit
b2c9e2d874
5 changed files with 22 additions and 27 deletions
13
CLAUDE.md
13
CLAUDE.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
14
PROGRESS.md
14
PROGRESS.md
|
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ export interface SkuInfoResponse {
|
|||
export interface DownloadOption {
|
||||
Uri: string
|
||||
Architecture: string
|
||||
FileSize?: number
|
||||
}
|
||||
|
||||
export interface ProxyResponse {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue