Prod: полный аудит и стабилизация — исправлены критические баги, расширено покрытие тестами, подготовлен бандл

- FileStore: защита от выхода за пределы хранилища (isInsideVault), валидация имён (запрет скрытых, зарезервированных, управляющих символов), проверка destDir, лимит watcher, dailyNote валидация, openVault проверка isDir
- DocumentManager: лимит 10МБ, кламп автосейва 0.5-120с, защита от OOM, бинарный детект, проверка путей
- VaultIndex: исправлен парсинг тегов (заголовки, инлайн-код), case-insensitive поиск, дедупликация рёбер, защита от traversal, лимит глубины, проверка vaultExists
- MdParser: блокировка всех on* событий, javascript:/data:, esc кавычек, MIME fallback, normalizeLang расширен (c/cpp/py/js и кириллица), wiki глобальный поиск, image лимит 3МБ, защита clipboard
- GitManager: commitAll с GIT_INDEX_ADD_DEFAULT и поддержкой пустых коммитов/удалений, fileGitStatus с cleanPath и проверкой префикса, repoExists/hasCommits с cleanPath, initRepo валидация
- SettingsStore: encodeKey/encodeRel для QSettings Ini, flat хранение viewMode, sync после записи, кламп ширин панелей, миграция старых ключей
- SyncManager: счётчик busy, обработка httpStatus при network error, валидация токена/провайдера/URL, GitHub Enterprise URL, таймаут
- VaultManager: валидация путей (корень, файл vs папка, права), проверка вложенности
- WindowController: per-window FileStore/DocumentManager/VaultIndex, cleanPath, закрытие окна через QQuickWindow::closing, обработка ошибок компонента
- QML: TableEditor пустая строка, WelcomeWizard QUrl парсинг, FileTree фильтр, GraphView лимит 600 узлов, RightPanel debounce rebuild, Theme singleton, MonthCalendar и др.
- Тесты: добавлены flora_settings_test, flora_sync_test, flora_extended_test, flora_integration_test — покрытие всех модулей, 20/20 зелёных
- Сборка: обновлён Flora.app (4.6М бинарь -> 173М бандл, ad-hoc подпись, проверен offscreen запуск, версия 0.0.1 + cmark 0.29 + libgit2 1.9)
- .gitignore: добавлены Testing/, coverage, gcda/gcov, __pycache__, .tmp, test-vault и др.
This commit is contained in:
2026-09-01 19:25:07 +03:00
parent 0858b48cb3
commit 95c03040c7
41 changed files with 3868 additions and 1098 deletions
+82 -44
View File
@@ -1,19 +1,24 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Flora
// Главное окно: меню вверху, три панели (дерево / редактор / правая).
// Мульти-хранилища: каждое окно привязано к своему хранилищу (windowVault).
ApplicationWindow {
id: window
width: 1280
height: 800
minimumWidth: 900
minimumHeight: 600
visible: true
title: "Flora"
color: "white"
color: Theme.bg
palette.window: Theme.bg
palette.windowText: Theme.text
palette.base: Theme.surface
palette.text: Theme.text
palette.highlight: Theme.accent
palette.highlightedText: "white"
// Хранилище этого окна (задаётся WindowController); у основного окна
// может быть пустым — тогда берём основное хранилище из настроек.
property string windowVault: ""
readonly property string activeVault: {
@@ -27,10 +32,8 @@ ApplicationWindow {
firstRun: vaultManager.vaults.length === 0
onFinished: {
if (fileStore.ready) {
// Добавлено новое хранилище при уже открытом — новое окно.
windowController.openWindow(welcomeWizard.vaultPath)
} else {
// Первый запуск: применяем хранилище к этому окну.
fileStore.openVault(welcomeWizard.vaultPath)
root_status("Хранилище открыто")
}
@@ -43,7 +46,7 @@ ApplicationWindow {
}
function root_status(msg, ok) {
console.log(msg)
// статус можно показать в UI, пока просто игнорируем
}
Component.onCompleted: {
@@ -52,34 +55,14 @@ ApplicationWindow {
} else if (vaultManager.currentVault.length > 0) {
fileStore.openVault(vaultManager.currentVault)
}
// Если хранилищ нет — мастер откроется автоматически.
if (vaultManager.vaults.length === 0) {
welcomeWizard.open()
} else {
// Автооткрытие первого .md файла для тестирования
autoOpenTimer.start()
}
}
Timer {
id: autoOpenTimer
interval: 200
onTriggered: {
if (!fileStore.ready) return
for (let i = 0; i < fileStore.rowCount(); i++) {
if (!fileStore.itemIsDir(i)) {
const p = fileStore.itemPath(i)
if (p.endsWith(".md")) {
documentManager.openDocument(p)
console.log("AUTO-OPENED:", p, "len:", documentManager.contentAt(documentManager.activeIndex).length)
break
}
}
}
}
}
menuBar: MenuBar {
// Переопределяем цвета под тему (Basic стиль не красит сам)
background: Rectangle { color: Theme.bgAlt; border.color: Theme.divider }
Menu {
title: qsTr("&Файл")
Action { text: qsTr("Добавить хранилище…"); onTriggered: window.openWizard() }
@@ -98,13 +81,35 @@ ApplicationWindow {
MenuSeparator { }
Action {
text: qsTr("Вернуться к предыдущей версии…"); enabled: false
// Диалог истории с diff-режимом — Фаза 9.
}
}
Menu {
title: qsTr("&Вид")
Action { text: qsTr("Светлая тема"); checkable: true; checked: true }
Action { text: qsTr("Тёмная тема"); checkable: true }
ActionGroup { id: themeGroup }
Action {
text: qsTr("Светлая тема")
checkable: true
checked: settingsStore.theme !== "dark"
ActionGroup.group: themeGroup
onTriggered: settingsStore.theme = "light"
}
Action {
text: qsTr("Тёмная тема")
checkable: true
checked: settingsStore.theme === "dark"
ActionGroup.group: themeGroup
onTriggered: settingsStore.theme = "dark"
}
MenuSeparator { }
Action {
text: qsTr("Сбросить ширины панелей")
onTriggered: {
settingsStore.leftPanelWidth = 220
settingsStore.rightPanelWidth = 220
leftPanel.width = 220
rightPanel.width = 220
}
}
}
Menu {
title: qsTr("&Справка")
@@ -121,28 +126,31 @@ ApplicationWindow {
standardButtons: Dialog.Ok
modal: true
anchors.centerIn: parent
background: Rectangle { color: Theme.surface; border.color: Theme.border; radius: 8 }
Label {
color: Theme.text
text: "Flora\nМинималистичный markdown-заметочник (GPL-3.0)\n"
+ "Версия " + Qt.application.version
}
}
// Переключатель хранилищ (кнопка внизу слева)
Popup {
id: vaultSwitcher
x: 12
y: window.height - height - 12
width: 320
width: 340
modal: false
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle { color: Theme.surface; border.color: Theme.borderStrong; radius: 8 }
ColumnLayout {
width: parent.width
spacing: 6
spacing: 8
Label {
text: qsTr("Хранилища")
font.bold: true
color: Theme.text
}
ListView {
@@ -151,10 +159,12 @@ ApplicationWindow {
Layout.fillWidth: true
model: vaultManager.vaults
clip: true
ScrollBar.vertical: ScrollBar {}
delegate: ItemDelegate {
width: ListView.view.width
text: modelData
highlighted: modelData === window.activeVault
palette.highlight: Theme.accent
onClicked: {
if (modelData === window.activeVault) {
vaultSwitcher.close()
@@ -169,15 +179,16 @@ ApplicationWindow {
Label {
visible: vaultList.count === 0
text: qsTr("Хранилищ пока нет")
color: "#888"
color: Theme.textMuted
}
Label {
text: qsTr("Текущее: %1").arg(window.activeVault.length > 0
? window.activeVault : "—")
color: "#666"
color: Theme.textSecondary
elide: Text.ElideMiddle
Layout.fillWidth: true
font.pixelSize: 11
}
Button {
@@ -189,14 +200,35 @@ ApplicationWindow {
}
SplitView {
id: mainSplit
anchors.fill: parent
orientation: Qt.Horizontal
// тонкая линия-разделитель под тему
handle: Rectangle {
implicitWidth: 1
implicitHeight: 1
color: Theme.divider
Rectangle { // увеличенная зона захвата
anchors.centerIn: parent
width: 8; height: parent.height
color: "transparent"
MouseArea {
anchors.fill: parent
cursorShape: Qt.SplitHCursor
onPressed: mouse.accepted = false
}
}
}
FileTreePanel {
id: leftPanel
SplitView.preferredWidth: window.width * 0.15
SplitView.minimumWidth: 120
SplitView.preferredWidth: settingsStore.leftPanelWidth
SplitView.minimumWidth: 180
SplitView.maximumWidth: 500
onWidthChanged: {
if (width >= 180 && width <= 500)
settingsStore.leftPanelWidth = width
}
onOpenVaultRequested: vaultSwitcher.open()
onOpenFileRequested: (path) => {
@@ -209,15 +241,21 @@ ApplicationWindow {
}
EditorPanel {
id: centerPanel
SplitView.fillWidth: true
SplitView.minimumWidth: 300
height: window.height - menuBar.height
height: window.height - (menuBar ? menuBar.height : 0)
}
RightPanel {
SplitView.preferredWidth: window.width * 0.15
SplitView.minimumWidth: 140
id: rightPanel
SplitView.preferredWidth: settingsStore.rightPanelWidth
SplitView.minimumWidth: 200
SplitView.maximumWidth: 500
onWidthChanged: {
if (width >= 200 && width <= 500)
settingsStore.rightPanelWidth = width
}
}
}
}
}