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
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:
parent
708e20f747
commit
7879d493ea
6 changed files with 1050 additions and 0 deletions
397
apps/admin/src/app/(admin)/releases/page.tsx
Normal file
397
apps/admin/src/app/(admin)/releases/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue