diff --git a/src/main/index.ts b/src/main/index.ts index b30372f..8aee3ec 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -169,7 +169,9 @@ app.whenReady().then(async () => { registerInboxApi({ repo, continuity, capture, health, intent, getInboxWindow, settings: settingsSvc, providerHolder, - paths: { profileDir: paths.profileDir } + paths: { profileDir: paths.profileDir }, + // v0.2.9 Cut B Task 16 — disabled 메모 일괄 재투입 시 in-memory queue 갱신. + enqueue: (id) => worker.enqueue(id) }); // registerSettingsApi 는 backup / exportSvc / importSvc / syncSvc / telemetry 가 // 생성된 뒤에 호출 (Task 10) — 아래 BackupService/ExportService/... 초기화 직후로 이동. diff --git a/src/main/ipc/inboxApi.ts b/src/main/ipc/inboxApi.ts index e17ed55..6f33776 100644 --- a/src/main/ipc/inboxApi.ts +++ b/src/main/ipc/inboxApi.ts @@ -25,6 +25,10 @@ export interface InboxIpcDeps { providerHolder: ProviderHolder; // v0.2.8 Cut A — `inbox:open-media` 의 path traversal 검사 baseline. paths: { profileDir: string }; + // v0.2.9 Cut B Task 16 — disabled 메모 일괄 처리 시 in-memory worker queue 갱신. + // 미주입 시 fire-and-forget skip (다음 launch 의 loadFromDb 가 처리). 본 hook 은 + // AiWorker 인스턴스 직접 주입을 피해 IPC 모듈이 worker import 를 갖지 않도록 분리. + enqueue?: (noteId: string) => Promise; } export function registerInboxApi(deps: InboxIpcDeps): void { @@ -226,6 +230,29 @@ export function registerInboxApi(deps: InboxIpcDeps): void { }); }); + // v0.2.9 Cut B Task 16 — disabled 메모 (ai_enabled OFF 시기 캡처) 일괄 재투입. + // OFF→ON 전환 후 사용자가 "지금 모두 처리" 버튼 클릭 path. repo.requeueDisabled 가 + // ai_status='pending' + pending_jobs row 보장, worker.enqueue 가 in-memory queue 갱신. + ipcMain.handle('inbox:enqueue-disabled', async () => { + // requeue 전 대상 id 수집 — UPDATE 가 status 바꾸므로 select 후 update 필요 없이 + // requeueDisabled 가 처리한 다음 pending_jobs 에서 다시 가져와 enqueue. + const targets = deps.repo.getAllPendingJobs().map((j) => j.noteId); + const before = new Set(targets); + const count = deps.repo.requeueDisabled(); + if (count > 0 && deps.enqueue) { + const after = deps.repo.getAllPendingJobs(); + // requeue 직후 새로 들어온 pending_jobs row 만 enqueue (기존 row 는 이미 in-memory queue 에). + for (const j of after) { + if (!before.has(j.noteId)) { + await deps.enqueue(j.noteId); + } + } + } + return { count }; + }); + + ipcMain.handle('inbox:get-disabled-count', () => deps.repo.countByAiStatus('disabled')); + ipcMain.handle('inbox:saveOllamaSettings', async (_e, value: { endpoint: string; model: string }) => { // 검증: 새 인스턴스로 healthCheck const trial = new LocalOllamaProvider({ endpoint: value.endpoint, model: value.model }); diff --git a/src/main/repository/NoteRepository.ts b/src/main/repository/NoteRepository.ts index adaecb1..679ec69 100644 --- a/src/main/repository/NoteRepository.ts +++ b/src/main/repository/NoteRepository.ts @@ -216,6 +216,50 @@ export class NoteRepository { return { ids }; } + /** + * v0.2.9 Cut B Task 16 — 모든 ai_status='disabled' 노트를 'pending' 으로 reset 하고 + * pending_jobs 재투입. 사용자가 settings.ai_enabled OFF→ON 전환 후 "지금 모두 처리" + * 버튼을 누른 path. 단일 transaction. 호출자가 `now` 주입 가능 (테스트성). + * + * INSERT OR IGNORE — race 안전 (이미 pending_jobs row 존재 시 skip). + * 반환값 = 처리된 노트 수 (UI 가 "N건 처리됨" 토스트 등 표시용). + */ + requeueDisabled(now: Date = new Date()): number { + const tx = this.db.transaction(() => { + const ts = now.toISOString(); + const targets = this.db + .prepare(`SELECT id FROM notes WHERE ai_status='disabled'`) + .all() as Array<{ id: string }>; + for (const { id } of targets) { + this.db + .prepare(`UPDATE notes SET ai_status='pending', updated_at=? WHERE id=?`) + .run(ts, id); + this.db + .prepare( + `INSERT OR IGNORE INTO pending_jobs (note_id, attempts, next_run_at) VALUES (?, 0, ?)` + ) + .run(id, ts); + } + return targets.length; + }); + return tx(); + } + + /** + * v0.2.9 Cut B Task 16 — ai_status 별 row count. + * 설정 페이지의 "원문만 저장된 메모 N건" 표기용 (status='disabled' 카운트). + * deleted_at 필터 없음 — disabled 메모도 trash 갈 수 있는데 사용자 의도는 + * "AI 처리할 게 얼마나 남았나?" 라 trashed 까지 포함되면 안 됨. → deleted_at IS NULL 추가. + */ + countByAiStatus(status: AiStatus): number { + const row = this.db + .prepare( + `SELECT COUNT(*) AS c FROM notes WHERE ai_status=? AND deleted_at IS NULL` + ) + .get(status) as { c: number }; + return row.c; + } + /** * pending_jobs 의 next_run_at + last_error 만 갱신, attempts 변경 없음. * v0.2.3 #2 — unreachable/timeout 무한 retry 시 사용 (incrementJobAttempt 와 별도 경로). diff --git a/src/preload/index.ts b/src/preload/index.ts index 74355be..33b6d42 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -78,6 +78,9 @@ const api: InklingApi = { getSettings: () => ipcRenderer.invoke('settings:get'), setAiEnabled: (enabled: boolean) => ipcRenderer.invoke('settings:set-ai-enabled', enabled), setOnboardingCompleted: (completed: boolean) => ipcRenderer.invoke('settings:set-onboarding-completed', completed), + // v0.2.9 Cut B Task 16 — disabled 메모 재투입 + count. + enqueueDisabled: () => ipcRenderer.invoke('inbox:enqueue-disabled'), + getDisabledCount: () => ipcRenderer.invoke('inbox:get-disabled-count'), } }; diff --git a/src/renderer/inbox/components/settings/AiProviderSection.tsx b/src/renderer/inbox/components/settings/AiProviderSection.tsx index db68e77..cd32e12 100644 --- a/src/renderer/inbox/components/settings/AiProviderSection.tsx +++ b/src/renderer/inbox/components/settings/AiProviderSection.tsx @@ -10,8 +10,9 @@ export function AiProviderSection(): React.ReactElement { const [error, setError] = useState(null); const [saveResult, setSaveResult] = useState(null); const [recheckResult, setRecheckResult] = useState(null); - // v0.2.9 Cut B Task 15: AI 자동 처리 토글. + // v0.2.9 Cut B Task 15-16: AI 자동 처리 토글 + disabled 메모 일괄 처리. const [aiEnabled, setAiEnabledState] = useState(null); + const [disabledCount, setDisabledCount] = useState(0); useEffect(() => { void (async () => { @@ -21,13 +22,29 @@ export function AiProviderSection(): React.ReactElement { setModel(s.model); } const settings = await inboxApi.getSettings(); - setAiEnabledState(settings.ai_enabled ?? true); + const enabled = settings.ai_enabled ?? true; + setAiEnabledState(enabled); + if (enabled) { + const c = await inboxApi.getDisabledCount(); + setDisabledCount(c); + } })(); }, []); async function onToggleAi(checked: boolean): Promise { await inboxApi.setAiEnabled(checked); setAiEnabledState(checked); + if (checked) { + const c = await inboxApi.getDisabledCount(); + setDisabledCount(c); + } else { + setDisabledCount(0); + } + } + + async function onProcessDisabled(): Promise { + await inboxApi.enqueueDisabled(); + setDisabledCount(0); } async function onSave(): Promise { @@ -79,6 +96,27 @@ export function AiProviderSection(): React.ReactElement {

)} + {/* v0.2.9 Cut B Task 16 — ON 전환 후 disabled 메모 일괄 처리 prompt */} + {aiEnabled === true && disabledCount > 0 && ( +
+ 원문만 저장된 메모 {disabledCount}건이 있습니다. + +
+ )}