Installing the previous build left an app that could not update itself: the
packaging path used to guarantee the native module build does not create the
updater configuration file, so the update client had nothing to read.
That file is now written from the single feed source and its presence in the
packaged app is checked before anything is published, so an installer that
cannot update can no longer be released.
The released installer could not start: it carried a better-sqlite3 build for the
host Node runtime instead of Electron, so the app died immediately with a module
version mismatch when it opened its database.
Packaging now proves the Electron build of every runtime-sensitive native module
before an installer or archive exists, and installers are produced only from that
verified tree, so the mistake cannot pass silently. The release pipelines run the
same check.
The default local model also pointed at a retired model: a *.gguf name that
Ollama cannot serve, while the settings, onboarding, and guide screens
recommended an older model. All of them now use the model the service code
already preferred.
Auto-update could not work at all: the installer was 189 MB because it carried
the local speech engine and ffmpeg, and the download feed rejects uploads over
about 100 MiB, so update metadata could never be published.
The installer now leaves those components out and the app fetches them the first
time they are needed, verifying every part and the joined archive before
installing. The installer is 90.6 MiB, the update feed is published again, and
updates stay small because the engine is not re-sent on every release.
The fetch is visible and recoverable: the download runs with progress, a failed
install cleans up after itself, and Settings > STT shows the runtime status with
a manual download action for when the automatic one cannot run.
The manual install path still needed 7-Zip, which the target machine does not
have, so "installable without a certificate" was not yet true.
The channel now also publishes the app as byte-split zip parts, and the install
script joins them and extracts with the built-in Windows Expand-Archive after
verifying every part and the joined archive. Version 1.3.1 republishes the
channel from a single build, because a version's artifacts can only match one
build and published volumes are never overwritten.
Local dictation had never produced a transcript on an installed build. The
engine itself was healthy; every connection to it was broken.
Installed builds shipped no speech engine at all: the packaging config had no
entry for the faster-whisper sidecar and no pipeline step built one, so the app
always fell back to a system Python without the runtime. Development was broken
too, because the sidecar and SoX paths were resolved against the Vite output
directory instead of the app root, which also meant recording failed with a SoX
ENOENT. On hosts where localhost resolves only to IPv6, every local request was
refused outright, which silently disabled both local transcription and the local
LLM.
The sidecar is now built and bundled (including the Silero VAD data it needs),
gated by a packaging check that fails when the engine or its data is missing.
Paths are discovered from the app root and fail loudly when the engine is
absent. Local engine URLs are normalized to the IPv4 loopback, decoding is tuned
so repeated hallucinations cannot compound (the same transcript now takes about
a fifth of the time), the engine is warmed up at startup, and holding the hotkey
now shows the text forming live in the recording tip.
Version 1.1.0 was published on 2026-09-15, so its tag is closed. Every commit
since then, the update-feed rework, dictionary import and export, the extra
push transports, and entitlement gating, needs its own immutable version.
Product version, Android version code, iOS build number, and all package and
store surfaces move to 1.2.0 / 1020001, with Korean and English store notes for
the new version code. The download centers read that version and its release
date from one place instead of repeating them inline.
Desktop, web, mobile, and the API each decided locally what a tier could do,
so a plan change could unlock a feature on one surface and not another.
Entitlement checks now live in `@d3ro/core` and are exercised by tests.
The shared theme and design-system packages also gain the tokens the new
surfaces consume, and the api-client exposes the dictionary and team types
the clients now send.
- press가 STT 초기화를 await하며 action queue 점유 → release 수십 초 지연·
유령 세션 반복 버그 수정 (initSTT fire-and-forget)
- 프리플라이트: STT 모델 미설치 시 즉시 에러 + 메인 UI 경고 + 온보딩 오픈
- 사이드카: 기동 중 프로세스 사망 시 30초 대기 없이 즉시 실패,
restartCount 리셋, error 리스너 부재 미처리 예외 방지
- 실시간 부분 전사: 1.5s 간격 interim → RecordingTip에 말하는 내용 미리보기
- Ollama 미가용 후처리 스킵 시 warning 배너, voice:error 브로드캐스트 신설
- 앱 아이콘: icon.svg 마스터(d3ro 브랜드 — 메탈 섀시+오렌지 웨이브) → png/ico
생성 스크립트(generate-icons.mjs, sharp) + electron-builder win/mac 연결
- 트레이: createEmpty() 빈 아이콘 → 실제 앱 아이콘 (getAppIconPath)
- 마이크 테스트: 초기 무음 level:0을 종료로 오인하던 조기 종료 버그 —
AUDIO.TEST_LEVEL에 done 플래그 신설(SSOT), STOP 버튼이 실제 캡처 중지,
testDevice 실패 시 상태 롤백
- LicenseTab/LicenseModal: LemonSqueezy 키 입력 삭제, Payple 구독 UI로 전환
- useLicenseState 훅: 라이선스+클라우드 인증 공용 상태 관리 추출
- PREMIUM_MODEL_LIMITS: 3곳 중복 → @d3ro/core/constants 단일 소스
- i18n: LemonSqueezy 전용 키 18개 삭제, 구독 관련 키 13개 추가 (12 locale)
- DashboardPage: dashboard.model* → license.model* 키 통일
listening 상태에서 풀 몰입 계측기 모드로 전환되는 VoiceRecordingPanel 추가.
recording-tip 팝업의 9바 cos-분포 waveform(BAR_COUNT=9, SMOOTHING=0.5,
RANDOM_FACTOR=0.35, 100ms)을 React로 포팅해 REC LED + elapsed 타이머 +
"SPEAK NOW" 힌트까지 구성. thinking/speaking 상태에서는 메시지 리스트로
복귀해 대화 맥락 유지 + 점 3개 typing indicator 버블 추가.
VoiceConversationService에 AudioCaptureService audio-level forwarding과
사운드 훅 4개(recording-start / recording-stop / chime / error)를 삽입.
chime은 recording-stop.wav 재사용(SoundEffectService SoundName 확장).
VOICE_CONVERSATION.AUDIO_LEVEL 채널 신설 + preload onAudioLevel API.
U8 Bug 13 동반 해소: finishListening에서 minBytes 미달 또는 VAD 무음 판정으로
빈 텍스트가 나오는 경우 조용히 listening으로 복귀하던 것을 _emitError('stt')로
사용자 피드백(에러 사운드 + 에러 이벤트)을 노출하도록 수정. 사용자가 "⏹ 눌러도
반응 없음"으로 오해하던 증상 해소.
Phase 3 재정의: Supabase = source of truth 방향 철회.
로컬 entry point 철학 하에 "로컬이 SoT, 클라우드는 로그인 시 mirror"로 확정.
CloudSyncService._initialSync():
- _onAuthenticated 끝에 fire-and-forget 호출
- pushAll → pullAll 순차 실행
- signin: 익명 로컬 데이터를 사용자 계정으로 업로드
- restore: 다른 기기 변경 반영
- 실패 시 warn만, 수동 Sync 버튼으로 재시도 가능
types.ts:
- FeatureAccess.reason: 'login_required' 추가
- UpgradePromptEvent.reason: 'login_required' 추가
LicenseService:
- consumeQuota(): login_required 케이스 — promptUpgrade + D3ROError(TierRequired)
- promptUpgrade(): login_required 시 requiredTier='free'
(로그인만 하면 free로 바로 사용 가능)
- quota_exceeded 시 현재 tier 상위로 승격 제안
UpgradePromptModal:
- isLoginRequired 분기 — title/desc 교체
- Primary button: "로그인하기" → d3ro:open-settings event (tab=cloud)
i18n ko/en:
- license.loginRequired.title/desc/signIn 추가
묶음 A — UI 정리 + i18n 완성:
[A1] desktop SettingsModal에 CloudSyncSection 통합 (탭 6번째)
- CloudIcon import, settings.tabs.cloud 키 추가
- 기존 about 탭은 index 5 -> 6
[A2] apps/web Sidebar 공유 layout 리팩터링
- app/(app)/layout.tsx 신규 (route group)
- dashboard/meetings/record/teams/billing을 (app)/ 아래로 git mv
- app/(app)/layout.tsx에 auth 가드 + Sidebar 통합
- 기존 개별 page.tsx에서 Sidebar/auth 중복 제거
- app/(app)/dashboard/layout.tsx 제거 (루트 layout이 처리)
[A3] 11개 locale에 V2 새 키 추가 (en/ja/zh/zh-TW/es/fr/de/pt/ru/vi/th)
- nav.meetings/record/teams/billing/logout
- login.subtitle/google/github/terms
- settings.tabs.cloud
묶음 B — V2-4b pull 동기화:
- CloudSyncService.pullAll() 신규
- history/dictionary 테이블 원격에서 fetch
- last_sync_at 이후 updated_at만 필터
- Last-Write-Wins 충돌 해결 (remote.updated_at > local.updated_at)
- 로컬에 없는 행은 INSERT, 있는 행은 UPDATE (구체적 컬럼 지정)
- IPC CLOUD_SYNC.PULL_ALL 채널 + handler + preload api
- CloudSyncSection에 Pull 버튼 추가 (Push 옆에 위치)
묶음 C — V2-8b Stripe webhook 서명 검증:
- stripe-webhook Edge Function에 Web Crypto API 기반 HMAC-SHA256 검증
- Stripe-Signature 헤더 파싱 (t=, v1= 엔트리)
- Replay 방지 (timestamp tolerance 300초)
- constantTimeEqual로 타이밍 공격 방지
- crypto.subtle.importKey/sign으로 HMAC 계산
- stripe-portal Edge Function 신규 (Customer Portal)
- JWT 인증 -> 기존 customer_id 조회 -> billing_portal/sessions 생성
- return_url 지원
- apps/web/components/billing/portal-button.tsx (구독 관리 버튼)
- billing 페이지에 Free 외 tier 사용자에게 PortalButton 표시
- config.toml에 stripe-portal 함수 등록 (verify_jwt=true)
묶음 D — V2-6b packages/ui-native:
- @d3ro/ui-native 신규 패키지 (React Native 전용 DS)
- theme.ts: d3roNativePalette/Typo/Radius (MUI 없는 정적 값)
- components/MetalCard.tsx: View + 섀시 섀도우
- components/PhosphorText.tsx: Text + 앰버 glow (textShadow)
- components/Led.tsx: View 원 + glow
- components/PhysicalButton.tsx: Pressable + 누름 느낌
- React/React-Native는 peerDependencies
- apps/mobile/package.json에 @d3ro/ui-native를 file: 의존성으로 추가
(mobile은 npm workspace 제외이므로 file path 필요)
검증:
- desktop typecheck OK
- web typecheck OK
- web next build OK (11 라우트, (app) 그룹 반영)
- 회귀 없음