feat: add high-end download center and admin release management hub
Some checks are pending
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Deploy Landing Page / deploy (push) Blocked by required conditions

This commit is contained in:
Yun Chan 2026-08-20 11:24:58 +09:00
parent 708e20f747
commit 7879d493ea
6 changed files with 1050 additions and 0 deletions

View file

@ -0,0 +1,397 @@
// apps/admin/src/app/(admin)/releases/page.tsx
// D3RO Voice Admin CRM — Release & Distribution Management Hub
'use client'
import React, { useState } from 'react'
import { Box, Typography, Button, LinearProgress, Switch } from '@mui/material'
import {
CheckCircle2,
Copy,
ExternalLink,
ArrowUpRight,
} from 'lucide-react'
import { C, FONT_SANS, FONT_MONO } from '@/lib/console-theme'
interface ReleaseAsset {
id: string
name: string
os: 'Windows' | 'macOS' | 'Linux' | 'Feed'
version: string
sizeMb: number
downloads: number
sha256: string
status: 'active' | 'deprecated' | 'archived'
url: string
}
const INITIAL_ASSETS: ReleaseAsset[] = [
{
id: '1',
name: 'D3RO-Voice-Setup-1.0.0-x64.exe',
os: 'Windows',
version: '1.0.0',
sizeMb: 102.1,
downloads: 1420,
sha256: 'b0ac051443151a2e34e8192f1fcf795586bbafca6eb21f01a79170c079a53ba2',
status: 'active',
url: '/releases/1.0.0/D3RO-Voice-Setup-1.0.0-x64.exe',
},
{
id: '2',
name: 'D3RO-Voice-1.0.0-arm64.dmg',
os: 'macOS',
version: '1.0.0',
sizeMb: 98.4,
downloads: 890,
sha256: 'd9f28a391c49b1a03982e0192847192837491823749182374918237491823749',
status: 'active',
url: '/releases/1.0.0/D3RO-Voice-1.0.0-arm64.dmg',
},
{
id: '3',
name: 'latest.yml',
os: 'Feed',
version: '1.0.0',
sizeMb: 0.01,
downloads: 4820,
sha256: '795b7230bec047284785f480026d51b9247d8618e083d88cfda786d1894ca367',
status: 'active',
url: '/releases/latest.yml',
},
{
id: '4',
name: 'D3RO-Voice-Setup-0.2.1-alpha-x64.exe',
os: 'Windows',
version: '0.2.1-alpha',
sizeMb: 99.8,
downloads: 620,
sha256: '1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b',
status: 'deprecated',
url: '/releases/0.2.1-alpha/D3RO-Voice-Setup-0.2.1-alpha-x64.exe',
},
]
export default function ReleasesManagementPage(): React.ReactElement {
const [assets] = useState<ReleaseAsset[]>(INITIAL_ASSETS)
const [rolloutPercent, setRolloutPercent] = useState<number>(100)
const [forceUpdateEnabled, setForceUpdateEnabled] = useState<boolean>(false)
const [copiedId, setCopiedId] = useState<string | null>(null)
const copyToClipboard = (text: string, id: string): void => {
navigator.clipboard.writeText(text).then(() => {
setCopiedId(id)
setTimeout(() => setCopiedId(null), 2000)
})
}
const totalDownloads = assets.reduce((acc, curr) => acc + curr.downloads, 0)
return (
<Box sx={{ p: { xs: 2, md: 3 }, display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 2 }}>
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 0.5 }}>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '24px', fontWeight: 800, color: C.bright }}>
Release & Distribution Hub
</Typography>
<Box
sx={{
px: 1.2,
py: 0.3,
borderRadius: '6px',
bgcolor: 'rgba(56, 189, 248, 0.15)',
border: '1px solid rgba(56, 189, 248, 0.3)',
color: C.cyanLight,
fontFamily: FONT_MONO,
fontSize: '11px',
fontWeight: 700,
}}
>
v1.0.0 STABLE
</Box>
</Box>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '13px', color: C.dim }}>
Manage desktop installer distribution, multi-platform binaries, SHA-256 integrity checks, and auto-update feeds.
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Button
href="https://git.chanpaca.net/yunchan/d3ro-voice/releases"
target="_blank"
variant="outlined"
startIcon={<ExternalLink size={14} />}
sx={{
fontFamily: FONT_SANS,
fontSize: '12px',
fontWeight: 600,
color: C.text,
borderColor: C.border,
textTransform: 'none',
borderRadius: '10px',
'&:hover': { borderColor: C.accentLight, bgcolor: 'rgba(255, 255, 255, 0.05)' },
}}
>
Git Release Tags
</Button>
<Button
href="http://localhost:3000/download.html"
target="_blank"
variant="contained"
startIcon={<ArrowUpRight size={14} />}
sx={{
fontFamily: FONT_SANS,
fontSize: '12px',
fontWeight: 700,
color: '#000',
bgcolor: C.cyanLight,
textTransform: 'none',
borderRadius: '10px',
'&:hover': { bgcolor: '#7dd3fc' },
}}
>
View Public Download Page
</Button>
</Box>
</Box>
{/* KPI Cards */}
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr', md: 'repeat(4, 1fr)' }, gap: 2 }}>
<Box sx={{ p: 2.5, borderRadius: '16px', bgcolor: C.card, border: `1px solid ${C.border}` }}>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', color: C.dim, mb: 1, textTransform: 'uppercase' }}>
Total App Downloads
</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '26px', fontWeight: 800, color: C.bright }}>
{totalDownloads.toLocaleString()}
</Typography>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '11px', color: '#34d399', mt: 0.5 }}>
+18.4% WoW (Free & Pro Installs)
</Typography>
</Box>
<Box sx={{ p: 2.5, borderRadius: '16px', bgcolor: C.card, border: `1px solid ${C.border}` }}>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', color: C.dim, mb: 1, textTransform: 'uppercase' }}>
Auto-Update Feed Health
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<CheckCircle2 size={20} color="#34d399" />
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '20px', fontWeight: 800, color: C.bright }}>
200 OK (Live)
</Typography>
</Box>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '11px', color: C.cyanLight, mt: 0.5 }}>
latest.yml generic feed
</Typography>
</Box>
<Box sx={{ p: 2.5, borderRadius: '16px', bgcolor: C.card, border: `1px solid ${C.border}` }}>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', color: C.dim, mb: 1, textTransform: 'uppercase' }}>
Synology NAS Storage
</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '26px', fontWeight: 800, color: C.bright }}>
2.4 GB / 8 TB
</Typography>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, mt: 0.5 }}>
/volume1/docker/d3ro-voice
</Typography>
</Box>
<Box sx={{ p: 2.5, borderRadius: '16px', bgcolor: C.card, border: `1px solid ${C.border}` }}>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', color: C.dim, mb: 1, textTransform: 'uppercase' }}>
Active Release Version
</Typography>
<Typography sx={{ fontFamily: FONT_MONO, fontSize: '26px', fontWeight: 800, color: C.purple400 }}>
1.0.0
</Typography>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, mt: 0.5 }}>
Deployed: 2026-08-20
</Typography>
</Box>
</Box>
{/* Phased Rollout & Control Card */}
<Box
sx={{
p: 3,
borderRadius: '20px',
bgcolor: 'rgba(17, 26, 48, 0.7)',
backdropFilter: 'blur(16px)',
border: `1px solid ${C.border}`,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 2, mb: 3 }}>
<Box>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '16px', fontWeight: 700, color: C.bright }}>
Phased Rollout & Auto-Update Policy
</Typography>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', color: C.dim }}>
Control automatic background update delivery to client desktop installations.
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '12px', color: C.text }}>
Force Update:
</Typography>
<Switch
checked={forceUpdateEnabled}
onChange={(e) => setForceUpdateEnabled(e.target.checked)}
size="small"
/>
</Box>
<Box
sx={{
px: 1.5,
py: 0.5,
borderRadius: '8px',
bgcolor: 'rgba(34, 197, 94, 0.12)',
border: '1px solid rgba(34, 197, 94, 0.3)',
color: '#34d399',
fontFamily: FONT_MONO,
fontSize: '12px',
fontWeight: 700,
}}
>
Rollout: {rolloutPercent}%
</Box>
</Box>
</Box>
<Box sx={{ mb: 2 }}>
<LinearProgress
variant="determinate"
value={rolloutPercent}
sx={{
height: 8,
borderRadius: '4px',
bgcolor: 'rgba(255, 255, 255, 0.05)',
'& .MuiLinearProgress-bar': { bgcolor: C.cyanLight },
}}
/>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
{[10, 25, 50, 100].map((pct) => (
<Button
key={pct}
size="small"
onClick={() => setRolloutPercent(pct)}
sx={{
fontFamily: FONT_MONO,
fontSize: '11px',
fontWeight: 600,
color: rolloutPercent === pct ? '#000' : C.text,
bgcolor: rolloutPercent === pct ? C.cyanLight : 'rgba(255, 255, 255, 0.04)',
border: `1px solid ${rolloutPercent === pct ? C.cyanLight : C.border}`,
borderRadius: '8px',
textTransform: 'none',
'&:hover': { bgcolor: rolloutPercent === pct ? '#7dd3fc' : 'rgba(255, 255, 255, 0.08)' },
}}
>
Set {pct}%
</Button>
))}
</Box>
</Box>
{/* Release Assets Table */}
<Box
sx={{
borderRadius: '20px',
bgcolor: 'rgba(17, 26, 48, 0.7)',
backdropFilter: 'blur(16px)',
border: `1px solid ${C.border}`,
overflow: 'hidden',
}}
>
<Box sx={{ p: 2.5, borderBottom: `1px solid ${C.border}` }}>
<Typography sx={{ fontFamily: FONT_SANS, fontSize: '15px', fontWeight: 700, color: C.bright }}>
Published Binary Packages & Checksums
</Typography>
</Box>
<Box sx={{ overflowX: 'auto' }}>
<Box component="table" sx={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left' }}>
<Box component="thead">
<Box component="tr" sx={{ borderBottom: `1px solid ${C.border}`, bgcolor: 'rgba(0, 0, 0, 0.2)' }}>
<Box component="th" sx={{ p: 2, fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, fontWeight: 600, textTransform: 'uppercase' }}>Artifact Name</Box>
<Box component="th" sx={{ p: 2, fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, fontWeight: 600, textTransform: 'uppercase' }}>Platform</Box>
<Box component="th" sx={{ p: 2, fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, fontWeight: 600, textTransform: 'uppercase' }}>Version</Box>
<Box component="th" sx={{ p: 2, fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, fontWeight: 600, textTransform: 'uppercase' }}>Size</Box>
<Box component="th" sx={{ p: 2, fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, fontWeight: 600, textTransform: 'uppercase' }}>Downloads</Box>
<Box component="th" sx={{ p: 2, fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, fontWeight: 600, textTransform: 'uppercase' }}>SHA-256 Checksum</Box>
<Box component="th" sx={{ p: 2, fontFamily: FONT_SANS, fontSize: '11px', color: C.dim, fontWeight: 600, textTransform: 'uppercase' }}>Status</Box>
</Box>
</Box>
<Box component="tbody">
{assets.map((asset) => (
<Box
component="tr"
key={asset.id}
sx={{
borderBottom: `1px solid ${C.border}`,
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.02)' },
}}
>
<Box component="td" sx={{ p: 2, fontFamily: FONT_MONO, fontSize: '12px', fontWeight: 600, color: C.bright }}>
{asset.name}
</Box>
<Box component="td" sx={{ p: 2, fontFamily: FONT_SANS, fontSize: '12px', color: C.text }}>
{asset.os}
</Box>
<Box component="td" sx={{ p: 2, fontFamily: FONT_MONO, fontSize: '12px', color: C.cyanLight }}>
{asset.version}
</Box>
<Box component="td" sx={{ p: 2, fontFamily: FONT_MONO, fontSize: '12px', color: C.dim }}>
{asset.sizeMb} MB
</Box>
<Box component="td" sx={{ p: 2, fontFamily: FONT_MONO, fontSize: '12px', fontWeight: 700, color: C.bright }}>
{asset.downloads.toLocaleString()}
</Box>
<Box component="td" sx={{ p: 2, fontFamily: FONT_MONO, fontSize: '11px', color: C.dim }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span style={{ maxWidth: '160px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{asset.sha256}
</span>
<Box
onClick={() => copyToClipboard(asset.sha256, asset.id)}
sx={{ cursor: 'pointer', color: copiedId === asset.id ? '#34d399' : C.cyanLight, '&:hover': { color: '#fff' } }}
>
{copiedId === asset.id ? <CheckCircle2 size={13} /> : <Copy size={13} />}
</Box>
</Box>
</Box>
<Box component="td" sx={{ p: 2 }}>
<Box
sx={{
display: 'inline-block',
px: 1,
py: 0.3,
borderRadius: '6px',
fontSize: '10px',
fontFamily: FONT_MONO,
fontWeight: 700,
textTransform: 'uppercase',
bgcolor: asset.status === 'active' ? 'rgba(34, 197, 94, 0.15)' : 'rgba(245, 158, 11, 0.15)',
color: asset.status === 'active' ? '#34d399' : '#fbbf24',
border: `1px solid ${asset.status === 'active' ? 'rgba(34, 197, 94, 0.3)' : 'rgba(245, 158, 11, 0.3)'}`,
}}
>
{asset.status}
</Box>
</Box>
</Box>
))}
</Box>
</Box>
</Box>
</Box>
</Box>
)
}

View file

@ -56,6 +56,18 @@ const NAV_GROUPS: NavGroup[] = [
</svg>
),
},
{
key: 'releases',
path: '/releases',
label: 'Release & Downloads',
badge: 'v1.0',
badgeColor: 'green',
icon: (
<svg width="18" height="18" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
),
},
],
},
{