chore: remove duplicate copies of the site, download center and installers (WS-C)

The landing page, download pages, invite page, assetlinks and installers
existed in two or three places; only site/ and the Forgejo feed are served.

- apps/api-server/wwwroot: delete the stale site build, download/invite
  pages, .well-known copy, legacy static admin and 1.0.0 binaries. The API
  no longer serves static files (UseStaticFiles/fallbacks and the apk/zip
  blocker removed); the Next admin is the only admin UI.
- Delete 19 tracked installers/packages (~568 MiB) under site/public/releases,
  apps/web/public/releases and wwwroot/releases; .gitignore blocks them.
- apps/web: delete the download/releases pages, desktop-release.ts, the
  download.html and assetlinks copies, and the accept-invite page (invites
  are only issued to the site's /accept-invite/). e2e specs call the /app
  base path and check the /download redirect instead.
- scripts: delete the retired release/NAS site scripts, drop the web target
  from sync-version, and check assetlinks in site/public only.
- Delete the unused Dockerfile.admin (apps/admin/Dockerfile is used).

Policy: docs/REFACTOR_POLICY.md Wave 3, W3-5 and W3-6.
This commit is contained in:
Yun Chan 2026-09-26 15:48:52 +09:00
parent b6fe588a7c
commit cd9d199dbf
53 changed files with 43 additions and 3278 deletions

View file

@ -1,183 +0,0 @@
'use client'
// apps/web/src/app/accept-invite/page.tsx
// 팀 초대 수락 페이지 — URL의 ?token=을 team-accept Edge Function으로 전달
import { Suspense, useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { Box, Stack, Alert, CircularProgress } from '@mui/material'
import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds'
import { d3roPalette } from '@d3ro/ui/theme'
import { getSupabaseBrowserClient, isSupabaseConfigured } from '@/lib/supabase-browser'
type AcceptState = 'loading' | 'success' | 'error' | 'need_login'
// useSearchParams는 Suspense boundary 필요 (Next.js 15 prerender 규칙)
export default function AcceptInvitePage(): React.ReactElement {
return (
<Suspense
fallback={
<Box
sx={{
minHeight: '100dvh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: d3roPalette.bg.app
}}
>
<CircularProgress color="warning" />
</Box>
}
>
<AcceptInviteInner />
</Suspense>
)
}
function AcceptInviteInner(): React.ReactElement {
const router = useRouter()
const searchParams = useSearchParams()
const token = searchParams.get('token')
const [state, setState] = useState<AcceptState>('loading')
const [message, setMessage] = useState<string>('')
const [teamId, setTeamId] = useState<string | null>(null)
useEffect(() => {
async function run(): Promise<void> {
if (!token) {
setState('error')
setMessage('유효하지 않은 초대 링크입니다 (토큰 누락).')
return
}
if (!isSupabaseConfigured()) {
setState('error')
setMessage('Supabase가 설정되지 않았습니다.')
return
}
const supabase = getSupabaseBrowserClient()
const {
data: { session }
} = await supabase.auth.getSession()
if (!session) {
setState('need_login')
setMessage('초대를 수락하려면 먼저 로그인해주세요.')
// 토큰을 sessionStorage에 저장해두고 로그인 후 돌아오도록
try {
sessionStorage.setItem('pending_invite_token', token)
} catch {
// storage 차단 시 무시
}
return
}
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/functions/v1/team-accept`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${session.access_token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ token })
}
)
if (!response.ok) {
const errData = (await response.json()) as { error?: string; message?: string }
setState('error')
setMessage(errData.message ?? errData.error ?? `실패: ${response.status}`)
return
}
const data = (await response.json()) as { team_id: string; role: string }
setTeamId(data.team_id)
setState('success')
setMessage(`팀에 가입되었습니다 (${data.role}). 잠시 후 이동합니다...`)
// 성공 시 3초 후 팀 페이지로
setTimeout(() => {
router.replace(`/teams/${data.team_id}`)
}, 2000)
} catch (e) {
setState('error')
setMessage(e instanceof Error ? e.message : 'Unknown error')
}
}
void run()
}, [token, router])
return (
<Box
sx={{
minHeight: '100dvh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
bgcolor: d3roPalette.bg.app,
p: 4
}}
>
<MetalCard sx={{ maxWidth: 480, width: '100%', p: 4 }}>
<Stack spacing={3} alignItems="center">
<PhosphorText variant="title">TEAM INVITE</PhosphorText>
{state === 'loading' && <CircularProgress color="warning" />}
{state === 'success' && (
<Alert severity="success" variant="outlined" sx={{ width: '100%' }}>
{message}
</Alert>
)}
{state === 'error' && (
<>
<Alert severity="error" variant="outlined" sx={{ width: '100%' }}>
{message}
</Alert>
<Box
component="a"
href="/dashboard"
sx={{
color: d3roPalette.accent.main,
textDecoration: 'none',
fontSize: 13
}}
>
대시보드로 이동
</Box>
</>
)}
{state === 'need_login' && (
<>
<Alert severity="info" variant="outlined" sx={{ width: '100%' }}>
{message}
</Alert>
<Box
component="a"
href="/login"
sx={{
color: d3roPalette.accent.main,
textDecoration: 'none',
fontSize: 13
}}
>
로그인 페이지로 이동
</Box>
</>
)}
{teamId && (
<Box sx={{ color: d3roPalette.text.muted, fontSize: 11 }}>
Team ID: {teamId}
</Box>
)}
</Stack>
</MetalCard>
</Box>
)
}

View file

@ -1,650 +0,0 @@
// apps/web/src/app/download/page.tsx
// D3RO Voice Web App — Official Download Center & Release History
'use client'
import React, { useState } from 'react'
import {
Box,
Typography,
Button,
Container,
Paper,
Chip,
} from '@mui/material'
import DownloadIcon from '@mui/icons-material/Download'
import ShieldOutlinedIcon from '@mui/icons-material/ShieldOutlined'
import AppleIcon from '@mui/icons-material/Apple'
import WindowsIcon from '@mui/icons-material/Window'
import StorageIcon from '@mui/icons-material/Storage'
import CloudUploadIcon from '@mui/icons-material/CloudUpload'
import OpenInNewIcon from '@mui/icons-material/OpenInNew'
import { d3roPalette } from '@d3ro/ui/theme'
import {
DESKTOP_FEED_URL,
DESKTOP_RELEASE_HUB_URL,
DESKTOP_RELEASES_URL,
DESKTOP_RELEASE_DATE,
DESKTOP_VERSION,
DESKTOP_WINDOWS_INSTALLER_FILENAME,
DESKTOP_WINDOWS_INSTALLER_URL,
} from '@/lib/desktop-release'
export default function DownloadPage(): React.ReactElement {
const [verifyStatus, setVerifyStatus] = useState<'idle' | 'computing' | 'done'>('idle')
const [computedHash, setComputedHash] = useState('')
const [fileName, setFileName] = useState('')
const handleFileVerify = async (e: React.ChangeEvent<HTMLInputElement>): Promise<void> => {
const files = e.target.files
if (!files || files.length === 0) return
const file = files[0]
setFileName(`${file.name} (${(file.size / (1024 * 1024)).toFixed(1)} MB)`)
setVerifyStatus('computing')
const arrayBuffer = await file.arrayBuffer()
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
setComputedHash(hashHex)
setVerifyStatus('done')
}
return (
<Box
sx={{
minHeight: '100dvh',
bgcolor: d3roPalette.bg.app,
color: d3roPalette.text.primary,
backgroundImage: 'radial-gradient(ellipse 80% 50% at 50% -20%, var(--d3-tag-cyan), transparent 70%)',
py: { xs: 4, md: 8 },
px: 2,
}}
>
<Container maxWidth="lg">
{/* Navigation Bar */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 6 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box
sx={{
width: 36,
height: 36,
borderRadius: '10px',
bgcolor: d3roPalette.accent.dark,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontWeight: 600,
color: 'var(--d3-text-inverse)',
boxShadow: '0 0 20px var(--d3-tag-cyan)',
}}
>
D3
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, letterSpacing: '-0.02em', color: 'var(--d3-text-inverse)' }}>
D3RO VOICE
</Typography>
<Chip
label={`v${DESKTOP_VERSION} OFFICIAL STABLE RELEASE`}
size="small"
sx={{
bgcolor: 'color-mix(in srgb, var(--d3-tag-green) 15%, transparent)',
color: d3roPalette.tag.green,
border: '1px solid var(--d3-status-success)',
fontWeight: 500,
fontSize: '10px',
}}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<Button
href={DESKTOP_RELEASES_URL}
target="_blank"
endIcon={<OpenInNewIcon sx={{ fontSize: '14px !important' }} />}
sx={{ color: d3roPalette.text.secondary, fontSize: '13px', textTransform: 'none', '&:hover': { color: 'var(--d3-text-inverse)' } }}
>
Forgejo Releases
</Button>
<Button
href="/login"
variant="outlined"
sx={{
borderColor: 'var(--d3-glass-hairlineStrong)',
color: d3roPalette.text.primary,
textTransform: 'none',
borderRadius: '10px',
'&:hover': { borderColor: d3roPalette.accent.light, bgcolor: 'color-mix(in srgb, var(--d3-accent-light) 8%, transparent)' },
}}
>
Web Console
</Button>
</Box>
</Box>
{/* Hero Section */}
<Box sx={{ textAlign: 'center', maxWidth: 700, mx: 'auto', mb: 8 }}>
<Chip
icon={<ShieldOutlinedIcon sx={{ fontSize: '14px !important', color: 'var(--d3-tag-green) !important' }} />}
label="OFFICIAL STABLE RELEASE"
sx={{
bgcolor: 'color-mix(in srgb, var(--d3-tag-green) 10%, transparent)',
color: d3roPalette.tag.green,
border: '1px solid var(--d3-status-success)',
fontWeight: 500,
fontSize: '11px',
mb: 3,
}}
/>
<Typography
variant="h3"
component="h1"
sx={{
fontWeight: 600,
letterSpacing: '-0.03em',
mb: 2,
fontSize: { xs: '2rem', md: '3rem' },
}}
>
Download{' '}
<Box component="span" sx={{ color: d3roPalette.accent.light }}>
D3RO Voice
</Box>{' '}
Desktop {DESKTOP_VERSION}
</Typography>
<Typography sx={{ color: d3roPalette.text.secondary, fontSize: '16px', lineHeight: 1.6 }}>
Official multi-platform release. Zero-latency offline Whisper Large-v3-Turbo,
cloud AI failover, local knowledge base, and update-feed verified integrity.
</Typography>
</Box>
{/* Primary Download Card */}
<Paper
elevation={0}
sx={{
maxWidth: 580,
mx: 'auto',
p: { xs: 3, sm: 4 },
borderRadius: '24px',
bgcolor: 'color-mix(in srgb, var(--d3-bg-app) 75%, transparent)',
backdropFilter: 'blur(20px)',
border: '1px solid var(--d3-tag-cyan)',
boxShadow: '0 24px 60px -15px var(--d3-scrim), 0 0 40px -10px var(--d3-tag-cyan)',
mb: 10,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box
sx={{
width: 48,
height: 48,
borderRadius: '14px',
bgcolor: 'color-mix(in srgb, var(--d3-accent-light) 15%, transparent)',
border: '1px solid var(--d3-tag-cyan)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: d3roPalette.accent.light,
}}
>
<ShieldOutlinedIcon />
</Box>
<Box>
<Typography sx={{ fontWeight: 600, fontSize: '18px', color: 'var(--d3-text-inverse)' }}>
D3RO Voice Desktop {DESKTOP_VERSION}
</Typography>
<Typography sx={{ color: d3roPalette.text.secondary, fontSize: '12px', fontFamily: 'monospace' }}>
Windows 10 / 11 (x64) · NSIS standalone installer
</Typography>
</Box>
</Box>
<Chip
label="VERIFIED & ACTIVE"
size="small"
sx={{
bgcolor: 'color-mix(in srgb, var(--d3-tag-green) 15%, transparent)',
color: d3roPalette.tag.green,
border: '1px solid var(--d3-status-success)',
fontWeight: 600,
fontSize: '10px',
}}
/>
</Box>
<Button
component="a"
href={DESKTOP_WINDOWS_INSTALLER_URL}
variant="contained"
fullWidth
size="large"
startIcon={<DownloadIcon />}
sx={{
py: 2,
borderRadius: '14px',
bgcolor: d3roPalette.accent.light,
color: d3roPalette.bg.card,
fontWeight: 600,
fontSize: '15px',
textTransform: 'none',
boxShadow: '0 8px 25px var(--d3-tag-cyan)',
'&:hover': { bgcolor: d3roPalette.accent.main },
}}
>
Download for Windows (x64) - v{DESKTOP_VERSION}
</Button>
<Box
sx={{
mt: 3,
pt: 2.5,
borderTop: '1px solid var(--d3-overlay-strong)',
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'flex-start', sm: 'center' },
justifyContent: 'space-between',
gap: 1.5,
fontSize: '12px',
color: d3roPalette.text.secondary,
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<span>SHA-512:</span>
<Button
href={`${DESKTOP_FEED_URL}/latest.yml`}
target="_blank"
size="small"
sx={{
color: d3roPalette.text.primary,
fontFamily: 'monospace',
fontSize: '11px',
p: 0,
minWidth: 0,
textTransform: 'none',
}}
>
latest.yml
</Button>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
<Button
href={DESKTOP_RELEASE_HUB_URL}
target="_blank"
size="small"
sx={{
color: d3roPalette.accent.light,
fontSize: '11px',
p: 0,
minWidth: 0,
textTransform: 'none',
}}
>
Release notes
</Button>
<Typography sx={{ color: d3roPalette.tag.green, fontSize: '11px', fontWeight: 600 }}>
✓ Update feed connected
</Typography>
</Box>
</Box>
</Paper>
{/* All Platform Bento Grid */}
<Box sx={{ mb: 10 }}>
<Typography variant="h5" sx={{ fontWeight: 600, mb: 1, color: 'var(--d3-text-inverse)' }}>
All Platform Packages
</Typography>
<Typography sx={{ color: d3roPalette.text.secondary, fontSize: '14px', mb: 4 }}>
Planned targets and their current verification status.
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: 'repeat(3, 1fr)' }, gap: 3 }}>
{/* Windows Card */}
<Paper
elevation={0}
sx={{
p: 3.5,
borderRadius: '20px',
bgcolor: 'color-mix(in srgb, var(--d3-bg-app) 60%, transparent)',
border: '1px solid var(--d3-overlay-strong)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
transition: 'border-color 0.2s',
'&:hover': { borderColor: 'color-mix(in srgb, var(--d3-accent-light) 40%, transparent)' },
}}
>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<WindowsIcon sx={{ color: d3roPalette.accent.light, fontSize: 28 }} />
<Chip label="x64 TARGET" size="small" sx={{ bgcolor: 'var(--d3-border-subtle)', color: d3roPalette.text.secondary }} />
</Box>
<Typography sx={{ fontWeight: 600, fontSize: '18px', color: 'var(--d3-text-inverse)', mb: 1 }}>Windows</Typography>
<Typography sx={{ color: d3roPalette.text.secondary, fontSize: '13px', mb: 3 }}>
Official stable installer with automated background updates and zero-latency local AI.
</Typography>
</Box>
<Button
component="a"
href={DESKTOP_WINDOWS_INSTALLER_URL}
variant="outlined"
fullWidth
startIcon={<DownloadIcon />}
sx={{
borderColor: 'color-mix(in srgb, var(--d3-accent-light) 30%, transparent)',
color: d3roPalette.accent.light,
textTransform: 'none',
borderRadius: '10px',
fontWeight: 500,
'&:hover': { bgcolor: 'color-mix(in srgb, var(--d3-accent-light) 10%, transparent)', borderColor: d3roPalette.accent.light },
}}
>
Download Setup (.exe)
</Button>
</Paper>
{/* macOS Card */}
<Paper
elevation={0}
sx={{
p: 3.5,
borderRadius: '20px',
bgcolor: 'color-mix(in srgb, var(--d3-bg-app) 60%, transparent)',
border: '1px solid var(--d3-overlay-strong)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
transition: 'border-color 0.2s',
'&:hover': { borderColor: 'color-mix(in srgb, var(--d3-tag-purple) 40%, transparent)' },
}}
>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<AppleIcon sx={{ color: d3roPalette.tag.purple, fontSize: 28 }} />
<Chip label="M1 / M2 / M3 / M4" size="small" sx={{ bgcolor: 'var(--d3-border-subtle)', color: d3roPalette.text.secondary }} />
</Box>
<Typography sx={{ fontWeight: 600, fontSize: '18px', color: 'var(--d3-text-inverse)', mb: 1 }}>macOS</Typography>
<Typography sx={{ color: d3roPalette.text.secondary, fontSize: '13px', mb: 3 }}>
Apple Silicon candidate undergoing code-signing and installation verification.
</Typography>
</Box>
<Button
disabled
variant="outlined"
fullWidth
startIcon={<DownloadIcon />}
sx={{
borderColor: 'color-mix(in srgb, var(--d3-tag-purple) 30%, transparent)',
color: d3roPalette.tag.purple,
textTransform: 'none',
borderRadius: '10px',
fontWeight: 500,
'&:hover': { bgcolor: 'color-mix(in srgb, var(--d3-tag-purple) 10%, transparent)', borderColor: d3roPalette.tag.purple },
}}
>
Verification pending
</Button>
</Paper>
{/* Synology NAS & Docker Card */}
<Paper
elevation={0}
sx={{
p: 3.5,
borderRadius: '20px',
bgcolor: 'color-mix(in srgb, var(--d3-bg-app) 60%, transparent)',
border: '1px solid var(--d3-overlay-strong)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
transition: 'border-color 0.2s',
'&:hover': { borderColor: 'color-mix(in srgb, var(--d3-tag-green) 40%, transparent)' },
}}
>
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<StorageIcon sx={{ color: d3roPalette.tag.green, fontSize: 28 }} />
<Chip label="Container Manager" size="small" sx={{ bgcolor: 'var(--d3-border-subtle)', color: d3roPalette.text.secondary }} />
</Box>
<Typography sx={{ fontWeight: 600, fontSize: '18px', color: 'var(--d3-text-inverse)', mb: 1 }}>Synology NAS</Typography>
<Typography sx={{ color: d3roPalette.text.secondary, fontSize: '13px', mb: 3 }}>
Self-hosted private deployment package for Synology Container Manager & CRM.
</Typography>
</Box>
<Button
href="https://git.chanpaca.net/yunchan/d3ro-voice/src/branch/main/docker-compose.nas.yml"
target="_blank"
variant="outlined"
fullWidth
startIcon={<OpenInNewIcon />}
sx={{
borderColor: 'color-mix(in srgb, var(--d3-tag-green) 30%, transparent)',
color: d3roPalette.tag.green,
textTransform: 'none',
borderRadius: '10px',
fontWeight: 500,
'&:hover': { bgcolor: 'color-mix(in srgb, var(--d3-tag-green) 10%, transparent)', borderColor: d3roPalette.tag.green },
}}
>
docker-compose.nas.yml →
</Button>
</Paper>
</Box>
</Box>
{/* Client-Side Cryptographic Verifier */}
<Paper
elevation={0}
sx={{
p: { xs: 3, md: 5 },
borderRadius: '24px',
bgcolor: 'color-mix(in srgb, var(--d3-bg-app) 65%, transparent)',
border: '1px solid var(--d3-tag-cyan)',
mb: 10,
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2, mb: 3 }}>
<Box>
<Typography sx={{ color: d3roPalette.accent.light, fontSize: '11px', fontFamily: 'monospace', fontWeight: 500, mb: 0.5 }}>
LOCAL FILE UTILITY
</Typography>
<Typography variant="h6" sx={{ fontWeight: 600, color: 'var(--d3-text-inverse)' }}>
SHA-256 File Calculator
</Typography>
<Typography sx={{ color: d3roPalette.text.secondary, fontSize: '13px' }}>
Compute a selected file hash locally. This does not certify an official D3RO Voice release.
</Typography>
</Box>
<Button
component="label"
variant="outlined"
startIcon={<CloudUploadIcon />}
sx={{
borderColor: 'color-mix(in srgb, var(--d3-accent-light) 30%, transparent)',
color: d3roPalette.accent.light,
borderRadius: '12px',
textTransform: 'none',
fontWeight: 600,
}}
>
Select File to Check
<input type="file" hidden onChange={handleFileVerify} />
</Button>
</Box>
{verifyStatus !== 'idle' && (
<Box
sx={{
p: 2.5,
borderRadius: '14px',
bgcolor: 'var(--d3-scrim)',
border: '1px solid var(--d3-overlay-strong)',
fontFamily: 'monospace',
fontSize: '12px',
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<span style={{ color: d3roPalette.text.primary, fontWeight: 600 }}>{fileName}</span>
{verifyStatus === 'computing' && <Chip label="Computing..." size="small" sx={{ bgcolor: d3roPalette.tag.orange, color: 'var(--d3-bg-app)' }} />}
{verifyStatus === 'done' && <Chip label="LOCAL HASH GENERATED" size="small" sx={{ bgcolor: d3roPalette.accent.light, color: 'var(--d3-bg-app)' }} />}
</Box>
<Box sx={{ color: d3roPalette.text.secondary }}>
Calculated: <span style={{ color: d3roPalette.accent.light }}>{computedHash || 'Hashing...'}</span>
</Box>
</Box>
)}
</Paper>
{/* Release Changelog Timeline */}
<Box sx={{ mb: 10 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h5" sx={{ fontWeight: 600, color: 'var(--d3-text-inverse)' }}>
Release Changelog & History
</Typography>
<Button
href={DESKTOP_RELEASES_URL}
target="_blank"
endIcon={<OpenInNewIcon sx={{ fontSize: '14px !important' }} />}
sx={{ color: d3roPalette.accent.light, fontSize: '13px', textTransform: 'none' }}
>
Forgejo Releases
</Button>
</Box>
{/* Latest release item */}
<Paper
elevation={0}
sx={{
p: 3.5,
borderRadius: '16px',
bgcolor: 'color-mix(in srgb, var(--d3-bg-app) 65%, transparent)',
borderLeft: '4px solid var(--d3-tag-green)',
border: '1px solid var(--d3-overlay-strong)',
borderLeftWidth: '4px',
mb: 3,
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Typography sx={{ fontWeight: 600, fontSize: '18px', color: 'var(--d3-text-inverse)', fontFamily: 'monospace' }}>
v{DESKTOP_VERSION}
</Typography>
<Chip
label="LATEST STABLE · OFFICIALLY VERIFIED"
size="small"
sx={{
bgcolor: 'color-mix(in srgb, var(--d3-tag-green) 15%, transparent)',
color: d3roPalette.tag.green,
border: '1px solid var(--d3-status-success)',
fontWeight: 500,
}}
/>
<Typography sx={{ color: d3roPalette.text.label, fontSize: '12px', fontFamily: 'monospace' }}>{DESKTOP_RELEASE_DATE}</Typography>
</Box>
<Button
component="a"
href={DESKTOP_WINDOWS_INSTALLER_URL}
size="small"
startIcon={<DownloadIcon />}
sx={{ color: d3roPalette.accent.light, textTransform: 'none', fontWeight: 500 }}
>
Download v{DESKTOP_VERSION} (.exe)
</Button>
</Box>
<Typography sx={{ color: d3roPalette.text.primary, fontSize: '13px', lineHeight: 1.6, mb: 2 }}>
• <strong>Multi-Platform Cross-Device Architecture</strong>: Synchronized ecosystem spanning Electron desktop, Next.js cloud console, and React Native mobile.<br />
• <strong>Canonical Forgejo Auto-Update & Policy SSOT</strong>: Fully automated, cryptographic release updates with delta installer support and remote kill switches.<br />
• <strong>Enterprise Red-Team Hardened Voice Engine</strong>: 18/18 headless & headful integration scenarios verified with 100% fail-closed auth security.<br />
• <strong>Offline-First Privacy Intelligence</strong>: Local Whisper Large-v3-Turbo with zero-latency push-to-talk transcription.
</Typography>
<Box
sx={{
p: 1.5,
borderRadius: '8px',
bgcolor: 'var(--d3-scrim)',
fontFamily: 'monospace',
fontSize: '11px',
color: d3roPalette.text.secondary,
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: 'space-between',
gap: 1,
}}
>
<a href={`${DESKTOP_FEED_URL}/latest.yml`} target="_blank" rel="noreferrer">
{DESKTOP_WINDOWS_INSTALLER_FILENAME} · latest.yml
</a>
<span>Windows x64 · NSIS installer</span>
</Box>
</Paper>
{/* v1.0.0 Release Item */}
<Paper
elevation={0}
sx={{
p: 3.5,
borderRadius: '16px',
bgcolor: 'color-mix(in srgb, var(--d3-bg-app) 65%, transparent)',
borderLeft: '4px solid var(--d3-accent-light)',
border: '1px solid var(--d3-overlay-strong)',
borderLeftWidth: '4px',
mb: 3,
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Typography sx={{ fontWeight: 600, fontSize: '18px', color: 'var(--d3-text-inverse)', fontFamily: 'monospace' }}>
v1.0.0
</Typography>
<Chip
label="ARCHIVED · DOWNLOAD UNAVAILABLE"
size="small"
sx={{
bgcolor: 'color-mix(in srgb, var(--d3-accent-light) 15%, transparent)',
color: d3roPalette.accent.light,
border: '1px solid var(--d3-tag-cyan)',
fontWeight: 500,
}}
/>
<Typography sx={{ color: d3roPalette.text.label, fontSize: '12px', fontFamily: 'monospace' }}>2026-08-20</Typography>
</Box>
<Button
disabled
size="small"
startIcon={<DownloadIcon />}
sx={{ color: d3roPalette.accent.light, textTransform: 'none', fontWeight: 500 }}
>
Historical binary unavailable
</Button>
</Box>
<Typography sx={{ color: d3roPalette.text.primary, fontSize: '13px', lineHeight: 1.6, mb: 2 }}>
• <strong>10+ Ad Mediation Engine</strong>: Real-time header bidding auction (EthicalAds, Carbon, GAM, Playwire, AppLovin, Unity).<br />
• <strong>Rewarded Video Token Refills</strong>: Watch 15s sponsored video to gain +50 Cloud AI tokens.<br />
• <strong>Forgejo CI/CD & Synology NAS Packaging</strong>: Multi-platform automated packaging and Docker CRM.<br />
• <strong>100% Local Whisper Large-v3-Turbo</strong>: Zero-latency offline speech transcription.
</Typography>
<Box
sx={{
p: 1.5,
borderRadius: '8px',
bgcolor: 'var(--d3-scrim)',
fontFamily: 'monospace',
fontSize: '11px',
color: d3roPalette.text.secondary,
display: 'flex',
justifyContent: 'space-between',
}}
>
<span>This entry is retained only as history. No installer, fixed hash, or size is offered as a current release.</span>
</Box>
</Paper>
</Box>
</Container>
</Box>
)
}

View file

@ -1,4 +0,0 @@
// apps/web/src/app/releases/page.tsx
import DownloadPage from '../download/page'
export default DownloadPage

View file

@ -1,27 +0,0 @@
// apps/web/src/lib/desktop-release.ts
// 데스크톱 공식 릴리스 계약 SSOT.
//
// 설치 파일은 canonical Forgejo feed에서만 배포한다. 웹/사이트 빌드 산출물에는
// 설치 바이너리가 포함되지 않으므로 `/releases/...` 로컬 경로는 배포 환경에서 404다.
export const DESKTOP_VERSION = '1.6.0'
/** 릴리스 게시일. `release/product-version.json`의 releaseDate와 같아야 한다. */
export const DESKTOP_RELEASE_DATE = '2026-09-24'
const FORGEJO_ORIGIN = 'https://git.chanpaca.net'
const FORGEJO_OWNER = 'yunchan'
const FORGEJO_REPO = 'd3ro-voice'
/** Registry 안에서 항상 최신 설치 자산을 가리키는 feed 루트 (updater와 동일). */
export const DESKTOP_FEED_URL = `${FORGEJO_ORIGIN}/api/packages/${FORGEJO_OWNER}/generic/${FORGEJO_REPO}/latest`
export const DESKTOP_WINDOWS_INSTALLER_FILENAME = `D3RO-Voice-Setup-${DESKTOP_VERSION}-x64.exe`
export const DESKTOP_WINDOWS_INSTALLER_URL = `${DESKTOP_FEED_URL}/${DESKTOP_WINDOWS_INSTALLER_FILENAME}`
/** Forgejo Release 허브 (릴리스 노트 + 자산 첨부). */
export const DESKTOP_RELEASE_HUB_URL = `${FORGEJO_ORIGIN}/${FORGEJO_OWNER}/${FORGEJO_REPO}/releases/tag/v${DESKTOP_VERSION}`
/** Release 자산 목록 (버전 아카이브). */
export const DESKTOP_RELEASES_URL = `${FORGEJO_ORIGIN}/${FORGEJO_OWNER}/${FORGEJO_REPO}/releases`