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:
parent
b6fe588a7c
commit
cd9d199dbf
53 changed files with 43 additions and 3278 deletions
|
|
@ -124,12 +124,12 @@ test.describe('Billing provider and Payple DOM flow', () => {
|
|||
})
|
||||
})
|
||||
|
||||
await page.goto('/login')
|
||||
await page.goto('/app/login')
|
||||
await page.getByPlaceholder('user@studio.com').fill(email)
|
||||
await page.getByPlaceholder('••••••••').fill(password)
|
||||
await page.getByRole('button', { name: '로그인', exact: true }).click()
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 15_000 })
|
||||
await page.goto('/billing')
|
||||
await page.goto('/app/billing')
|
||||
|
||||
await expect(page.getByTestId('billing-current-tier')).toHaveText('FREE')
|
||||
await expect(page.getByTestId('billing-current-provider')).toContainText('없음')
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ test.describe('Dashboard, dictionary and command Supabase parity', () => {
|
|||
let otherId = ''
|
||||
|
||||
async function login(page: import('@playwright/test').Page): Promise<void> {
|
||||
await page.goto('/login')
|
||||
await page.goto('/app/login')
|
||||
await page.getByPlaceholder('user@studio.com').fill(ownerEmail)
|
||||
await page.getByPlaceholder('••••••••').fill(password)
|
||||
await page.getByRole('button', { name: '로그인', exact: true }).click()
|
||||
|
|
@ -146,7 +146,7 @@ test.describe('Dashboard, dictionary and command Supabase parity', () => {
|
|||
|
||||
test('dictionary uses RLS CRUD, filters, pagination, duplicate protection and optimistic rollback', async ({ page }) => {
|
||||
await login(page)
|
||||
await page.goto('/dictionary')
|
||||
await page.goto('/app/dictionary')
|
||||
await expect(page.getByText('CUSTOM DICTIONARY')).toBeVisible()
|
||||
await expect(page.getByText(foreignDictionaryWord)).toHaveCount(0)
|
||||
await expect(page.getByText(oldestWord)).toHaveCount(0)
|
||||
|
|
@ -237,7 +237,7 @@ test.describe('Dashboard, dictionary and command Supabase parity', () => {
|
|||
expect(bootstrapA.data).toHaveLength(4)
|
||||
expect(bootstrapB.data).toHaveLength(4)
|
||||
await bootstrapClient.auth.signOut({ scope: 'local' })
|
||||
await page.goto('/commands')
|
||||
await page.goto('/app/commands')
|
||||
await expect(page.getByText('SYNCED COMMANDS (4)')).toBeVisible()
|
||||
await expect(page.getByText('Translate to English', { exact: true }).first()).toBeVisible()
|
||||
await expect(page.getByText('BUILT-IN · READ ONLY').first()).toBeVisible()
|
||||
|
|
|
|||
|
|
@ -90,13 +90,13 @@ test.describe('History Supabase SSOT', () => {
|
|||
})
|
||||
|
||||
test('session, RLS, search, favorite, pagination, detail and mutation rollback work end-to-end', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
await page.goto('/app/login')
|
||||
await page.getByPlaceholder('user@studio.com').fill(ownerEmail)
|
||||
await page.getByPlaceholder('••••••••').fill(password)
|
||||
await page.getByRole('button', { name: '로그인', exact: true }).click()
|
||||
await page.waitForURL(/\/dashboard/, { timeout: 15_000 })
|
||||
|
||||
await page.goto('/history')
|
||||
await page.goto('/app/history')
|
||||
await expect(page.getByText('TRANSCRIPTION HISTORY')).toBeVisible()
|
||||
await expect(page.getByText(foreignTitle)).toHaveCount(0)
|
||||
await expect(page.getByTestId(`history-card-${oldestId}`)).toHaveCount(0)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { SITE_URLS } from '@d3ro/core/web-urls';
|
||||
|
||||
const SCREENSHOT_DIR = path.resolve('C:/Users/encep/.gemini/antigravity/brain/bbff18a3-721d-4c43-989f-1d6964e15be7/screenshots');
|
||||
|
||||
|
|
@ -21,9 +22,9 @@ test.describe.serial('Extreme Red Team - Cycle 4: Web Console & Public Surfaces'
|
|||
});
|
||||
});
|
||||
|
||||
test('RT-14: Web Public Hub - /login, /download, /releases & /accept-invite', async ({ page }) => {
|
||||
test('RT-14: Web Public Hub - /login and the /download redirect to the landing site', async ({ page }) => {
|
||||
// 1. Test /login
|
||||
await page.goto('/login');
|
||||
await page.goto('/app/login');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await expect(page.getByText(/D3RO[- ]VOICE/i)).toBeVisible({ timeout: 10000 });
|
||||
|
|
@ -35,49 +36,10 @@ test.describe.serial('Extreme Red Team - Cycle 4: Web Console & Public Surfaces'
|
|||
path: path.join(SCREENSHOT_DIR, 'rt14_01_web_login.png'),
|
||||
});
|
||||
|
||||
// 2. Test /download
|
||||
await page.goto('/download');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await expect(page.getByText(/OFFICIAL STABLE RELEASE|D3RO Voice Desktop 1.1.0/i).first()).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByText(/Windows|macOS/i).first()).toBeVisible();
|
||||
|
||||
const primaryDownloadBtn = page.getByRole('link', { name: /Download for Windows/i });
|
||||
await expect(primaryDownloadBtn).toBeVisible();
|
||||
await expect(primaryDownloadBtn).toHaveAttribute('href', '/releases/1.1.0/D3RO-Voice-Setup-1.1.0-x64.exe');
|
||||
await expect(primaryDownloadBtn).toHaveAttribute('download', 'D3RO-Voice-Setup-1.1.0-x64.exe');
|
||||
|
||||
const cardDownloadBtn = page.getByRole('link', { name: /Download Setup \(\.exe\)/i });
|
||||
await expect(cardDownloadBtn).toBeVisible();
|
||||
await expect(cardDownloadBtn).toHaveAttribute('href', '/releases/1.1.0/D3RO-Voice-Setup-1.1.0-x64.exe');
|
||||
|
||||
// Verify static release asset HTTP availability
|
||||
const releaseHead = await page.request.head('/releases/1.1.0/D3RO-Voice-Setup-1.1.0-x64.exe');
|
||||
expect(releaseHead.status()).toBe(200);
|
||||
expect(Number(releaseHead.headers()['content-length'])).toBeGreaterThan(100000000);
|
||||
|
||||
// Screenshot download
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, 'rt14_02_web_download.png'),
|
||||
});
|
||||
|
||||
// 3. Test /releases
|
||||
await page.goto('/releases');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await expect(page.getByText(/OFFICIAL STABLE RELEASE|D3RO Voice Desktop 1.1.0/i).first()).toBeVisible({ timeout: 10000 });
|
||||
await expect(page.getByRole('link', { name: /Download for Windows/i })).toBeVisible();
|
||||
|
||||
// Screenshot releases
|
||||
await page.screenshot({
|
||||
path: path.join(SCREENSHOT_DIR, 'rt14_03_web_releases.png'),
|
||||
});
|
||||
|
||||
// 4. Test /accept-invite
|
||||
await page.goto('/accept-invite?token=red-team-bogus-token');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
await expect(page.getByText(/TEAM INVITE|초대/i).first()).toBeVisible({ timeout: 10000 });
|
||||
// 2. /download lives only on the landing site; the web app just redirects there.
|
||||
const downloadRes = await page.request.get('/app/download', { maxRedirects: 0 });
|
||||
expect([307, 308]).toContain(downloadRes.status());
|
||||
expect(downloadRes.headers()['location']).toBe(SITE_URLS.download);
|
||||
|
||||
expect(uncaughtExceptions).toEqual([]);
|
||||
});
|
||||
|
|
@ -98,7 +60,7 @@ test.describe.serial('Extreme Red Team - Cycle 4: Web Console & Public Surfaces'
|
|||
];
|
||||
|
||||
for (const route of protectedRoutes) {
|
||||
await page.goto(route);
|
||||
await page.goto(`/app${route}`);
|
||||
await page.waitForURL(/\/login/, { timeout: 10000 });
|
||||
expect(page.url()).toContain('/login');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,36 +10,31 @@ import { test, expect } from '@playwright/test'
|
|||
|
||||
test.describe('Smoke: unauthenticated access', () => {
|
||||
test('루트(/)는 /login으로 리다이렉트된다 (Supabase 미설정 시)', async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.goto('/app')
|
||||
await page.waitForURL(/\/login/, { timeout: 10000 })
|
||||
expect(page.url()).toContain('/login')
|
||||
})
|
||||
|
||||
test('/login 페이지가 로드되고 D3RO VOICE 로고가 표시된다', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
await page.goto('/app/login')
|
||||
await expect(page.getByText(/D3RO[- ]VOICE/i)).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
|
||||
test('/login에 Google/GitHub OAuth 버튼이 보인다', async ({ page }) => {
|
||||
await page.goto('/login')
|
||||
await page.goto('/app/login')
|
||||
await expect(page.getByRole('button', { name: /Google/i })).toBeVisible()
|
||||
await expect(page.getByRole('button', { name: /GitHub/i })).toBeVisible()
|
||||
})
|
||||
|
||||
test('/dashboard는 미로그인 시 /login으로 리다이렉트', async ({ page }) => {
|
||||
await page.goto('/dashboard')
|
||||
await page.goto('/app/dashboard')
|
||||
await page.waitForURL(/\/login/, { timeout: 10000 })
|
||||
expect(page.url()).toContain('/login')
|
||||
})
|
||||
|
||||
test('/meetings도 미로그인 시 /login으로 리다이렉트', async ({ page }) => {
|
||||
await page.goto('/meetings')
|
||||
await page.goto('/app/meetings')
|
||||
await page.waitForURL(/\/login/, { timeout: 10000 })
|
||||
expect(page.url()).toContain('/login')
|
||||
})
|
||||
|
||||
test('/accept-invite?token=bogus는 페이지 로드 (에러 메시지 표시)', async ({ page }) => {
|
||||
await page.goto('/accept-invite?token=bogus')
|
||||
await expect(page.getByText(/TEAM INVITE/i)).toBeVisible({ timeout: 10000 })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.d3ro.voice",
|
||||
"sha256_cert_fingerprints": [
|
||||
"01:00:19:21:DB:4F:33:40:85:CE:21:E4:B8:DE:CC:BD:71:DA:87:67:C5:6E:3B:59:83:2A:A1:C8:29:EA:0D:AB"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta http-equiv="refresh" content="0; url=/download" />
|
||||
<link rel="canonical" href="https://d3ro.chanpaca.net/download" />
|
||||
<title>D3RO Voice 릴리스 준비</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>D3RO Voice 릴리스 준비</h1>
|
||||
<p>설치 파일은 서명과 업데이트 경로 검증이 끝난 뒤 공식 페이지에서 제공합니다.</p>
|
||||
<p><a href="/download">공식 릴리스 준비 페이지로 이동</a></p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,231 +0,0 @@
|
|||
x64:
|
||||
firstOrDefaultFilePatterns:
|
||||
- '!**/node_modules/**'
|
||||
- '!build{,/**/*}'
|
||||
- '!release/1.0.0{,/**/*}'
|
||||
- out/**/*
|
||||
- '!out/**/*.map'
|
||||
- package.json
|
||||
- '!**/*.{iml,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,suo,xproj,cc,d.ts,mk,a,o,obj,forge-meta,pdb}'
|
||||
- '!**/._*'
|
||||
- '!**/electron-builder.{yaml,yml,json,json5,toml,ts}'
|
||||
- '!**/{.git,.hg,.svn,CVS,RCS,SCCS,__pycache__,.DS_Store,thumbs.db,.gitignore,.gitkeep,.gitattributes,.npmignore,.idea,.vs,.flowconfig,.jshintrc,.eslintrc,.circleci,.yarn-integrity,.yarn-metadata.json,yarn-error.log,yarn.lock,package-lock.json,npm-debug.log,pnpm-lock.yaml,bun.lock,bun.lockb,appveyor.yml,.travis.yml,circle.yml,.nyc_output,.husky,.github,electron-builder.env}'
|
||||
- '!.yarn{,/**/*}'
|
||||
- '!.editorconfig'
|
||||
- '!.yarnrc.yml'
|
||||
nodeModuleFilePatterns:
|
||||
- '**/*'
|
||||
- out/**/*
|
||||
- '!out/**/*.map'
|
||||
nsis:
|
||||
script: |-
|
||||
!include "D:\workspace\D3ROVoice\node_modules\app-builder-lib\templates\nsis\include\StdUtils.nsh"
|
||||
!addincludedir "D:\workspace\D3ROVoice\node_modules\app-builder-lib\templates\nsis\include"
|
||||
!macro _isUpdated _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "updated"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isUpdated `"" isUpdated ""`
|
||||
|
||||
!macro _isForceRun _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "force-run"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isForceRun `"" isForceRun ""`
|
||||
|
||||
!macro _isKeepShortcuts _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "keep-shortcuts"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isKeepShortcuts `"" isKeepShortcuts ""`
|
||||
|
||||
!macro _isNoDesktopShortcut _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "no-desktop-shortcut"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isNoDesktopShortcut `"" isNoDesktopShortcut ""`
|
||||
|
||||
!macro _isDeleteAppData _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "delete-app-data"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isDeleteAppData `"" isDeleteAppData ""`
|
||||
|
||||
!macro _isForAllUsers _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "allusers"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isForAllUsers `"" isForAllUsers ""`
|
||||
|
||||
!macro _isForCurrentUser _a _b _t _f
|
||||
${StdUtils.TestParameter} $R9 "currentuser"
|
||||
StrCmp "$R9" "true" `${_t}` `${_f}`
|
||||
!macroend
|
||||
!define isForCurrentUser `"" isForCurrentUser ""`
|
||||
|
||||
!macro addLangs
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
!insertmacro MUI_LANGUAGE "German"
|
||||
!insertmacro MUI_LANGUAGE "French"
|
||||
!insertmacro MUI_LANGUAGE "SpanishInternational"
|
||||
!insertmacro MUI_LANGUAGE "SimpChinese"
|
||||
!insertmacro MUI_LANGUAGE "TradChinese"
|
||||
!insertmacro MUI_LANGUAGE "Japanese"
|
||||
!insertmacro MUI_LANGUAGE "Korean"
|
||||
!insertmacro MUI_LANGUAGE "Italian"
|
||||
!insertmacro MUI_LANGUAGE "Dutch"
|
||||
!insertmacro MUI_LANGUAGE "Danish"
|
||||
!insertmacro MUI_LANGUAGE "Swedish"
|
||||
!insertmacro MUI_LANGUAGE "Norwegian"
|
||||
!insertmacro MUI_LANGUAGE "Finnish"
|
||||
!insertmacro MUI_LANGUAGE "Russian"
|
||||
!insertmacro MUI_LANGUAGE "Portuguese"
|
||||
!insertmacro MUI_LANGUAGE "PortugueseBR"
|
||||
!insertmacro MUI_LANGUAGE "Polish"
|
||||
!insertmacro MUI_LANGUAGE "Ukrainian"
|
||||
!insertmacro MUI_LANGUAGE "Czech"
|
||||
!insertmacro MUI_LANGUAGE "Slovak"
|
||||
!insertmacro MUI_LANGUAGE "Hungarian"
|
||||
!insertmacro MUI_LANGUAGE "Arabic"
|
||||
!insertmacro MUI_LANGUAGE "Turkish"
|
||||
!insertmacro MUI_LANGUAGE "Thai"
|
||||
!insertmacro MUI_LANGUAGE "Vietnamese"
|
||||
!macroend
|
||||
|
||||
!addincludedir "D:\workspace\D3ROVoice\apps\desktop\build"
|
||||
!include "D:\workspace\D3ROVoice\apps\desktop\build\installer.nsh"
|
||||
!include "C:\Users\encep\AppData\Local\Temp\t-1IM43R\2-messages.nsh"
|
||||
!addplugindir /x86-unicode "C:\Users\encep\AppData\Local\electron-builder\Cache\nsis\nsis-resources-3.4.1-nsis-resources-3.4.1\plugins\x86-unicode"
|
||||
|
||||
Var newStartMenuLink
|
||||
Var oldStartMenuLink
|
||||
Var newDesktopLink
|
||||
Var oldDesktopLink
|
||||
Var oldShortcutName
|
||||
Var oldMenuDirectory
|
||||
|
||||
!include "common.nsh"
|
||||
!include "MUI2.nsh"
|
||||
!include "multiUser.nsh"
|
||||
!include "allowOnlyOneInstallerInstance.nsh"
|
||||
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
!ifmacrodef customUnInstallSection
|
||||
!define MUI_COMPONENTSPAGE_NODESC
|
||||
!insertmacro MUI_UNPAGE_COMPONENTS
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!ifdef INSTALL_MODE_PER_ALL_USERS
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
RequestExecutionLevel user
|
||||
!else
|
||||
RequestExecutionLevel admin
|
||||
!endif
|
||||
!else
|
||||
RequestExecutionLevel user
|
||||
!endif
|
||||
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
SilentInstall silent
|
||||
!else
|
||||
Var appExe
|
||||
Var launchLink
|
||||
!endif
|
||||
|
||||
!ifdef ONE_CLICK
|
||||
!include "oneClick.nsh"
|
||||
!else
|
||||
!include "assistedInstaller.nsh"
|
||||
!endif
|
||||
|
||||
!insertmacro addLangs
|
||||
|
||||
!ifmacrodef customHeader
|
||||
!insertmacro customHeader
|
||||
!endif
|
||||
|
||||
Function .onInit
|
||||
Call setInstallSectionSpaceRequired
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
${LogSet} on
|
||||
|
||||
!ifmacrodef preInit
|
||||
!insertmacro preInit
|
||||
!endif
|
||||
|
||||
!ifdef DISPLAY_LANG_SELECTOR
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
!endif
|
||||
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
WriteUninstaller "${UNINSTALLER_OUT_FILE}"
|
||||
!insertmacro quitSuccess
|
||||
!else
|
||||
!insertmacro check64BitAndSetRegView
|
||||
|
||||
!ifdef ONE_CLICK
|
||||
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
|
||||
!else
|
||||
${IfNot} ${UAC_IsInnerInstance}
|
||||
!insertmacro ALLOW_ONLY_ONE_INSTALLER_INSTANCE
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!insertmacro initMultiUser
|
||||
|
||||
!ifmacrodef customInit
|
||||
!insertmacro customInit
|
||||
!endif
|
||||
|
||||
!ifmacrodef addLicenseFiles
|
||||
InitPluginsDir
|
||||
!insertmacro addLicenseFiles
|
||||
!endif
|
||||
!endif
|
||||
FunctionEnd
|
||||
|
||||
!ifndef BUILD_UNINSTALLER
|
||||
!include "installUtil.nsh"
|
||||
!endif
|
||||
|
||||
Section "install" INSTALL_SECTION_ID
|
||||
!ifndef BUILD_UNINSTALLER
|
||||
# If we're running a silent upgrade of a per-machine installation, elevate so extracting the new app will succeed.
|
||||
# For a non-silent install, the elevation will be triggered when the install mode is selected in the UI,
|
||||
# but that won't be executed when silent.
|
||||
!ifndef INSTALL_MODE_PER_ALL_USERS
|
||||
!ifndef ONE_CLICK
|
||||
${if} $hasPerMachineInstallation == "1" # set in onInit by initMultiUser
|
||||
${andIf} ${Silent}
|
||||
${ifNot} ${UAC_IsAdmin}
|
||||
ShowWindow $HWNDPARENT ${SW_HIDE}
|
||||
!insertmacro UAC_RunElevated
|
||||
${Switch} $0
|
||||
${Case} 0
|
||||
${Break}
|
||||
${Case} 1223 ;user aborted
|
||||
${Break}
|
||||
${Default}
|
||||
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate, error $0"
|
||||
${Break}
|
||||
${EndSwitch}
|
||||
Quit
|
||||
${else}
|
||||
!insertmacro setInstallModePerAllUsers
|
||||
${endIf}
|
||||
${endIf}
|
||||
!endif
|
||||
!endif
|
||||
!include "installSection.nsh"
|
||||
!endif
|
||||
SectionEnd
|
||||
|
||||
Function setInstallSectionSpaceRequired
|
||||
!insertmacro setSpaceRequired ${INSTALL_SECTION_ID}
|
||||
FunctionEnd
|
||||
|
||||
!ifdef BUILD_UNINSTALLER
|
||||
!include "uninstaller.nsh"
|
||||
!endif
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
version: 1.0.0
|
||||
files:
|
||||
- url: D3RO-Voice-Setup-1.0.0-x64.exe
|
||||
sha512: Ae4ZDaqL6mtkpyGsX9rna6WT3hhtChiEF4hl+Yx1CQvjCPwTq7LmhcBEm40TvRHOVtjQSASsCTs1wxVgSRo1Ow==
|
||||
size: 102123822
|
||||
path: D3RO-Voice-Setup-1.0.0-x64.exe
|
||||
sha512: Ae4ZDaqL6mtkpyGsX9rna6WT3hhtChiEF4hl+Yx1CQvjCPwTq7LmhcBEm40TvRHOVtjQSASsCTs1wxVgSRo1Ow==
|
||||
releaseDate: '2026-08-20T01:56:59.299Z'
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
// apps/web/src/app/releases/page.tsx
|
||||
import DownloadPage from '../download/page'
|
||||
|
||||
export default DownloadPage
|
||||
|
|
@ -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`
|
||||
Loading…
Add table
Add a link
Reference in a new issue