Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ef021bb
fix: harden PowerShell native host registration
esleeeeee Jul 21, 2026
5098349
fix: make native messaging diagnostics fail open
esleeeeee Jul 21, 2026
19fba5a
fix: render WinUI content per-monitor responsively
esleeeeee Jul 21, 2026
2c8f84a
docs: record Whale and DPI verification
esleeeeee Jul 21, 2026
dea36fe
fix: track browser cancellation and routing states
esleeeeee Jul 22, 2026
edb0637
feat: add per-download folder selection and history controls
esleeeeee Jul 22, 2026
63b0ab5
fix: balance shared page spacing and viewport width
esleeeeee Jul 22, 2026
facabef
fix: refresh active selection prompts safely
esleeeeee Jul 22, 2026
8e83b74
fix: keep cancellation file names stable
esleeeeee Jul 22, 2026
dd459f1
docs: record SelectSubfolder Whale verification
esleeeeee Jul 22, 2026
5d4a074
feat: extend routing jobs and live metadata
esleeeeee Jul 22, 2026
1998cd4
feat: add hierarchical picker and tray workflows
esleeeeee Jul 22, 2026
2a4e558
feat: package per-user installer
esleeeeee Jul 22, 2026
a36326b
test: add installed Whale validation tools
esleeeeee Jul 22, 2026
85fc571
docs: record installed background workflow
esleeeeee Jul 22, 2026
e664803
Fix pending prompts, cancellation, version, and themes
esleeeeee Jul 22, 2026
49c87eb
Preserve existing settings when saving theme
esleeeeee Jul 22, 2026
59adffc
Avoid duplicate source revision in product version
esleeeeee Jul 22, 2026
a60ea32
Apply theme-aware window backgrounds
esleeeeee Jul 22, 2026
f4a2fee
Use theme-aware card surfaces
esleeeeee Jul 22, 2026
2e2f528
Match window chrome to the selected theme
esleeeeee Jul 22, 2026
40e7644
Keep startup reconciliation from refreshing prompt age
esleeeeee Jul 22, 2026
55bce83
Document cancellation and reconciliation verification
esleeeeee Jul 22, 2026
ed0aacc
Fix minimized selection window foreground activation
esleeeeee Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ TestResults/

# Node
node_modules/
build-tests/
*.tsbuildinfo

# Local application data
Expand Down
54 changes: 34 additions & 20 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ chrome.downloads.onCreated/onChanged

### Extension

Manifest V3 service worker이며 권한은 `downloads`, `nativeMessaging`뿐입니다. `onCreated`에서 referrer, 최초/최종 파일 URL을 별도 필드로 전달하고 `onChanged`에서 완료 또는 중단 상태를 전송합니다. 연결 오류는 다운로드를 취소하거나 변경하지 않습니다.
Manifest V3 service worker이며 권한은 `downloads`, `nativeMessaging`뿐입니다. `onCreated`에서 referrer, 최초/최종 파일 URL을 별도 필드로 전달하고, `onChanged`의 `filename` delta를 같은 Job의 메타데이터 갱신으로 전송합니다. `USER_CANCELED` error delta는 state delta를 기다리지 않고 즉시 idempotent `download.cancelled`로 전송합니다. interrupted는 `downloads.search({ id })` 후 delta error → item error → 마지막 error 순서로 원인을 결정합니다. `onErased`는 기록 삭제 진단일 뿐 취소가 아닙니다. 시작 재조정은 complete/cancelled/interrupted/in_progress/stale을 구분하고 item 누락을 취소로 추측하지 않습니다. 연결 로그에는 정제된 ID/state/error/전송 결과만 기록합니다.

Chromium downloads API에는 신뢰할 수 있는 시작 탭 URL 필드가 없습니다. 따라서 활성 탭을 다운로드 출처로 추측하지 않고 `initiatingPageUrl`은 근거가 있을 때만 사용합니다. 현재 확장은 referrer를 우선 근거로 전달합니다.

### Native Host

브라우저가 실행하는 짧은 수명의 브리지입니다. 고정 개발 확장 origin, 프로토콜 버전, 비어 있지 않은 request ID, 명령 allowlist, 최대 1 MiB 메시지를 검사합니다. 셸 문자열을 실행하지 않으며 장기 상태나 UI가 없습니다. Agent가 없으면 같은 설치 디렉터리의 정확한 `DownloadRouter.Agent.exe`만 시작합니다.
브라우저가 실행하는 짧은 수명의 브리지입니다. 고정 개발 확장 origin, 프로토콜 버전, 비어 있지 않은 request ID, 명령 allowlist, 최대 1 MiB 메시지를 검사합니다. 임의 셸 문자열을 실행하지 않으며 장기 상태나 UI가 없습니다. Agent가 없으면 같은 설치 디렉터리의 정확한 `DownloadRouter.Agent.exe`만 숨김 프로세스로 분리 실행해 Native Messaging stdin/stdout/stderr를 상속하지 않게 합니다.

### Agent

현재 사용자 범위 mutex로 단일 인스턴스를 보장합니다. 여러 Native Host 연결을 비동기 Named Pipe 서버로 병합합니다. 명령 처리기는 DB 오류와 경계 오류를 안전한 오류 응답으로 바꾸며, 규칙이 없을 때 작업을 만들지 않습니다.
현재 사용자 범위 mutex로 단일 인스턴스를 보장합니다. 여러 Native Host 연결을 비동기 Named Pipe 서버로 병합합니다. 명령 처리기는 DB 오류와 경계 오류를 안전한 오류 응답으로 바꾸며, 규칙이 없을 때 작업을 만들지 않습니다. SelectSubfolder 작업 생성 시 설치 폴더 또는 개발 출력의 App만 실행 요청하며 실패해도 브라우저 다운로드와 원본 위치를 바꾸지 않습니다.

### Core

Expand All @@ -40,7 +40,7 @@ Chromium downloads API에는 신뢰할 수 있는 시작 탭 URL 필드가 없
- IDN 호스트 정규화와 도메인 경계 비교
- 경로 토큰 해석과 루트 내부 검증
- URL 민감정보 제거
- 명시적 작업 상태 머신
- 브라우저 전송과 라우팅 결정을 분리한 명시적 상태 머신

### Infrastructure

Expand All @@ -56,42 +56,56 @@ SQLite 마이그레이션은 `schema_migrations`, `rules`, `download_jobs`, `job

### WinUI 3 App

Agent와 동일한 Core 라이브러리를 참조하지만 DB나 파일 이동 구현을 직접 호출하지 않고 Named Pipe 명령으로 통신합니다. 현재 대시보드, 규칙, 이력, 규칙별 선택 대기 그룹, 브라우저 연결, 일반 설정, 진단, 정보 화면 골격이 있습니다.
Agent와 동일한 Core 라이브러리를 참조하지만 DB나 파일 이동 구현을 직접 호출하지 않고 Named Pipe 명령으로 통신합니다. App은 사용자 범위 mutex로 단일 인스턴스를 유지하는 트레이 호스트이며 `--background`에서는 메인 창을 표시하지 않습니다. 500ms 간격으로 Pending을 읽되 자동 팝업 정책을 통과한 항목만 중복 없는 FIFO `FolderSelectionWindow`로 표시합니다. 선택창은 owner 없는 독립 top-level HWND입니다. 표시 때 선택창 HWND만 `SW_SHOWNORMAL`, XAML activate, topmost/bring-to-top, foreground 순으로 처리하고, Windows foreground 제한이 직접 호출을 거부하면 현재 foreground thread와 입력 큐를 잠시 연결해 선택창을 다시 활성화합니다. MainWindow에는 restore를 호출하지 않으며 FlashWindowEx는 모든 활성화 시도가 실패했을 때만 사용합니다.

선택창 전면 진단은 MainWindow와 선택창의 HWND, visible, iconic, owner, foreground 및 Show/Bring/SetForeground 결과만 기록합니다. 다운로드 URL, query, token, 전체 로컬 경로는 기록하지 않습니다.

공통 `FolderTreePicker`는 루트 하나만 먼저 만들고 노드 확장 시 해당 단계의 자식만 비동기로 읽습니다. 로드된 노드를 중복 조회하지 않고, 접근 불가 항목은 노드 단위 오류로 제한하며 reparse point는 선택 경계에서 제외합니다. 팝업, 선택 대기, 이력 경로 변경이 같은 컴포넌트와 Agent 명령을 사용합니다. 규칙의 저장 루트는 경계가 아직 정해지지 않은 선택이므로 HWND로 초기화한 Windows `FolderPicker`를 사용합니다.

앱은 unpackaged WinUI 3 프로세스를 manifest에서 Per-Monitor V2로 선언합니다. 크기와 여백은 장치 독립 픽셀(DIP)을 사용하고 루트에서 layout rounding을 적용합니다. 모든 화면은 하나의 `NavigationView -> vertical ScrollViewer -> stretch viewport -> MaxWidth 1100 form` 구조를 공유합니다. 실제 폼 폭은 `ViewportWidth - 좌우 Padding`과 1100 DIP 중 작은 값이며 가로 스크롤을 만들지 않습니다. Compact는 `16,32,16,24`, Wide는 `32,48,32,32` DIP 패딩을 사용합니다.

## 작업 상태

```text
WaitingForDownload
WaitingForSelection
ReadyToMove
Moving
Completed
RetryPending
Interrupted
Cancelled
Failed
BrowserTransferState RoutingState
InProgress WaitingForSelection
Complete SelectionReady
Cancelled Moving
Interrupted RetryPending
Completed
Skipped
Failed
NotRequired
```

상태 전이는 `DownloadJobStateMachine`이 검사합니다. 완료 전에 사용자가 선택하면 `WaitingForDownload`, 완료 후 선택하면 `ReadyToMove`를 거쳐 동일한 이동 경로를 사용합니다.
두 축의 상태 전이는 `DownloadJobStateMachine`이 각각 검사합니다. 완료 전에 선택하면 `InProgress / SelectionReady`만 저장하고 파일을 이동하지 않습니다. `Complete / SelectionReady`가 된 뒤에만 Moving으로 전이합니다. 사용자 취소는 BrowserTransferState를 `Cancelled`로 바꾸고 WaitingForSelection/SelectionReady는 `NotRequired`로 종료합니다. Skipped/Failed 같은 이미 확정된 진단 라우팅은 보존할 수 있으며, 표시·Pending·이동 가능 여부는 항상 BrowserTransferState를 우선합니다. 다른 중단은 일반적으로 `Interrupted / Failed`입니다. 기존 단일 `Status` 열은 마이그레이션과 호환 표시용 파생 값으로 유지합니다.

## 저장 위치 선택 경계

저장 루트를 정규화한 뒤 상대 경로를 결합하고 다시 루트 내부인지 확인합니다. `..`, 루트 경로 자체 변경, reparse point 통과를 거부합니다. App은 reparse point 디렉터리를 열거하지 않습니다. 보안 결정은 UI가 아니라 Agent에서도 다시 검증합니다.

## 팝업 수명과 테마

자동 선택 팝업은 `SelectionPromptPolicy.AutoPromptWindow`의 30분 안에 생성되었거나 브라우저 이벤트가 갱신된 SelectSubfolder 대기 Job만 대상으로 합니다. 오래되거나 브라우저 record가 stale인 Job은 DB/대기 탭/InfoBadge에 남고 자동 큐에는 들어가지 않습니다. `모두 나중에 선택`은 메모리의 현재 큐 ID만 세션 숨김 집합으로 옮기므로 RoutingState와 파일은 바뀌지 않고 이후 새 ID는 다시 큐에 들어옵니다.

`ThemeManager`는 App 시작 때 `config.local.json`의 System/Light/Dark를 정규화하고 모든 Window 루트 FrameworkElement를 등록합니다. 설정 변경은 등록된 열린 Window 전체에 적용되고 새 FolderSelectionWindow는 Content 지정 직후 등록됩니다. System은 `ElementTheme.Default`입니다. UI 설정 저장은 SQLite 이력 스키마와 분리합니다.

## 배포

- 앱, Agent, Native Host: Windows x64 self-contained publish
- 확장: `npm ci` 후 TypeScript compile, manifest 복사, ZIP 생성
- Native Host: HKCU 브라우저별 registry adapter와 `%LOCALAPPDATA%` manifest
- Installer: per-user, `PrivilegesRequired=lowest`
- 버전: `Directory.Build.props` VersionPrefix를 App/Agent/Native Host assembly와 Installer AppVersion의 단일 원본으로 사용
- 자동 시작: `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`에 설치 App의 정확한 따옴표 경로와 `--background`
- X 버튼: 기본은 AppWindow 숨김, 트레이 `종료` 또는 `--shutdown`만 App/Agent 정상 종료
- 제거: 설치 파일·자동 시작·Native Host만 제거하고 사용자 DB와 규칙은 보존

설치 페이로드와 PC별 manifest는 커밋하지 않습니다.

## 후속 설계 항목

- Agent가 선택 대기 알림/창을 활성화하는 방식
- 새 폴더 만들기와 새로 고침을 포함한 전용 트리 선택기
- Windows 시작 시 실행 설정 저장 및 등록
- 세션을 넘는 “나중에 선택” 알림 정책과 새 폴더 만들기
- 중단된 작업의 시작 시 복구 워커
- 브라우저 adapter의 실제 레지스트리/실행 경로 검증
- 설치 서명과 업데이트 전략
- Whale 이외 브라우저 adapter의 실제 레지스트리/실행 경로 검증
- 설치 코드 서명과 업데이트 채널
77 changes: 72 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,90 @@

### Added

- 선택창 전면 표시 전후의 MainWindow/FolderSelectionWindow HWND, visible, iconic, owner, foreground와 native 호출 결과를 남기는 경로·URL 비포함 진단
- 숨김/최소화 MainWindow, iconic 선택창 복원, foreground fallback, FIFO 취소 제거를 검증하는 Core 회귀 테스트
- 30분 자동 팝업 정책, 이전 세션 대기 작업 안내와 선택 창의 `모두 나중에 선택` 큐 일괄 숨김
- Extension `download.cancelled`/`download.interrupted`, 시작 시 complete/interrupted/in_progress/stale 재조정과 정제된 연결 진단
- 전역 `ThemeManager`, System/Light/Dark 사용자 설정 저장과 열린/새 Window 즉시 적용
- 정보 화면 제품명·0.3.1 버전·commit·설치형/개발 빌드 표시, 데이터 폴더/GitHub 열기
- `Directory.Build.props` 단일 버전 원본과 App/Agent/Native Host/Installer 일치 검증

- .NET 10/WinUI 3 모노레포와 재현 가능한 bootstrap/build/test/publish 스크립트
- Manifest V3 TypeScript 확장, Native Messaging 브리지, Named Pipe Agent
- SQLite 스키마와 마이그레이션, 규칙/작업/이벤트 저장
- 도메인·정확한 호스트·URL 포함 규칙과 출처 대상 선택
- 경로 토큰, 루트 경계와 reparse point 방어
- 동일/교차 볼륨 안전 이동, 안정화 확인, SHA-256 검증, 중복 이름 보존
- WinUI 대시보드, 규칙, 이력, 선택 대기 그룹, 브라우저, 진단 화면
- 27개 .NET 테스트, 4개 TypeScript 테스트, Agent/Native Host 스모크 테스트
- WinUI 대시보드, 규칙, 파일별 선택 대기, 상태 필터·삭제 이력, 브라우저, 진단 화면
- 73개 .NET 테스트, 13개 TypeScript 테스트, Agent/Native Host 스모크 테스트
- per-user Native Host 등록 및 Inno Setup 설치 골격
- Native Host 등록의 Windows PowerShell 5.1 회귀 테스트와 개인정보 로그 회귀 테스트
- 매칭 규칙의 작업 생성·실제 파일 이동을 검증하는 Agent 통합 테스트
- BrowserTransferState/RoutingState 분리와 기존 DB v2 마이그레이션
- 단일 FIFO 선택 팝업, App 자동 시작 요청, 파일별/선택 항목 폴더 적용과 이동 건너뛰기
- Extension 시작 시 진행 중 브라우저 다운로드 상태 재조정
- 계층형 lazy `FolderTreePicker`와 고정 크기 독립 `FolderSelectionWindow`
- 이력의 Job별 경로 선택·변경, 완료 파일 확인 후 재이동, 현재 필터 전체 선택
- NavigationView Pending `InfoBadge`와 공통 대시보드 집계
- 규칙 편집·활성 토글·soft delete·저장 루트 Windows FolderPicker
- 단일 인스턴스 트레이 호스트, `--background`/`--shutdown`, HKCU 로그인 자동 시작
- per-user Inno Setup 설치/업그레이드/제거 및 설치 파일 빌드 스크립트

### Changed

- 취소 상태는 선택 여부와 관계없이 `Cancelled`가 되고 Waiting/SelectionReady 라우팅은 `NotRequired`로 종료
- UI 상태 갱신 주기를 500ms로 줄여 취소 후 팝업·Pending·이력을 1초 이내 반영
- 브라우저 기록에서 찾지 못한 진행 작업은 취소로 추측하지 않고 stale 진단 상태로 보존하며 자동 팝업에서 제외
- service worker 시작 재조정은 Job 상태만 맞추고 생성/마지막 실시간 브라우저 이벤트 기반의 30분 팝업 연령은 유지

- WinUI 앱을 Per-Monitor V2로 선언하고 공통 콘텐츠를 세로 ScrollViewer, stretch viewport, 1100 DIP 반응형 폼으로 재구성
- 다운로드 이력이 규칙에 매칭된 작업만 표시함을 명시하고 데이터 변경 시 3초 간격으로 자동 갱신
- Whale 설치 탐지에 Program Files와 Program Files (x86) 후보를 추가
- 대시보드가 다운로드 중, 선택 대기, 완료, 취소/중단, 재시도/실패를 별도로 집계
- 취소 이력을 자동 삭제하지 않고 취소선과 낮은 opacity로 유지하며 DB 이력만 사용자 삭제
- 취소 시 당시 RoutingState를 보존하되 표시, Pending과 이동 가능 여부는 BrowserTransferState를 우선
- 사이트 규칙 화면을 규칙 편집 Card와 저장된 규칙 Card 섹션으로 구분

### Fixed

- MainWindow가 최소화된 상태에서 선택창이 foreground를 얻지 못하고 작업표시줄 강조로만 끝나던 문제
- foreground 제한 시 바로 Flash fallback으로 종료하던 순서를 선택창 HWND의 normal 표시, 활성화, topmost, 입력 스레드 연결 재시도로 보강
- 앱 시작 때 오래된 `WaitingForSelection` 전체를 FIFO에 재삽입해 20개 이상 팝업이 연속 표시되던 문제
- service worker 시작 검색 결과가 오래된 Job의 마지막 브라우저 이벤트 시각을 현재로 덮어 다시 자동 팝업 대상으로 만들던 문제
- Whale가 `state` delta 없이 `error.current=USER_CANCELED`만 보낼 때 Extension이 조기 반환해 취소가 누락되던 문제
- `downloads.onErased`와 실제 사용자 취소를 혼동할 수 있는 시작 재조정 공백
- 일반 설정의 테마 ComboBox가 저장·적용 로직에 연결되지 않아 화면이 바뀌지 않던 문제
- 다크 테마에서 코드로 만든 카드와 기본 창 제목 표시줄이 라이트 리소스를 사용하던 대비 문제
- 정보 화면에 실행 버전이 없고 설치 프로그램 버전만 별도 하드코딩되어 구성 요소가 불일치할 수 있던 문제

- DPI awareness 누락으로 QHD 125%에서 앱 전체가 96 DPI 비트맵으로 확대되던 흐릿한 렌더링
- NavigationView 콘텐츠 폭/중복 패딩과 고정 MinWidth 때문에 좁은 창에서 오른쪽이 잘리던 레이아웃
- PowerShell 5.1에 없는 경로 API와 RegistryKey 직접 쓰기로 실패하던 HKCU Native Host 등록
- PowerShell 5.1이 Native Host manifest에 UTF-8 BOM을 붙이던 문제
- Extension이 Native Messaging 실패를 아무 진단 없이 무시하던 문제
- Native Host가 자동 시작한 Agent에 브라우저 표준 스트림 핸들을 상속하던 문제
- `WaitingForSelection` 하나로 전송 중/완료/취소를 동시에 표현해 취소 항목이 Pending에 남던 문제
- ScrollViewer가 좁은 창에서도 1100 DIP 폼을 측정해 우측 콘텐츠가 창 밖으로 나가던 문제
- 모든 페이지 제목이 TitleBar 바로 아래에 붙어 보이던 공통 상단 여백
- 다운로드 완료로 파일명이 바뀐 동안 열린 선택 팝업이 이전 이름과 상태를 유지하던 문제
- `download`, `*.crdownload` 등 임시 이름을 확정 이름처럼 표시하던 문제와 같은 다운로드의 중복 Job 가능성
- 긴 트리 노드가 선택 창 DesiredSize와 폭을 계속 키우던 문제
- 최소화·트레이 상태에서 ContentDialog가 사용자 화면에 직접 나타나지 않던 문제
- RoutingState에 따라 취소선이 누락되던 문제
- 선택 대기 수가 내비게이션에서 보이지 않던 문제
- 이력의 3상태 전체 선택이 한 번의 클릭으로 해제되지 않던 문제
- `dotnet publish` 결과에서 unpackaged WinUI XBF/PRI가 빠져 설치 App이 시작하지 못하던 문제

### Security

- 명령 allowlist, 1 MiB 메시지 제한, current-user IPC, URL 민감정보 정제
- native SQLite bundle을 advisory 경고가 없는 고정 버전으로 명시
- Extension 오류 로그를 bounded error code로 제한하고 파일 로그에서 URL, 로컬 경로, 토큰, 예외 메시지를 제거

### Known limitations

- 실제 브라우저 검증 전
- 선택 대기 자동 알림/팝업, 새 폴더 생성/새로 고침 미구현
- 시작 시 실행 토글과 미완료 작업 자동 복구 미연결
- Whale Automatic/SelectSubfolder는 실제 이동 검증 완료, 다른 Chromium 브라우저는 검증 전
- Windows 100%/150% 및 FHD/4K 실제 디스플레이 시각 검증 전
- 새 폴더 생성과 세션 간 “나중에 선택” 알림 미구현
- 실제 로그아웃/로그인 자동 시작과 미완료 작업 자동 복구 미검증
- 설치 파일 코드 서명 미구현
Loading
Loading