Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7af4263
feat(storage): add vault doctor API
antonreshetov Jun 15, 2026
a1d3609
feat(storage): add vault doctor preferences UI
antonreshetov Jun 15, 2026
a21d39e
fix(storage): separate vault doctor summary labels
antonreshetov Jun 15, 2026
54b268a
feat(storage): resolve vault doctor duplicate ids
antonreshetov Jun 15, 2026
219784d
feat(storage): add vault doctor conflict decisions UI
antonreshetov Jun 15, 2026
415d234
fix(storage): harden vault doctor conflict handling
antonreshetov Jun 15, 2026
1fa1b0f
fix(storage): constrain vault doctor conflict list
antonreshetov Jun 15, 2026
8627ad1
fix(storage): clarify vault doctor id decisions
antonreshetov Jun 15, 2026
0147138
fix(storage): clip vault doctor conflict panel corners
antonreshetov Jun 15, 2026
034604e
feat(storage): redesign vault doctor conflict decision UI
antonreshetov Jun 15, 2026
835e3de
feat(storage): proactively notify about vault conflicts on startup
antonreshetov Jun 15, 2026
d412858
feat(storage): auto-scan vault doctor when opened from startup notice
antonreshetov Jun 15, 2026
1554bf7
refactor(storage): extract useVaultDoctor composable, scroll to section
antonreshetov Jun 15, 2026
60daacf
fix(storage): improve radio contrast on card background in dark theme
antonreshetov Jun 15, 2026
ff63c61
fix(storage): lift conflict panel onto background surface for contrast
antonreshetov Jun 15, 2026
1ca63a5
fix(storage): delay vault doctor scan loader to avoid flicker
antonreshetov Jun 15, 2026
11ad1a8
fix(storage): delay vault doctor apply loader to avoid flicker
antonreshetov Jun 15, 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
114 changes: 114 additions & 0 deletions src/main/api/dto/vault-doctor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Elysia, { t } from 'elysia'

const vaultDoctorSpace = t.Union([
t.Literal('code'),
t.Literal('notes'),
t.Literal('http'),
t.Literal('math'),
])

const vaultDoctorAction = t.Union([
t.Literal('create-folder-metadata'),
t.Literal('detect-conflict'),
t.Literal('write-frontmatter'),
t.Literal('register-file'),
t.Literal('reassign-id'),
t.Literal('repair-environment-state'),
t.Literal('repair-math-state'),
t.Literal('sync-state'),
t.Literal('skip'),
])

const vaultDoctorKind = t.Union([
t.Literal('conflict'),
t.Literal('environment'),
t.Literal('file'),
t.Literal('folder'),
t.Literal('math-sheet'),
t.Literal('note'),
t.Literal('snippet'),
])

const vaultDoctorStatus = t.Union([
t.Literal('applied'),
t.Literal('blocked'),
t.Literal('needs-decision'),
t.Literal('pending'),
t.Literal('skipped'),
])

const vaultDoctorConflictReason = t.Union([
t.Literal('conflicted-copy'),
t.Literal('duplicate-id'),
t.Literal('invalid-frontmatter'),
t.Literal('merge-markers'),
])

const vaultDoctorFingerprint = t.Object({
mtimeMs: t.Number(),
path: t.String(),
size: t.Number(),
})

const vaultDoctorItem = t.Object({
action: vaultDoctorAction,
fingerprint: vaultDoctorFingerprint,
kind: vaultDoctorKind,
path: t.String(),
space: vaultDoctorSpace,
status: vaultDoctorStatus,
})

const vaultDoctorWarning = t.Object({
code: t.String(),
details: t.Optional(t.Record(t.String(), t.String())),
path: t.String(),
space: vaultDoctorSpace,
})

const vaultDoctorConflictGroup = t.Object({
id: t.String(),
items: t.Array(vaultDoctorItem),
reason: vaultDoctorConflictReason,
})

const vaultDoctorInput = t.Object({
decisions: t.Optional(
t.Array(
t.Object({
groupId: t.String(),
keepPath: t.String(),
}),
),
),
spaces: t.Optional(t.Array(vaultDoctorSpace)),
})

const vaultDoctorResponse = t.Object({
conflictGroups: t.Array(vaultDoctorConflictGroup),
items: t.Array(vaultDoctorItem),
summary: t.Object({
affectedFiles: t.Number(),
blocked: t.Number(),
conflicts: t.Number(),
folders: t.Number(),
httpEnvironments: t.Number(),
httpRequests: t.Number(),
mathSheets: t.Number(),
notes: t.Number(),
skipped: t.Number(),
snippets: t.Number(),
warnings: t.Number(),
}),
warnings: t.Array(vaultDoctorWarning),
})

export const vaultDoctorDTO = new Elysia().model({
vaultDoctorInput,
vaultDoctorResponse,
})

export type VaultDoctorInput = typeof vaultDoctorInput.static
export type VaultDoctorResponse = typeof vaultDoctorResponse.static
export type VaultDoctorItem = typeof vaultDoctorItem.static
export type VaultDoctorWarning = typeof vaultDoctorWarning.static
35 changes: 34 additions & 1 deletion src/main/api/routes/system.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Elysia } from 'elysia'
import { resetRuntimeCache } from '../../storage/providers/markdown'
import {
applyVaultDoctor,
previewVaultDoctor,
} from '../../storage/providers/markdown/doctor'
import { getVaultPath } from '../../storage/providers/markdown/runtime'
import { vaultDoctorDTO } from '../dto/vault-doctor'

const app = new Elysia({ prefix: '/system' })
const app = new Elysia({ prefix: '/system' }).use(vaultDoctorDTO)

app.get(
'/storage-vault-path',
Expand Down Expand Up @@ -34,4 +39,32 @@ app.post(
},
)

app.post(
'/vault-doctor/preview',
({ body }) => {
return previewVaultDoctor(body)
},
{
body: 'vaultDoctorInput',
response: 'vaultDoctorResponse',
detail: {
tags: ['System'],
},
},
)

app.post(
'/vault-doctor/apply',
({ body }) => {
return applyVaultDoctor(body)
},
{
body: 'vaultDoctorInput',
response: 'vaultDoctorResponse',
detail: {
tags: ['System'],
},
},
)

export default app
12 changes: 10 additions & 2 deletions src/main/i18n/locales/en_US/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@
"migrateToMarkdown": [
"Migrate to Markdown Vault?",
"The selected vault will be overwritten with the current SQLite library."
],
"vaultDoctorApply": [
"Apply vault fixes?",
"massCode will update safe metadata fixes and selected duplicate-id decisions. Unselected conflicts will be skipped."
]
},
"success": {
"copied": "Copied to clipboard",
"migrateToMarkdown": "Migrated to Markdown Vault. Folders: {{folders}}, snippets: {{snippets}}, tags: {{tags}}.",
"vaultMoved": "Vault successfully moved.",
"vaultLoaded": "Vault successfully loaded.",
"licenseActivated": "License activated. Thank you for supporting massCode!"
"licenseActivated": "License activated. Thank you for supporting massCode!",
"vaultDoctorApplied": "Vault Doctor applied {{count}} fixes.",
"vaultDoctorClean": "Vault Doctor found no issues."
},
"warning": {
"noUndo": "You cannot undo this action.",
Expand All @@ -33,7 +39,9 @@
"codeBlockRenderer": [
"When using Codemirror, the language to be set for the code block must correspond to one of the values of the",
"languages"
]
],
"vaultDoctorConflicts": "Vault: {{count}} sync conflict(s) need attention",
"vaultDoctorReview": "Review"
},
"error": {
"migration": "Auto-migration from SQLite failed: {{error}}",
Expand Down
39 changes: 38 additions & 1 deletion src/main/i18n/locales/en_US/preferences.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,44 @@
"moveVault": "Move Vault",
"movingVault": "Moving vault...",
"count": "Count",
"vaultPath": "Vault Path"
"vaultPath": "Vault Path",
"vaultDoctor": {
"label": "Vault Doctor",
"description": "Scan the vault for metadata issues, sync conflicts, and safe repairs.",
"scan": {
"label": "Check vault",
"action": "Scan vault",
"scanning": "Scanning..."
},
"apply": {
"action": "Apply fixes",
"applying": "Applying..."
},
"summary": {
"safe": "Safe fixes",
"conflicts": "Conflicts",
"blocked": "Blocked",
"warnings": "Warnings"
},
"conflicts": "Needs decision",
"decisions": {
"duplicateId": "Duplicate id: {{id}}",
"files": "Files: {{count}}",
"progress": "{{selected}}/{{total}} resolved",
"choose": "Choose one file to keep this id. The other {{count}} will get new unique ids.",
"original": "Original",
"copy": "Copy",
"keepsId": "keeps id {{id}}",
"newId": "new id"
},
"reason": {
"merge-markers": "Merge markers",
"invalid-frontmatter": "Invalid frontmatter",
"conflicted-copy": "Conflicted copy"
},
"moreWarnings": "+{{count}} more warnings",
"warnings": "Warnings"
}
},
"editor": {
"label": "Code Editor",
Expand Down
12 changes: 10 additions & 2 deletions src/main/i18n/locales/ru_RU/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@
"migrateToMarkdown": [
"Мигрировать в Markdown Vault?",
"Выбранный vault будет перезаписан текущей библиотекой SQLite."
],
"vaultDoctorApply": [
"Применить исправления vault?",
"massCode обновит безопасные metadata-исправления и выбранные решения по duplicate-id. Невыбранные конфликты будут пропущены."
]
},
"success": {
"copied": "Скопировано в буфер обмена",
"migrateToMarkdown": "Выполнена миграция в Markdown Vault. Папок: {{folders}}, сниппетов: {{snippets}}, тегов: {{tags}}.",
"vaultLoaded": "Хранилище успешно загружено.",
"licenseActivated": "Лицензия активирована. Спасибо за поддержку massCode!"
"licenseActivated": "Лицензия активирована. Спасибо за поддержку massCode!",
"vaultDoctorApplied": "Vault Doctor применил {{count}} исправлений.",
"vaultDoctorClean": "Vault Doctor не нашёл проблем."
},
"warning": {
"noUndo": "Это действие нельзя отменить.",
Expand All @@ -28,7 +34,9 @@
"codeBlockRenderer": [
"При использовании Codemirror язык, который будет установлен для блока кода, должен соответствовать одному из значений",
"languages"
]
],
"vaultDoctorConflicts": "Vault: {{count}} конфликтов требуют внимания",
"vaultDoctorReview": "Разобрать"
},
"error": {
"migration": "Автомиграция из SQLite завершилась ошибкой: {{error}}",
Expand Down
39 changes: 38 additions & 1 deletion src/main/i18n/locales/ru_RU/preferences.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,44 @@
"label": "Хранилище",
"migrateSqliteToMarkdown": "Мигрировать в Markdown Vault",
"count": "Количество",
"vaultPath": "Путь к vault"
"vaultPath": "Путь к vault",
"vaultDoctor": {
"label": "Vault Doctor",
"description": "Проверяет vault на проблемы metadata, конфликты синхронизации и безопасные исправления.",
"scan": {
"label": "Проверка vault",
"action": "Сканировать vault",
"scanning": "Сканирование..."
},
"apply": {
"action": "Применить исправления",
"applying": "Применение..."
},
"summary": {
"safe": "Безопасные исправления",
"conflicts": "Конфликты",
"blocked": "Заблокировано",
"warnings": "Предупреждения"
},
"conflicts": "Требует решения",
"decisions": {
"duplicateId": "Одинаковый id: {{id}}",
"files": "Файлов: {{count}}",
"progress": "Решено {{selected}}/{{total}}",
"choose": "Выберите один файл, который сохранит этот id. Остальные {{count}} получат новые уникальные id.",
"original": "Оригинал",
"copy": "Копия",
"keepsId": "сохранит id {{id}}",
"newId": "новый id"
},
"reason": {
"merge-markers": "Merge-маркеры",
"invalid-frontmatter": "Битый frontmatter",
"conflicted-copy": "Конфликтная копия"
},
"moreWarnings": "+{{count}} предупреждений",
"warnings": "Предупреждения"
}
},
"editor": {
"label": "Редактор",
Expand Down
Loading