Replace repo with papayu-refactored: desktop/ structure, Tailwind UI, simplified Tauri backend

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Yuriy 2026-02-11 22:00:43 +03:00
parent 289b1b6dac
commit 1c91f51a1d
317 changed files with 15520 additions and 36604 deletions

View File

@ -0,0 +1,185 @@
# Аудит программы PAPA YU
**Дата:** 29 января 2026
**Объект:** приложение PAPA YU (Tauri 2 + React), репозиторий PAPA-YU.
---
## 1. Архитектура программы
### 1.1. Общая схема
| Слой | Технологии | Расположение |
|------|------------|--------------|
| **Backend** | Tauri 2, Rust | `desktop/src-tauri/` |
| **Frontend** | React 19, Vite 7, TypeScript | `desktop/ui/` |
| **Конфигурация** | tauri.conf.json, capabilities | `desktop/src-tauri/` |
- Одно окно приложения (title: «PAPA YU», 800×600, resizable).
- Frontend загружается в dev с `http://localhost:5173`, в production из `../ui/dist`.
### 1.2. Дерево каталогов (ключевое)
```
PAPA-YU/
├── desktop/
│ ├── src-tauri/ # Tauri (Rust)
│ │ ├── src/
│ │ │ ├── main.rs # точка входа → app_lib::run()
│ │ │ ├── lib.rs # Builder, плагины, invoke_handler
│ │ │ ├── types.rs # Action, AnalyzeReport, DiffItem, …
│ │ │ └── commands/
│ │ │ ├── mod.rs
│ │ │ ├── analyze_project.rs
│ │ │ ├── preview_actions.rs
│ │ │ ├── apply_actions.rs
│ │ │ └── undo_last.rs
│ │ ├── capabilities/default.json
│ │ ├── icons/ # иконки приложения (в т.ч. icon_source.png)
│ │ ├── Cargo.toml
│ │ └── tauri.conf.json
│ └── ui/ # React SPA
│ ├── src/
│ │ ├── App.tsx, main.tsx
│ │ ├── config/routes.ts
│ │ ├── store/app-store.ts
│ │ ├── lib/analyze.ts, event-bus.ts, anime-utils.ts
│ │ ├── components/layout/Layout.tsx, ErrorBoundary, ErrorDisplay
│ │ └── pages/ # Tasks, Dashboard, Reglamenty, …
│ ├── public/logo-papa-yu.png
│ ├── package.json
│ └── vite.config.ts
├── desktop-core/tools/project-auditor/ # index.ts (минимально/пусто)
├── index/manifest.json # version, sources
├── PAPA YU.command # скрипт запуска .app
├── СБОРКА_И_ОБНОВЛЕНИЯ.md
├── ОТЧЁТОРОГРАММЕ.md
└── СИНХРОНИЗАЦИЯ_ПАПОК.md
```
### 1.3. Backend (Rust)
- **Плагины:** dialog, updater, process; в debug — log.
- **Команды (invoke):** `analyze_project`, `preview_actions`, `apply_actions`, `undo_last`.
- **Типы:** Action, ActionKind, AnalyzeReport, PreviewResult, ApplyResult, UndoResult, DiffItem и др. (см. `types.rs`).
### 1.4. Frontend (React)
- **Роутинг:** HashRouter, маршруты в `config/routes.ts` и `App.tsx`.
- **Состояние:** Zustand (`store/app-store.ts`): currentRoute, systemStatus, recentAuditEvents, error.
- **Связь с backend:** вызовы `invoke()` из `lib/analyze.ts` (analyze_project) и из `pages/Tasks.tsx` (preview_actions, apply_actions, undo_last).
- **События:** `listen('analyze_progress')` в Tasks; eventBus (NAVIGATE, ROUTE_CHANGED) в Layout.
---
## 2. Пути запуска
| Способ | Путь / команда | Примечание |
|--------|----------------|------------|
| Разработка | `cd ~/PAPA-YU/desktop/src-tauri``cargo tauri dev` | Поднимает UI на порту 5173, открывает окно |
| Запуск .app (после сборки) | `~/PAPA-YU/desktop/src-tauri/target/release/bundle/macos/PAPA YU.app` | Двойной клик или `open "…/PAPA YU.app"` |
| Скрипт в корне проекта | `~/PAPA-YU/PAPA YU.command` | Запускает указанный .app; если его нет — предлагает собрать |
| DMG (установка) | `~/PAPA-YU/desktop/src-tauri/target/release/bundle/dmg/` | После `cargo tauri build`; пользователь открывает DMG и перетаскивает приложение в «Программы» |
| Сборка | `cd ~/PAPA-YU/desktop/src-tauri``cargo tauri build` | beforeBuildCommand: сборка UI из `$HOME/PAPA-YU/desktop/ui` |
**Жёстко заданные пути в конфиге:**
- `tauri.conf.json``build.beforeDevCommand` и `beforeBuildCommand`: `$HOME/PAPA-YU/desktop/ui`.
- `build.frontendDist`: `../ui/dist` (относительно src-tauri).
---
## 3. Связи и ссылки
### 3.1. Внешние
| Назначение | URL / значение |
|------------|----------------|
| Обновления (updater) | `https://github.com/yrippert-maker/papayu/releases/latest/download/latest.json` |
| Репозиторий (по документации) | `https://github.com/yrippert-maker/papayu` |
### 3.2. Внутренние (Frontend ↔ Backend)
| UI | Команда Tauri | Файл |
|----|----------------|------|
| Анализ проекта | `analyze_project(path)` | `lib/analyze.ts``invoke('analyze_project', { path })` |
| Предпросмотр изменений | `preview_actions(payload)` | `Tasks.tsx``invoke('preview_actions', { payload })` |
| Применить изменения | `apply_actions(payload)` | `Tasks.tsx``invoke('apply_actions', { payload })` |
| Откат | `undo_last(path)` | `Tasks.tsx``invoke('undo_last', { path })` |
### 3.3. События (Backend → Frontend)
| Событие | Где эмитится | Где слушается |
|---------|--------------|----------------|
| `analyze_progress` | analyze_project.rs, apply_actions.rs, preview_actions.rs, undo_last.rs | Tasks.tsx |
### 3.4. Права (capabilities)
- `core:default`, `dialog:allow-open`, `core:event:allow-listen`, `updater:default`, `process:allow-restart`.
---
## 4. Маршруты UI
| Путь | Страница | Доступ |
|------|----------|--------|
| `/` | Редирект на `/tasks` | — |
| `/tasks` | Tasks (анализ проекта) | Навбар |
| `/control-panel` | Dashboard | Навбар |
| `/reglamenty` | Reglamenty | Нет в навбаре (прямой URL) |
| `/tmc-zakupki`, `/finances`, `/personnel` | TMCZakupki, Finances, Personnel | Нет в навбаре |
| `/chat` | Редирект на `/tasks` | — |
| `/policies`, `/audit`, `/secrets` | PolicyEngine, AuditLogger, SecretsGuard | Прямой URL / с Dashboard |
| `*` | NotFound | — |
---
## 5. Отчёт об ошибках
### 5.1. Ошибка в интерфейсе: «Could not fetch a valid…»
**Симптом:** Рядом с логотипом в шапке приложения отображается сообщение вида «Could not fetch a valid...» (обрезка полного текста).
**Причина:** Это результат нажатия кнопки «Проверить обновления» (иконка загрузки рядом с логотипом). Плагин updater вызывает `check()` по endpoint
`https://github.com/yrippert-maker/papayu/releases/latest/download/latest.json`.
Ошибка возникает, когда:
1. На GitHub нет релиза с файлом `latest.json`, или
2. В `tauri.conf.json` в `plugins.updater.pubkey` указана заглушка `REPLACE_WITH_PUBLIC_KEY_AFTER_tauri_signer_generate`, или
3. Ответ сервера не прошёл проверку подписи/формата.
**Рекомендации:**
- Для отключения проверки обновлений до настройки: не показывать текст ошибки в шапке постоянно; показывать только после нажатия кнопки и обрабатывать «нет обновлений» и сетевые ошибки отдельно (например, тост «Обновлений нет» / «Ошибка сети»).
- Для включения обновлений: сгенерировать ключи (`tauri signer generate`), подставить публичный ключ в `tauri.conf.json`, публиковать подписанные артефакты и `latest.json` в GitHub Releases (см. `СБОРКА_И_ОБНОВЛЕНИЯ.md`).
### 5.2. Конфигурация updater
- **Проблема:** `pubkey` в `tauri.conf.json` — строка-заглушка; проверка подписи обновлений не может пройти.
- **Действие:** Заменить на реальный публичный ключ после `tauri signer generate` или временно не показывать пользователю сырое сообщение об ошибке updater.
### 5.3. Зависимость путей от $HOME
- **Проблема:** Сборка и dev зависят от путей `$HOME/PAPA-YU/desktop/ui`. При переносе проекта или другом имени пользователя команды могут не найти каталог.
- **Действие:** Либо сохранять единый стандарт размещения (например, всегда `~/PAPA-YU`), либо вынести путь в переменную окружения и использовать её в конфиге/скриптах.
### 5.4. Скрипт PAPA YU.command
- **Проблема:** Ищет .app в `$ROOT/desktop/src-tauri/target/release/bundle/macos`, где `ROOT` — каталог, откуда запущен скрипт. Если скрипт вызывается не из корня PAPA-YU, путь будет неверным.
- **Действие:** Либо всегда запускать из корня проекта, либо задать ROOT явно (например, по фиксированному пути или по расположению скрипта с учётом известной структуры папок).
### 5.5. desktop-core и index
- `desktop-core/tools/project-auditor/index.ts` — по сути пустой/минимальный; не используется из основного приложения.
- `index/manifest.json` — только `version` и пустой `sources`; связь с Tauri/UI не прослеживается.
---
## 6. Сводка
| Раздел | Статус |
|--------|--------|
| Архитектура (Tauri + React, команды, плагины) | Описана, связи учтены |
| Пути запуска (dev, .app, DMG, скрипт) | Зафиксированы |
| Связи (invoke, события, endpoint обновлений) | Перечислены |
| Ошибка «Could not fetch a valid…» | Объяснена (updater + pubkey/релизы), даны рекомендации |
| Конфиг updater и пути $HOME | Отмечены как зоны внимания |
Рекомендуется: (1) скрыть или смягчить отображение ошибки updater до настройки подписи и релизов; (2) подставить реальный `pubkey` и настроить публикацию `latest.json` при желании использовать обновления; (3) зафиксировать в документации единый путь установки/запуска (например, `~/PAPA-YU` и запуск через `PAPA YU.command` или установку из DMG).

View File

@ -0,0 +1,201 @@
# Полный отчёт по программе PAPA YU (Tauri + React)
**Дата:** 23 января 2026
**Объект анализа:** приложение papa-yu (десктоп на Tauri 2 + React 19).
**Задача:** анализ архитектуры, связей, промптов и состояния программы в целом **без внесения исправлений**.
---
## 1. Архитектура программы
### 1.1. Общая схема
Приложение построено по схеме **двух слоёв**:
| Слой | Технологии | Расположение |
|------|------------|--------------|
| **Backend (оболочка)** | Tauri 2, Rust | `desktop/src-tauri/` |
| **Frontend (UI)** | React 19, Vite 7, TypeScript | `desktop/ui/` |
- **Tauri** обеспечивает нативное окно, загрузку веб-интерфейса (из `../ui/dist` в production или с `http://localhost:5173` в dev) и минимальную инициализацию (плагин логов в debug).
- **React-приложение** — SPA с HashRouter: все экраны рендерятся на клиенте, отдельного сервера рендеринга нет.
### 1.2. Структура каталогов
```
papa-yu/
├── desktop/
│ ├── src-tauri/ # Tauri (Rust)
│ │ ├── src/
│ │ │ ├── main.rs # точка входа, вызов app_lib::run()
│ │ │ └── lib.rs # tauri::Builder, setup (log), run
│ │ ├── capabilities/
│ │ │ └── default.json # окно "main", core:default
│ │ ├── build.rs
│ │ ├── Cargo.toml
│ │ └── tauri.conf.json
│ └── ui/ # Frontend (React)
│ ├── src/
│ │ ├── App.tsx
│ │ ├── main.tsx
│ │ ├── index.css
│ │ ├── config/routes.ts
│ │ ├── store/app-store.ts
│ │ ├── lib/event-bus.ts
│ │ ├── components/
│ │ │ ├── layout/Layout.tsx
│ │ │ ├── ErrorBoundary.tsx
│ │ │ └── ErrorDisplay.tsx
│ │ └── pages/
│ │ ├── Dashboard.tsx
│ │ ├── Tasks.tsx
│ │ ├── ChatAgent.tsx
│ │ ├── PolicyEngine.tsx
│ │ ├── AuditLogger.tsx
│ │ ├── SecretsGuard.tsx
│ │ ├── Reglamenty.tsx
│ │ ├── TMCZakupki.tsx
│ │ ├── Finances.tsx
│ │ ├── Personnel.tsx
│ │ ├── Documents.tsx
│ │ └── NotFound.tsx
│ ├── package.json
│ ├── vite.config.ts
│ └── tailwind.config.ts
├── desktop-core/
│ └── tools/project-auditor/
│ └── index.ts # пустой файл
├── index/
│ └── manifest.json # version, sources
├── СИНХРОНИЗАЦИЯ_ПАПОК.md
└── PAPA YU.command
```
### 1.3. Сборка и запуск
- **Конфиг Tauri** (`tauri.conf.json`): productName «PAPA YU», identifier `com.papa-yu`, одно окно 800×600, resizable. `beforeDevCommand` и `beforeBuildCommand` обращаются к `$HOME/papa-yu/desktop/ui` (npm run dev / npm run build), frontend в dev — `http://localhost:5173`, в production — `../ui/dist`.
- **UI:** сборка — `tsc -b && vite build`, dev — `vite` (порт по умолчанию Vite, в конфиге Tauri указан 5173).
- **Единый запуск для пользователя:** скрипт `PAPA YU.command` в корне; для разработки — `cargo tauri dev` из `desktop/src-tauri`.
---
## 2. Выстроенные связи
### 2.1. Роутинг
- **React Router:** `HashRouter`, маршруты заданы в `App.tsx` и в `config/routes.ts`.
- **Маршруты:** `/` (Dashboard), `/tasks`, `/reglamenty`, `/tmc-zakupki`, `/finances`, `/personnel`, `/documents`, `/chat`, `/policies`, `/audit`, `/secrets`, `*` (NotFound).
- **RouteTracker:** при смене `location.pathname` вызывает `useAppStore.getState().setCurrentRoute(location.pathname)` — актуализация текущего маршрута в store.
### 2.2. Глобальное состояние (Zustand)
**Store** (`store/app-store.ts`):
- `currentRoute` — текущий путь (обновляется RouteTracker).
- `systemStatus` — три флага: `policyEngine`, `auditLogger`, `secretsGuard` (все по умолчанию `'active'`); используются на Dashboard для отображения статусов карточек.
- `recentAuditEvents` — массив событий аудита; пополняется при переходе с Dashboard по карточкам (addAuditEvent).
- `error` / `setError` — глобальное сообщение об ошибке (показ через ErrorDisplay).
Никаких вызовов Tauri (invoke) или бэкенд-API из store нет — только клиентское состояние.
### 2.3. Event Bus
**Файл:** `lib/event-bus.ts`.
- Синглтон с методами `on(event, callback)` и `emit(event, data)`.
- События: `NAVIGATE`, `ROUTE_CHANGED`.
- **Использование:** в `Layout.tsx` при клике по пункту навбара вызываются `eventBus.emit(Events.NAVIGATE, { path })` и `eventBus.emit(Events.ROUTE_CHANGED, { path })`. Подписчиков на эти события в коде нет — механизм заложен под будущее использование.
### 2.4. Layout и навигация
- **Layout** оборачивает все страницы внутри `HashRouter` и рендерит навбар с 8 пунктами: Панель, Задачи, Регламенты, ТМЦ и закупки, Финансы, Персонал, Документы, Чат с агентом. Страницы Policy Engine, Audit, Secrets в навбар не выведены — доступ только через Dashboard.
- Навбар строится по `ROUTES` из `config/routes.ts`; активный пункт — по `location.pathname`. Переход — через `Link` и дополнительный `onClick` с вызовом eventBus.
### 2.5. Dashboard → Policy / Audit / Secrets
- На **Dashboard** три карточки: Движок политик, Журнал аудита, Защита секретов.
- По клику: `eventBus.emit(NAVIGATE)`, `addAuditEvent(...)` с типом `navigation`, затем `navigate(path)` на `/policies`, `/audit` или `/secrets`.
- Текст и статус карточек берутся из `useAppStore``systemStatus` (всегда «Активен» в текущей реализации).
### 2.6. Обработка ошибок
- **ErrorBoundary** (классовый компонент) оборачивает всё приложение в `App.tsx`. При ошибке в дереве React показывает экран «Произошла ошибка» с кнопками «Вернуться» (сброс состояния ошибки) и «Перезагрузить».
- **ErrorDisplay** — глобальный тост: показывает `error` из store, автоскрытие через 10 с и кнопку закрытия. Чтобы сообщение появилось, кто-то должен вызвать `setError(...)`; в текущем коде вызовов нет (задел под будущее).
### 2.7. Связь Frontend ↔ Backend (Tauri)
- **Нет кастомных Tauri-команд**`lib.rs` только `tauri::Builder::default()`, setup с плагином log и `run()`).
- **Нет вызовов `invoke`** из UI — фронт не обращается к Rust-логике.
- **Capabilities:** в `default.json` разрешено только окно `main` и `core:default`. Специальных разрешений (файловая система, shell и т.д.) не настроено.
Итог: связь между фронтом и бэкендом ограничена тем, что Tauri загружает и отображает веб-интерфейс; обмена данными или командами между React и Rust нет.
---
## 3. Разделы и страницы
| Маршрут | Страница | Назначение по коду |
|--------|----------|--------------------|
| `/` | Dashboard | Панель с тремя карточками (Policy Engine, Audit Logger, Secrets Guard) и переходом на соответствующие страницы; блок «Система безопасности». |
| `/tasks` | Tasks | Задачи: пример списка задач, теги, категория (Аудит/Рефакторинг/Анализ/Другое), «пути к папкам» (folder links). Всё в локальном state, без сохранения и без отправки на backend. |
| `/reglamenty` | Reglamenty | Раздел «Регламенты» (АРМАК, ФАА, ЕАСА, Mura Menasa) — заглушка/контент по ТЗ. |
| `/tmc-zakupki` | TMCZakupki | ТМЦ и закупки — заглушка. |
| `/finances` | Finances | Финансы — заглушка. |
| `/personnel` | Personnel | Персонал — заглушка. |
| `/documents` | Documents | Документы — заглушка. |
| `/chat` | ChatAgent | Чат с агентом: ввод сообщений, кнопки «Очистка чата» и «Откат» (откат последнего сообщения). Ответ — захардкоженная строка (имитация через setTimeout). |
| `/policies` | PolicyEngine | Движок политик — страница по маршруту, доступ с Dashboard. |
| `/audit` | AuditLogger | Журнал аудита — страница по маршруту, доступ с Dashboard. |
| `/secrets` | SecretsGuard | Защита секретов — страница по маршруту, доступ с Dashboard. |
| `*` | NotFound | Страница «не найдено». |
Страницы PolicyEngine, AuditLogger, SecretsGuard, Reglamenty, TMCZakupki, Finances, Personnel, Documents в отчёте не разбирались построчно — констатируются как реализованные экраны с навигацией; бизнес-логика (правила, логи, секреты, регламенты и т.д.) может быть минимальной или заглушками.
---
## 4. Backend (Rust / Tauri) и desktop-core
### 4.1. Что есть
- **main.rs:** точка входа, `windows_subsystem = "windows"` в release, вызов `app_lib::run()`.
- **lib.rs:** сборка приложения Tauri, в `setup` при `cfg!(debug_assertions)` подключается `tauri_plugin_log` (LevelFilter::Info), затем `run(tauri::generate_context!())`.
- **tauri.conf.json:** конфигурация окна, сборки, путей к UI (см. выше).
- **capabilities/default.json:** одно окно `main`, права `core:default`.
- **Cargo.toml:** зависимости — tauri 2.9.5, tauri-plugin-log 2, serde, serde_json, log; сборка — tauri-build 2.5.3.
Кастомных команд, обработчиков событий Tauri, доступа к файлам/сети из Rust — нет.
### 4.2. desktop-core
- В репозитории есть каталог `desktop-core/tools/project-auditor/` с файлом `index.ts`. Файл **пустой** — логика аудитора проекта в Tauri-приложении не реализована и из приложения не вызывается.
---
## 5. Промпты и ИИ
- В кодовой базе **papa-yu (Tauri + UI) нет ни одного промпта** к внешним ИИ/LLM и нет интеграции с API языковых моделей.
- **Чат с агентом** (`ChatAgent.tsx`): ответ агента — фиксированная строка: *«Ответ ИИ агента будет отображаться здесь. Результаты действий агента подключаются к backend.»* — выводится через `setTimeout` через 500 ms после отправки сообщения пользователем. То есть реализована только **заглушка диалога**; реальный диалог с ИИ и промпты планируются к подключению позже.
---
## 6. Связь с другими папками (по документации)
По файлу **СИНХРОНИЗАЦИЯ_ПАПОК.md**:
- **`~/Desktop/папа-ю`** — документация и ТЗ (спеки, планы). Кода нет; используется как ориентир.
- **`~/PAPA/PAPA-YU`** — полная версия на **Electron** + React (Dashboard, Tasks, Policy Engine, Chat и др.). Запуск: `npm run dev:electron` / `npm run build:electron`. Рассматривается как источник фич для переноса в Tauri-версию.
- **`~/papa-yu`** — основное десктоп-приложение на **Tauri** + React; единая точка запуска и текущая разработка.
Текущий отчёт относится только к коду в **`~/papa-yu`** (и при необходимости к упомянутому в нём `desktop-core` и `index/manifest.json`).
---
## 7. Краткие выводы
1. **Архитектура:** двухслойная — Tauri 2 (оболочка + лог в debug) и SPA на React 19 (Vite 7, HashRouter, Zustand, свой event-bus).
2. **Связи:** роутинг и store синхронизированы (currentRoute); навбар и Dashboard ведут на все заявленные страницы; event-bus и ErrorDisplay заложены, но мало задействованы; **обмена с Rust нет** — кастомных команд и invoke нет.
3. **Разделы:** все перечисленные в ТЗ маршруты присутствуют; часть страниц — заглушки или минимальный UI; задачи и чат работают только на клиенте (чат — с фиксированным ответом).
4. **Backend:** только запуск Tauri и плагин логов; desktop-core/project-auditor пустой.
5. **Промпты:** в проекте papa-yu промптов и интеграции с ИИ нет; чат — заглушка с текстом про будущее подключение к backend.
Документ подготовлен как отчёт по текущему состоянию программы **без предложений по исправлению кода**.

View File

@ -0,0 +1,161 @@
# Отчёт о выполненных улучшениях
**Дата:** 29 января 2026
**Основа:** план улучшений от P0 до P2 (обновления, пути, скрипт, чистка, диагностика, контракты, безопасность).
---
## 1) P0 — Обновления: довести до рабочего состояния
### Сделано
- **CI (GitHub Actions):**
- **`.github/workflows/ci.yml`** — при push/PR: lint и typecheck UI, `cargo check` backend.
- **`.github/workflows/release.yml`** — при push тега `v*`: сборка на macOS, подпись (если задан `TAURI_SIGNING_PRIVATE_KEY` в Secrets), публикация в GitHub Releases и генерация `latest.json` (опция `includeUpdaterJson: true` в tauri-action).
- **Pubkey:** В конфиге по-прежнему заглушка `REPLACE_WITH_PUBLIC_KEY_AFTER_tauri_signer_generate`. Инструкция по генерации ключей и подстановке публичного ключа — в `СБОРКА_И_ОБНОВЛЕНИЯ.md`.
- **UI «Обновления»:**
- Маршрут **`/updates`**, страница **Updates** (`desktop/ui/src/pages/Updates.tsx`): текущая версия (через getVersion), канал (stable), URL endpoint, кнопка «Проверить обновления», статус (успех/ошибка), лог операций и кнопка «Скопировать лог».
- **Навигация:** В шапку добавлены пункты «Обновления» и «Диагностика» (ссылки на `/updates` и `/diagnostics`).
### Результат
Проверка обновлений вынесена на отдельный экран с логом и копированием. Релизы можно автоматизировать через тег и Secrets; после подстановки pubkey и публикации подписанного релиза кнопка «Проверить обновления» перестаёт быть источником непонятных ошибок.
---
## 2) P0P1 — Убрать зависимость от `$HOME/PAPA-YU`
### Сделано
- **Пути сборки в `tauri.conf.json`:**
- `beforeDevCommand` и `beforeBuildCommand` используют переменную окружения **`PAPAYU_PROJECT_ROOT`** с запасным значением `$HOME/PAPA-YU`:
- `cd "${PAPAYU_PROJECT_ROOT:-$HOME/PAPA-YU}/desktop/ui" && ...`
- При другом расположении репозитория достаточно задать `export PAPAYU_PROJECT_ROOT=/путь/к/PAPA-YU` перед `cargo tauri dev` или `cargo tauri build`.
- **Данные приложения:** Уже используют системные директории Tauri (`app_data_dir`, `app_config_dir`) — не привязаны к $HOME. Пути видны на экране «Диагностика».
- **Миграция со старого пути:** Не реализована (при необходимости можно добавить проверку существования старого каталога и предложение импорта в настройках).
### Результат
Сборка и dev не жёстко привязаны к `$HOME/PAPA-YU`; данные приложения хранятся в системных путях. Единый источник описания путей — `СБОРКА_И_ОБНОВЛЕНИЯ.md`.
---
## 3) P1 — `PAPA YU.command`: устойчивый запуск
### Сделано
- В скрипте явно задана переменная **`SCRIPT_DIR`** как каталог, в котором лежит скрипт: `SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"`, далее `ROOT="$SCRIPT_DIR"`. Путь к .app вычисляется от `ROOT` (т.е. от расположения скрипта), а не от текущей рабочей директории.
- В комментарии указано, что запуск устойчив к текущей директории.
### Результат
Двойной клик по скрипту из любого места (или запуск из терминала с любой cwd) корректно находит `desktop/src-tauri/target/release/bundle/macos/` относительно каталога, где лежит `PAPA YU.command`.
---
## 4) P1 — Чистка/упрощение репозитория
### Сделано
- **desktop-core:** Добавлен **`desktop-core/README.md`** с пометкой, что каталог сохранён для совместимости, а `tools/project-auditor/` основным приложением не используется.
- **index:** Добавлен **`index/README.md`** с пояснением, что `manifest.json` не используется Tauri/UI напрямую.
- **CI:** В `.github/workflows/ci.yml` добавлены шаги **lint** и **typecheck** (UI), а также **cargo check** (backend); они выполняются при push/PR в main/master.
### Результат
Неиспользуемые каталоги помечены; один запуск CI проверяет lint, TypeScript и сборку Rust без полной сборки приложения.
---
## 5) P1 — Стабильность UX: прогресс, ошибки, логи
### Сделано
- **Экран «Диагностика»** (`/diagnostics`, страница **Diagnostics**):
- Версии: приложение (из `get_app_info`), Tauri (getVersion).
- Пути: `app_data_dir`, `app_config_dir` (системные директории Tauri/OS).
- Состояние обновлений: endpoint, требование подписи.
- Кнопки: «Скопировать отчёт» и «Экспортировать логи» (скачивание текстового файла с тем же содержимым).
- **Backend:** Добавлена команда **`get_app_info`** (Rust), возвращающая `version`, `app_data_dir`, `app_config_dir` для отображения на экране диагностики.
- **Человекочитаемые сообщения об ошибках updater** уже были внедрены ранее (в Layout при ошибке проверки обновлений показывается короткое сообщение вместо сырого текста).
### Результат
Единый экран диагностики с версиями, путями и экспортом логов; пользователь может скопировать или сохранить отчёт для поддержки.
---
## 6) P1P2 — Контракты между UI и Tauri
### Сделано
- Добавлен документ **`docs/CONTRACTS.md`**:
- Рекомендуемый формат ответов команд (ok/data, error/code/details).
- Таблица команд: `analyze_project`, `preview_actions`, `apply_actions`, `undo_last`, `get_app_info` (вход/выход, файлы UI).
- Таблица событий: `analyze_progress` (payload, где эмитится/слушается).
- Краткое описание транзакционности apply/undo (snapshot, сессия, last_session.txt).
### Результат
Один источник правды для контрактов UI ↔ Tauri; при изменении форматов можно версионировать payload событий и расширять манифест сессий.
---
## 7) P1P2 — Безопасность
### Проверено (без изменений кода)
- **Capabilities:** В `capabilities/default.json` заданы только: `core:default`, `dialog:allow-open`, `core:event:allow-listen`, `updater:default`, `process:allow-restart`. Доступа к FS или shell нет — принцип наименьших привилегий соблюдён.
- **Валидация путей:** В `apply_actions.rs` и `preview_actions.rs` используется `safe_join` (запрет абсолютных путей и `..`).
- **Updater:** Подпись обязательна; в конфиге задаётся `pubkey`; неподписанные обновления не принимаются.
### Результат
Права минимальны; пути проверяются на бэкенде; обновления требуют подписи.
---
## 8) Что не делалось в рамках этого отчёта
- **P2 — Производительность:** Фоновые задачи, cancel token, виртуализация списков, debounce не реализовывались (оставлены как следующий этап).
- **Миграция с $HOME/PAPA-YU:** Не реализована; при необходимости можно добавить проверку старого пути и UI импорта.
- **Persisted store для «рабочей директории»:** Выбор папки в Задачах по-прежнему не сохраняется глобально в настройках; при желании можно добавить позже.
---
## Сводка изменённых/добавленных файлов
| Файл | Действие |
|------|----------|
| `.github/workflows/ci.yml` | Создан — lint, typecheck, cargo check |
| `.github/workflows/release.yml` | Создан — сборка, подпись, релиз, latest.json |
| `desktop/src-tauri/tauri.conf.json` | Пути сборки через `PAPAYU_PROJECT_ROOT` |
| `desktop/src-tauri/src/commands/get_app_info.rs` | Создан — команда для диагностики |
| `desktop/src-tauri/src/commands/mod.rs` | Подключён get_app_info |
| `desktop/src-tauri/src/lib.rs` | Зарегистрирован get_app_info |
| `desktop/ui/src/config/routes.ts` | Маршруты UPDATES, DIAGNOSTICS |
| `desktop/ui/src/App.tsx` | Роуты /updates, /diagnostics |
| `desktop/ui/src/pages/Updates.tsx` | Создан — экран «Обновления» |
| `desktop/ui/src/pages/Diagnostics.tsx` | Создан — экран «Диагностика» |
| `desktop/ui/src/components/layout/Layout.tsx` | В навбар добавлены «Обновления», «Диагностика» |
| `PAPA YU.command` | Устойчивый путь через SCRIPT_DIR |
| `desktop-core/README.md` | Создан — пометка архива |
| `index/README.md` | Создан — пометка архива |
| `docs/CONTRACTS.md` | Создан — контракты UI ↔ Tauri |
| `СБОРКА_И_ОБНОВЛЕНИЯ.md` | Описание PAPAYU_PROJECT_ROOT и путей |
| `ОТЧЁТ_УЛУЧШЕНИЙ.md` | Этот отчёт |
---
## Рекомендации по следующим шагам
1. Сгенерировать ключи подписи (`npx tauri signer generate`), подставить публичный ключ в `tauri.conf.json`, добавить `TAURI_SIGNING_PRIVATE_KEY` в GitHub Secrets и один раз выпустить релиз по тегу (например `v0.1.0`) — после этого проверка обновлений будет работать полностью.
2. При необходимости добавить в настройки сохранение «последней выбранной папки проекта» и миграцию со старого $HOME/PAPA-YU.
3. При росте нагрузки на анализ/превью — вынести тяжёлые операции в фон с возможностью отмены и добавить debounce/кэш на UI.

View File

@ -0,0 +1,116 @@
# PAPA YU — сборка DMG и обновления
## Единые пути и связи
| Назначение | Значение |
|------------|----------|
| Репозиторий | `https://github.com/yrippert-maker/papayu` |
| Обновления (endpoint) | `https://github.com/yrippert-maker/papayu/releases/latest/download/latest.json` |
| Установка приложения (macOS) | `/Applications/PAPA YU.app` (рекомендуется) |
| Данные приложения | Системная папка приложения (Tauri `app_data_dir`) — не зависит от $HOME |
| Логотип в проекте | `desktop/src-tauri/icons/icon_source.png`, `desktop/ui/public/logo-papa-yu.png` |
| Путь проекта при сборке | Задаётся переменной `PAPAYU_PROJECT_ROOT`; по умолчанию `$HOME/PAPA-YU` |
Конфиг: `desktop/src-tauri/tauri.conf.json` (пути сборки, `plugins.updater.endpoints`). Чтобы сборка работала из другого каталога, задайте `export PAPAYU_PROJECT_ROOT=/путь/к/PAPA-YU` перед `cargo tauri dev` или `cargo tauri build`.
---
## Сборка DMG (один запускающий файл)
Из корня проекта (или из `desktop/src-tauri`):
```bash
cd ~/PAPA-YU/desktop/src-tauri
cargo tauri build
```
Результат на macOS:
- **.app:** `desktop/src-tauri/target/release/bundle/macos/PAPA YU.app`
- **.dmg:** `desktop/src-tauri/target/release/bundle/dmg/` (если targets включают dmg)
Только DMG:
```bash
cargo tauri build --bundles dmg
```
Пользователь открывает DMG → перетаскивает «PAPA YU» в «Программы» → запускает приложение. Дальнейшие изменения — через кнопку обновления в приложении.
---
## Кнопка обновления (новый логотип)
В интерфейсе рядом с логотипом PAPA YU есть кнопка с иконкой логотипа и стрелкой загрузки. По нажатию:
1. Проверяется наличие новой версии по GitHub Releases.
2. При наличии — скачивание и установка, затем перезапуск приложения.
Единая точка входа для обновлений — эта кнопка; пути и URL заданы в конфиге.
---
## Подписание обновлений (обязательно для updater)
**Ключи (публичный и приватный) вы получаете из команды генерации, указанной ниже.** Публичный ключ вставляется в конфиг; приватный — хранится локально и в GitHub Secrets для подписи релизов.
---
### Как сгенерировать ключи
**Один блок команд** (выполните в **терминале** — команда запросит пароль для защиты ключа; можно нажать Enter для пустого пароля):
```bash
mkdir -p ~/.tauri
cd ~/PAPA-YU/desktop/src-tauri
cargo tauri signer generate -w ~/.tauri/papayu.key
```
В проекте нет Tauri CLI в npm, поэтому используется **`cargo tauri signer generate`** из каталога `desktop/src-tauri` (нужен установленный Rust и Cargo). При запросе пароля введите свой или нажмите Enter для пустого пароля.
После выполнения в консоль будет выведен **публичный ключ** — скопируйте его целиком для шага «Вставить публичный ключ» ниже. Приватный ключ сохранится в `~/.tauri/papayu.key`.
---
**По шагам:**
1. **Создайте каталог для ключа** (опционально):
```bash
mkdir -p ~/.tauri
```
2. **Перейдите в каталог Tauri и запустите генерацию ключей** (обязательно в **интерактивном терминале** — команда запросит пароль):
```bash
cd ~/PAPA-YU/desktop/src-tauri
cargo tauri signer generate -w ~/.tauri/papayu.key
```
Флаг `-w` (или `--write-keys`) задаёт файл, **куда будет записан приватный ключ**. Путь `~/.tauri/papayu.key` — пример; можно использовать свой (например `./papayu.key` в репозитории, но тогда не коммитьте этот файл).
3. **Результат команды:**
- В консоль выведется **публичный ключ** — длинная строка, обычно начинается с `dW50cnVzdGVk...` или похожего (base64).
- **Приватный ключ** сохраняется в файл, указанный в `-w` (например `~/.tauri/papayu.key`). Этот файл никому не передавайте и не добавляйте в репозиторий.
4. **Куда подставить ключи:**
- **Публичный ключ** — в `desktop/src-tauri/tauri.conf.json` в блоке `plugins.updater.pubkey` (замените заглушку `REPLACE_WITH_PUBLIC_KEY_AFTER_tauri_signer_generate` на выведенную строку целиком).
- **Приватный ключ** — для локальной подписи передаётся через переменную окружения `TAURI_SIGNING_PRIVATE_KEY` или через файл `-w`. Для GitHub Actions добавьте содержимое файла `~/.tauri/papayu.key` в секрет репозитория с именем `TAURI_SIGNING_PRIVATE_KEY`.
**Проверка:** после подстановки публичного ключа в конфиг пересоберите приложение и опубликуйте релиз с подписью; кнопка «Проверить обновления» в приложении должна перестать выдавать ошибку подписи.
---
Обновления должны быть подписаны. Один раз:
1. Сгенерировать ключи (см. раздел «Как сгенерировать ключи» выше).
2. Вставить публичный ключ в `desktop/src-tauri/tauri.conf.json`:
```json
"plugins": {
"updater": {
"endpoints": ["https://github.com/yrippert-maker/papayu/releases/latest/download/latest.json"],
"pubkey": "ВАШ_ПУБЛИЧНЫЙ_КЛЮЧ"
}
}
```
Сейчас в конфиге стоит заглушка `REPLACE_WITH_PUBLIC_KEY_AFTER_tauri_signer_generate` — замените на ключ из шага 1.
3. При сборке релиза подписывать артефакты приватным ключом (переменная `TAURI_SIGNING_PRIVATE_KEY` или `-w ~/.tauri/papayu.key`). Для GitHub Releases можно использовать [tauri-action](https://github.com/tauri-apps/tauri-action) с опцией подписания и публикации `latest.json`.
После этого кнопка обновления будет работать с единым endpoint и путями, заданными выше.

View File

@ -0,0 +1,51 @@
# Синхронизация папок PAPA-YU
Три папки работают как одно целое:
| Папка | Назначение |
|-------|------------|
| **`~/Desktop/папа-ю`** | Документация и ТЗ: спецификации, планы, корректные документы. Не содержит кода. |
| **`~/PAPA/PAPA-YU`** | Полная версия приложения на **Electron** + React (Dashboard, Tasks, Policy Engine, Chat и др.). Альтернативный запуск: `npm run dev:electron` или сборка через `npm run build:electron`. |
| **`~/papa-yu`** | **Основное десктоп-приложение на Tauri** + React. Здесь единая точка запуска. |
---
## Запуск (без терминала)
**Основная кнопка — только запуск:**
- **`PAPA YU.command`** — главная кнопка. Двойной клик **сразу запускает** программу (сборка не выполняется).
Если приложение ещё не собрано, скрипт подскажет, что нужно сначала выполнить сборку.
**Первая сборка или обновление:**
- **`PAPA YU — Сборка и запуск.command`** — запускайте один раз при первой установке или после обновления кода. Скрипт соберёт приложение и откроет его.
**Иконка основной кнопки (по желанию):**
Откройте «Сведения» (Get Info) у файла `PAPA YU.command`, перетащите иконку из `PAPA YU.app` (после сборки) в угол иконки в окне «Сведения» — кнопка в Finder будет с иконкой приложения.
---
## Запуск для разработки (с терминалом)
```bash
cd ~/papa-yu/desktop/src-tauri
cargo tauri dev
```
---
## Исправленные моменты
- **tauri.conf.json** приведён к схеме Tauri v2: `devPath``devUrl`, `distDir``frontendDist`. Пути `beforeDevCommand` и `beforeBuildCommand` используют `$HOME/papa-yu/desktop/ui`, чтобы сборка работала независимо от текущей директории.
- **Идентификатор приложения:** заменён с `com.tauri.dev` на `com.papa-yu` (уникальный и без суффикса `.app` для macOS).
- **Сборка:** в скрипте используется `CI=false` при вызове `cargo tauri build`, чтобы избежать ошибки `--ci` в некоторых окружениях.
- **Основная кнопка:** `PAPA YU.command` — только запуск приложения (без сборки). Для первой сборки используется `PAPA YU — Сборка и запуск.command`.
---
## Связь папок
- **папа-ю** — только чтение: ТЗ и спецификации для ориентира.
- **PAPA-YU** — полный код (Electron); при необходимости оттуда можно переносить фичи в Tauri-версию в `papa-yu`.
- **papa-yu** — основная десктоп-версия (Tauri); здесь ведётся разработка и отсюда идёт единый запуск.

52
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,52 @@
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
lint-typecheck:
name: Lint & TypeScript
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: desktop/ui/package-lock.json
- name: Install UI deps
run: cd desktop/ui && npm ci
- name: Lint
run: cd desktop/ui && npm run lint
- name: TypeScript
run: cd desktop/ui && npx tsc --noEmit
rust-check:
name: Rust check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
desktop/src-tauri/target
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-cargo-${{ hashFiles('desktop/src-tauri/Cargo.lock') }}
- name: Check
run: cd desktop/src-tauri && cargo check

View File

@ -1,64 +0,0 @@
name: CI (fmt, clippy, audit, protocol, frontend build)
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install and build
run: npm ci && npm run build
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Format check
run: cd src-tauri && cargo fmt --check
- name: Clippy
run: cd src-tauri && cargo clippy --all-targets --
- name: Install cargo-audit
run: cargo install cargo-audit
- name: Cargo audit
run: cd src-tauri && cargo audit
continue-on-error: true
- name: Install cargo-deny
run: cargo install cargo-deny
- name: Cargo deny
run: cd src-tauri && cargo deny check
continue-on-error: true
- name: Tests (all, including golden_traces)
run: cd src-tauri && cargo test --no-fail-fast

111
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,111 @@
# Релиз: сборка .app / DMG / exe / deb, подпись (при наличии ключа), публикация в GitHub Releases и latest.json
# Настройка: в Secrets репозитория добавить TAURI_SIGNING_PRIVATE_KEY (приватный ключ от tauri signer generate)
name: Release
on:
push:
tags:
- 'v*'
release:
types: [published]
concurrency:
group: release-${{ github.event.release.tag_name || github.ref_name }}
cancel-in-progress: false
permissions:
contents: write
jobs:
release:
name: Build & Release (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: desktop/ui/package-lock.json
- name: Install Rust toolchain (macOS)
if: matrix.os == 'macos-latest'
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: aarch64-apple-darwin,x86_64-apple-darwin
- name: Install Rust toolchain (other)
if: matrix.os != 'macos-latest'
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Install Linux deps
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev patchelf pkg-config
- name: Install UI deps
run: cd desktop/ui && npm ci
- name: Debug tauri config
if: matrix.os == 'macos-latest'
run: |
echo "PWD=$(pwd)"
grep -n "beforeBuildCommand" desktop/src-tauri/tauri.conf.json || true
find . -maxdepth 4 \( -name "tauri.conf.json" -o -name "tauri.config.*" \) || true
- name: Build Tauri (release)
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
PAPAYU_PROJECT_ROOT: ${{ github.workspace }}
with:
projectPath: desktop/src-tauri
includeUpdaterJson: true
tagName: ${{ github.event.release.tag_name || github.ref_name }}
releaseName: ${{ github.event.release.tag_name || github.ref_name }}
- name: Smoke — check macOS bundle
if: matrix.os == 'macos-latest'
run: |
ls -la desktop/src-tauri/target/release/bundle || true
find desktop/src-tauri/target/release/bundle -maxdepth 4 -type f 2>/dev/null | head -100
- name: Smoke — check Windows bundle
if: matrix.os == 'windows-latest'
run: dir desktop\src-tauri\target\release\bundle
- name: Smoke — check Linux bundle
if: matrix.os == 'ubuntu-latest'
run: |
ls -la desktop/src-tauri/target/release/bundle || true
find desktop/src-tauri/target/release/bundle -maxdepth 4 -type f 2>/dev/null | head -100
- name: Validate latest.json schema (if present)
id: validate-latest-json
if: matrix.os == 'macos-latest'
run: |
F=$(find desktop/src-tauri -name 'latest.json' -type f 2>/dev/null | head -1)
if [ -n "$F" ]; then
npx --yes ajv-cli@latest validate -s docs/latest.schema.json -d "$F" || (echo "--- latest.json (first 50 lines) ---"; head -50 "$F"; exit 1)
else
echo "latest.json not found in workspace (uploaded by tauri-action); skipping validation"
fi
- name: Dump latest.json on validation failure
if: failure() && matrix.os == 'macos-latest'
run: |
F=$(find desktop/src-tauri -name 'latest.json' -type f 2>/dev/null | head -1)
if [ -n "$F" ]; then echo "--- latest.json ---"; head -80 "$F"; fi

47
.gitignore vendored
View File

@ -1,44 +1,19 @@
# Секреты — не коммитить
# env
.env
.env.*
!.env.example
*.key
*.pem
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Vite / Build
dist/
*.local
# Rust / Tauri
src-tauri/target/
**/*.rs.bk
Cargo.lock
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
*.env
# OS
.DS_Store
Thumbs.db
# Node
node_modules
dist
build
# Rust
/desktop/src-tauri/target
/desktop/src-tauri/.cargo
# Logs
*.log
# TypeScript cache
*.tsbuildinfo
tsconfig.tsbuildinfo
tsconfig.node.tsbuildinfo
# Временные файлы
*.tmp
*.temp

108
AUDIT.md
View File

@ -1,108 +0,0 @@
# Полный аудит приложения PAPA YU
**Дата:** 2026-01-28
**Цель:** проверка компонентов, связей UI ↔ backend, исправление отображения блока выбора пути к папкам и заключение.
---
## 1. Структура проекта
| Путь | Назначение |
|------|------------|
| `src/` | React (Vite) — UI |
| `src/pages/Tasks.tsx` | Страница «Задачи» — анализ проекта, выбор папок, preview/apply/undo/redo |
| `src/pages/Dashboard.tsx` | Страница «Панель управления» |
| `src/App.tsx` | Роутинг, Layout (header + main) |
| `src-tauri/src/` | Rust (Tauri) — команды, tx, types |
---
## 2. Проверенные компоненты
### 2.1 UI
- **App.tsx** — маршруты `/` (Tasks), `/control-panel` (Dashboard). Layout: header с навигацией, main с `overflow: visible` (исправлено).
- **Tasks.tsx** — блок «Путь к папке проекта»:
- Расположен **первым** под заголовком «Анализ проекта».
- Секция с `data-section="path-selection"` и классом `tasks-sources`.
- Две кнопки: **«Выбрать папку»** (основная синяя), **«+ Добавить ещё папку»**.
- Список выбранных папок или текст «Папки не выбраны. Нажмите кнопку «Выбрать папку» выше.».
- Ниже: поле ввода пути и кнопка «Отправить».
- **index.css** — правило для `.tasks-sources[data-section="path-selection"]`: `display: block !important`, `visibility: visible !important`, чтобы блок не скрывался.
### 2.2 Связи UI → Backend (invoke)
| Действие в UI | Команда Tauri | Файл Rust |
|---------------|---------------|-----------|
| Загрузка списка папок при монтировании | `get_folder_links` | folder_links.rs |
| Сохранение списка папок | `set_folder_links` (links: { paths }) | folder_links.rs |
| Анализ + preview + apply (пакет) | `run_batch_cmd` (payload: paths, confirm_apply, auto_check, selected_actions) | run_batch.rs |
| Состояние undo/redo | `get_undo_redo_state_cmd` | undo_last.rs, tx/store.rs |
| Откат | `undo_last` | undo_last.rs |
| Повтор | `redo_last` | redo_last.rs |
| Генерация плана (v2.4) | `generate_actions` | generate_actions.rs |
Выбор папки через диалог: `open({ directory: true })` из `@tauri-apps/plugin-dialog` — плагин зарегистрирован в `lib.rs` (`tauri_plugin_dialog::init()`).
---
## 3. Backend (Rust)
### 3.1 Зарегистрированные команды (lib.rs)
- `analyze_project_cmd`, `preview_actions_cmd`, `apply_actions_cmd`, `run_batch_cmd`
- `undo_last`, `undo_available`, `redo_last`, `get_undo_redo_state_cmd`
- `generate_actions`
- `get_folder_links`, `set_folder_links`
### 3.2 Модули
- **commands/** — analyze_project, apply_actions, preview_actions, run_batch, undo_last, redo_last, generate_actions, folder_links, auto_check.
- **tx/** — limits (preflight), store (undo/redo stacks), mod (snapshot_before, rollback_tx, apply_actions_to_disk, collect_rel_paths, write_manifest, read_manifest, etc.).
- **types** — ApplyPayload, ApplyResult, TxManifest, Action, ActionKind, AnalyzeReport, BatchPayload, BatchEvent, etc.
### 3.3 Folder links
- `FolderLinks { paths: Vec<String> }` — сериализуется в `app_data_dir/folder_links.json`.
- `load_folder_links`, `save_folder_links` — используются в `get_folder_links` / `set_folder_links`.
Связь с UI: при загрузке Tasks вызывается `get_folder_links` и при необходимости обновляется `folderLinks`; при добавлении/удалении папки вызывается `set_folder_links`. Формат `{ links: { paths } }` соответствует типу `FolderLinks`.
---
## 4. Внесённые исправления
1. **Блок выбора пути к папке (Tasks.tsx)**
- Секция «Путь к папке проекта» вынесена в начало страницы (сразу под заголовком).
- Заголовок секции: «Путь к папке проекта», подпись с указанием нажать кнопку или ввести путь.
- Кнопки «Выбрать папку» и «+ Добавить ещё папку» оформлены заметно (размер, контраст, тень у основной).
- Добавлены `className="tasks-sources"` и `data-section="path-selection"` для стилей и отладки.
- Секция с рамкой, фоном и `minHeight: 140px`, чтобы блок всегда занимал место и был виден.
- В строке ввода оставлены только поле пути и «Отправить» (дублирующая кнопка «Выбрать папку» убрана, чтобы не путать с блоком выше).
2. **Layout (App.tsx)**
- Для `main` заданы `overflow: visible` и `minHeight: 0`, чтобы контент не обрезался.
3. **Глобальные стили (index.css)**
- Добавлено правило для `.tasks-sources[data-section="path-selection"]`: блок принудительно видим.
---
## 5. Рекомендации после обновления кода
- Перезапустить приложение: `cd papa-yu/src-tauri && cargo tauri dev`.
- В браузере/WebView сделать жёсткое обновление (Ctrl+Shift+R / Cmd+Shift+R), чтобы подтянуть новый UI без кэша.
- Если используется только фронт (Vite): перезапустить `npm run dev` и обновить страницу.
После этого в начале страницы «Задачи» должен отображаться блок «Путь к папке проекта» с кнопками «Выбрать папку» и «+ Добавить ещё папку» и списком выбранных папок.
---
## 6. Заключение
- **Компоненты:** App, Tasks, Dashboard, Layout и точки входа (main.tsx, index.html) проверены; маршруты и вложенность корректны.
- **Связи UI ↔ backend:** вызовы `get_folder_links`, `set_folder_links`, `run_batch_cmd`, `get_undo_redo_state_cmd`, `undo_last`, `redo_last` соответствуют зарегистрированным командам и типам (FolderLinks, BatchPayload, ApplyPayload и т.д.).
- **Исправления:** блок выбора пути к папкам сделан первым и визуально выделен; добавлены гарантии видимости через разметку и CSS; дублирование кнопки убрано.
- **Ошибки:** явных ошибок в компонентах и связях не выявлено. Если на экране по-прежнему не видно кнопок и блока, наиболее вероятны кэш сборки или WebView — выполнить перезапуск и жёсткое обновление по п. 5.
Аудит выполнен. Состояние: **исправления внесены, рекомендации по обновлению даны.**

View File

@ -1,106 +0,0 @@
# Changelog
Все значимые изменения в проекте PAPA YU фиксируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/).
---
## [2.4.5] — 2025-01-31
### Добавлено
- **Distill Online Research → Project Note:** кнопка «Save as Project Note» в блоке Online Research (Задачи) — сохраняет результат online research в domain notes проекта.
- **Контекст v3:** FILE-блоки при protocol_version=3 теперь включают sha256 (base_sha256 для EDIT_FILE). Исправлено: ранее sha256 добавлялся только для v2.
- **C1C3 Protocol v3:** schema (after minLength=0, maxLength 50k для before/after), валидатор (after может быть пустым для delete), repair-промпты для ERR_EDIT_ANCHOR_NOT_FOUND / ERR_EDIT_BEFORE_NOT_FOUND / ERR_EDIT_AMBIGUOUS, golden traces v3 + CI. Обновлён schema_hash в fixtures.
### Обновлено
- `docs/IMPLEMENTATION_STATUS_ABC.md`: A4, B3, C отмечены как реализованные.
---
## [2.4.4] — 2025-01-31
### Protocol stability (v1)
- **Schema version:** `LLM_PLAN_SCHEMA_VERSION=1`, `x_schema_version` в схеме, `schema_hash` (sha256) в trace.
- **Версионирование:** при изменении контракта ответа LLM — увеличивать schema_version; trace содержит schema_version и schema_hash для воспроизводимости.
- **Рекомендуемый тег:** `v1.0.0` или `v0.x` — зафиксировать «стабильный релиз» перед введением v2.
### Добавлено
- **UX:** история сессий по проекту — блок «История сессий» с раскрывающимся списком сессий (дата, количество событий, последнее сообщение); обновление списка после agentic run.
- **UX:** в блоке профиля отображаются лимиты (max_actions_per_tx, timeout_sec).
- **UX:** фильтр расширений в диалоге «Прикрепить файл» (исходники и конфиги: .ts, .tsx, .js, .jsx, .rs, .py, .json, .toml, .md, .yml, .yaml, .css, .html, .xml).
- **UX:** горячие клавиши — Ctrl+Enter (Cmd+Enter): отправить/запустить анализ; Escape: сбросить превью изменений.
- **UX:** тёмная тема — переключатель в боковой панели, CSS-переменные для обоих режимов, сохранение выбора в localStorage, поддержка системных настроек.
- **UX:** экспорт/импорт настроек — кнопки в боковой панели для сохранения и восстановления всех настроек (проекты, профили, сессии, папки) в JSON-файл.
- **Тестирование:** юнит-тесты в Rust для `detect_project_type`, `get_project_limits`, `is_protected_file`, `is_text_allowed`, `settings_export` (18 тестов).
- **Тестирование:** тестовые фикстуры в `tests/fixtures/` — минимальные проекты для E2E тестирования (minimal-node, minimal-rust).
- **Документация:** E2E сценарий в `docs/E2E_SCENARIO.md`; обновлён README до v2.4.4; README для тестов в `tests/README.md`.
- **Контекст прикреплённых файлов:** в отчёт и batch передаётся список прикреплённых файлов (`attached_files` в `BatchPayload` и `AnalyzeReport`); фронт передаёт его при вызове `runBatchCmd`.
- **LLM-планировщик:** при заданном `PAPAYU_LLM_API_URL` команда «Предложить исправления» вызывает OpenAI-совместимый API (OpenAI, Ollama и др.); ответ парсится в план действий (CREATE_FILE, CREATE_DIR и т.д.). Без настройки — эвристический план по отчёту.
- **Бэкенд:** команды `export_settings` и `import_settings` для резервного копирования и переноса настроек между машинами.
- **Конфиг:** расширенный allowlist команд verify (`verify_allowlist.json`) — добавлены cargo clippy, tsc --noEmit, mypy, pytest --collect-only.
- **Инфраструктура:** инициализирован Git-репозиторий с улучшенным .gitignore.
- **Preview diff в propose flow:** после получения плана автоматически вызывается `preview_actions`, diffs отображаются в UI.
- **ERR_UPDATE_WITHOUT_BASE:** в режиме APPLY UPDATE_FILE разрешён только для файлов, прочитанных в Plan (FILE[path] или === path ===).
- **Protected paths:** denylist для `.env`, `*.pem`, `*.key`, `*.p12`, `id_rsa*`, `**/secrets/**`.
- **Content validation:** запрет NUL, >10% non-printable = ERR_PSEUDO_BINARY; лимиты max_path_len=240, max_actions=200, max_total_content_bytes=5MB.
- **EOL:** `PAPAYU_NORMALIZE_EOL=lf` — нормализация \r\n→\n и trailing newline.
- **Наблюдаемость:** trace_id (UUID) на каждый propose; лог-ивенты LLM_REQUEST_SENT, LLM_RESPONSE_OK, VALIDATION_FAILED, APPLY_SUCCESS, APPLY_ROLLBACK, PREVIEW_READY.
- **Трассировка:** `PAPAYU_TRACE=1` — запись в `.papa-yu/traces/<trace_id>.json`.
- **Детерминизм LLM:** temperature=0, max_tokens=65536, top_p=1, presence_penalty=0, frequency_penalty=0 (PAPAYU_LLM_TEMPERATURE, PAPAYU_LLM_MAX_TOKENS).
- **Capability detection:** при ошибке API response_format — автоматический retry без response_format (Ollama и др.).
- **Schema version:** `x_schema_version` в llm_response_schema.json; schema_hash (sha256) в trace; LLM_PLAN_SCHEMA_VERSION в prompt.
- **Кеш контекста:** read_file/search/logs/env кешируются в plan-цикле; CONTEXT_CACHE_HIT/MISS.
- **Контекст-диета:** PAPAYU_CONTEXT_MAX_FILES=8, MAX_FILE_CHARS=20k, MAX_TOTAL_CHARS=120k; head+tail truncation; MIN_CHARS_FOR_PRIORITY0=4k; CONTEXT_DIET_APPLIED.
- **Trace:** context_stats (files_count, dropped, total_chars, logs_chars, truncated) и cache_stats (hits/misses по env/logs/read/search, hit_rate).
- **Кеш logs:** ключ Logs включает `last_n` — разные last_n не пересекаются.
- **Golden traces:** эталонные fixtures в `docs/golden_traces/v1/` — формат protocol/request/context/result (без raw_content). Тест `golden_traces_v1_validate` валидирует schema_version, schema_hash, JSON schema, validate_actions, NO_CHANGES при apply+empty. Конвертер `trace_to_golden` (cargo run --bin trace_to_golden).
- **Compatibility matrix:** в PROTOCOL_V1.md — Provider Compatibility таблица и 5 поведенческих гарантий.
- **PROTOCOL_V2_PLAN.md:** план v2 (PATCH_FILE, REPLACE_RANGE, base_sha256).
- **make/npm shortcuts:** `make golden` (trace→fixture), `make test-protocol` (golden_traces_v1_validate).
- **CI:** `.github/workflows/protocol-check.yml` — golden_traces_v1_validate на push/PR.
- **Политика golden traces:** в docs/golden_traces/README.md — когда/как обновлять, при смене schema_hash.
- **Protocol v2 schema (plumbing):** `llm_response_schema_v2.json` — object-only, PATCH_FILE, base_sha256. `PAPAYU_PROTOCOL_VERSION=1|2` (default 1). schema_version и schema_hash динамические в trace.
- **V2 system prompt:** `FIX_PLAN_SYSTEM_PROMPT_V2` при protocol=2 и fix-plan/fixit.
- **Контекст v2:** FILE-блоки с sha256: `FILE[path] (sha256=...):` для base_sha256 в PATCH_FILE.
- **PATCH_FILE engine:** diffy, sha256_hex, looks_like_unified_diff, apply_unified_diff. ActionKind::PatchFile, apply_patch_file_tx, preview. ERR_PATCH_NOT_UNIFIED, ERR_BASE_MISMATCH, ERR_PATCH_APPLY_FAILED.
- **Коммит 5:** v2 prompt UPDATE_FILE запрещён для существующих. ERR_V2_UPDATE_EXISTING_FORBIDDEN (plan + apply). bytes_before/bytes_after в DiffItem. ERR_NON_UTF8_FILE docs.
- **Golden traces v2:** docs/golden_traces/v2/ (5 fixtures), golden_traces_v2_validate. CI: v1 + v2.
### Изменено
- Лимиты профиля применяются в `apply_actions_tx` и `run_batch` — при превышении `max_actions_per_tx` возвращается ошибка TOO_MANY_ACTIONS.
- Таймаут проверок в verify и auto_check задаётся из профиля (`timeout_sec`); в `verify_project` добавлен таймаут на выполнение каждой проверки (spawn + try_wait + kill при превышении).
- Синхронизированы версии в package.json, Cargo.toml и tauri.conf.json.
---
## [2.4.3] — ранее
### Реализовано
- Профиль по пути (тип проекта, лимиты, goal_template).
- Agentic run — цикл анализ → план → превью → применение → проверка → откат при ошибке.
- Прикрепление файлов, кнопка «Прикрепить файл».
- Guard опасных изменений (is_protected_file, is_text_allowed).
- Подтверждение Apply (user_confirmed).
- Единый API-слой (src/lib/tauri.ts), типы в src/lib/types.ts.
- Компоненты PathSelector, AgenticResult, хук useUndoRedo.
- Транзакционное apply с snapshot и откатом при падении auto_check.
- Undo/Redo по последней транзакции.
- Единый batch endpoint (run_batch): analyze → preview → apply (при confirmApply) → autoCheck.
---
## [2.3.2] — ранее
- Apply + Real Undo (snapshot в userData/history, откат при падении check).
- AutoCheck для Node, Rust, Python.
- Actions: README, .gitignore, tests/, .env.example.
- UX: двухфазное применение, кнопки «Показать исправления», «Применить», «Отмена», «Откатить последнее».
- Folder Links (localStorage + userData/folder_links.json).
- Брендинг PAPA YU, минимальный размер окна 1024×720.

View File

@ -1,24 +0,0 @@
.PHONY: golden golden-latest test-protocol test-all
# make golden TRACE_ID=<id> — из .papa-yu/traces/<id>.json
# make golden — из последней трассы (golden-latest)
golden:
@if [ -n "$$TRACE_ID" ]; then \
cd src-tauri && cargo run --bin trace_to_golden -- "$$TRACE_ID"; \
else \
$(MAKE) golden-latest; \
fi
golden-latest:
@LATEST=$$(ls -t .papa-yu/traces/*.json 2>/dev/null | head -1); \
if [ -z "$$LATEST" ]; then \
echo "No traces in .papa-yu/traces/. Run with PAPAYU_TRACE=1, propose fixes, then make golden."; \
exit 1; \
fi; \
cd src-tauri && cargo run --bin trace_to_golden -- "../$$LATEST"
test-protocol:
cd src-tauri && cargo test golden_traces
test-all:
cd src-tauri && cargo test

View File

@ -1,17 +0,0 @@
#!/bin/bash
# PAPA YU — сборка приложения и запуск (первая установка или после обновления кода).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
echo " Сборка PAPA YU..."
export CI=false
if ! npm run tauri build; then
echo ""
echo " Ошибка сборки. Проверьте: npm install, Rust и Xcode Command Line Tools."
read -n 1 -s -r -p " Нажмите любую клавишу..."
exit 1
fi
BUNDLE="$SCRIPT_DIR/src-tauri/target/release/bundle/macos/PAPA YU.app"
[ -d "$BUNDLE" ] && open "$BUNDLE" || open "$SCRIPT_DIR/src-tauri/target/release/bundle/macos"
echo " Готово."

View File

@ -1,7 +1,11 @@
#!/bin/bash
# PAPA YU — запуск приложения (двойной клик). Сборка не выполняется.
# PAPA YU — запуск приложения (основная кнопка)
# Двойной клик: сразу запускает программу. Сборка не выполняется.
# Путь к .app вычисляется от каталога, в котором лежит этот скрипт (устойчиво к текущей директории).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUNDLE_DIR="$SCRIPT_DIR/src-tauri/target/release/bundle/macos"
ROOT="$SCRIPT_DIR"
BUNDLE_DIR="$ROOT/desktop/src-tauri/target/release/bundle/macos"
find_app() {
[ -d "$BUNDLE_DIR/PAPA YU.app" ] && echo "$BUNDLE_DIR/PAPA YU.app" && return
@ -19,7 +23,8 @@ fi
echo ""
echo " PAPA YU не найден."
echo " Для первой сборки запустите: «PAPA YU — Сборка и запуск.command»"
echo " Для первой сборки откройте:"
echo " «PAPA YU — Сборка и запуск.command»"
echo ""
read -n 1 -s -r -p " Нажмите любую клавишу..."
exit 1

184
README.md
View File

@ -1,176 +1,32 @@
# PAPA YU v2.4.5
# PAPA YU
Десктопное приложение для анализа проекта и автоматических исправлений (README, .gitignore, tests/, структура) с **транзакционным apply**, **реальным undo** и **autoCheck с откатом**.
[![CI](https://github.com/yrippert-maker/papayu/actions/workflows/ci.yml/badge.svg)](https://github.com/yrippert-maker/papayu/actions/workflows/ci.yml)
## Единственная папка проекта
Десктопное приложение (Tauri 2 + React).
Вся разработка, сборка и запуск ведутся из **этой папки** (например `/Users/.../Desktop/papa-yu`). ТЗ и спецификации лежат отдельно в папке **папа-ю** на рабочем столе (не переносятся). Подробнее: `docs/ЕДИНАЯ_ПАПКАРОЕКТА.md`.
---
**Установка в «Программы» (запуск без терминала):** один раз двойной клик по **`Установить в Программы.command`** — приложение соберётся и скопируется в папку «Программы». После этого запускайте PAPA YU из Launchpad, Spotlight (Cmd+Пробел) или Finder → Программы, как любое другое приложение.
## Сборка и запуск
**Без установки:** двойной клик по `PAPA YU.command` (только запуск уже собранного .app) или по `PAPA YU — Сборка и запуск.command` (сборка + запуск).
- **Разработка:** из корня репозитория: `cd desktop/src-tauri && cargo tauri dev` (или из `desktop/ui``npm run dev`, отдельно backend по необходимости).
- **Сборка:** `cd desktop/src-tauri && cargo tauri build`.
- Подробнее: [docs/РЕЛИЗ_И_ОБНОВЛЕНИЯ.md](docs/РЕЛИЗ_И_ОБНОВЛЕНИЯ.md).
## Требования
---
- Node.js 18+
- Rust 1.70+
- npm
## Release process
## Запуск
- Релизы собираются в GitHub Actions по тегам `v*` (workflow **Release**).
- Чеклист выпуска релиза и проверки обновлений: [docs/РЕЛИЗ_И_ОБНОВЛЕНИЯ.md](docs/РЕЛИЗ_И_ОБНОВЛЕНИЯ.md).
- Если сборка падает: [docs/CI_ОТЛАДКА_РЕЛИЗА.md](docs/CI_ОТЛАДКА_РЕЛИЗА.md) — классификация ошибок и точечные патчи.
```bash
cd papa-yu
npm install
npm run tauri dev
```
Из корня проекта можно также: `cd src-tauri && cargo tauri dev`.
**Если в окне видно «Could not fetch a valid…»** — фронт не загрузился. Запускайте приложение только так: в терминале из папки проекта выполните `npm run tauri dev` (это поднимает и Vite, и Tauri). Не открывайте скомпилированный .app без dev-сервера, если хотите видеть интерфейс.
## Сборка
```bash
npm run tauri build
```
Установка в «Программы» из терминала: `npm run install-app`.
## v2.4.5 — что реализовано
### Добавлено в v2.4.5
- **Save as Project Note** — кнопка в блоке Online Research сохраняет результат в domain notes проекта (distill через LLM).
- **Контекст v3** — при `PAPAYU_PROTOCOL_VERSION=3` FILE-блоки включают sha256 для base_sha256 в EDIT_FILE.
## v2.4.4 — что реализовано
### Анализ и профиль
- **Анализ по пути** — выбор папки или ввод пути вручную; анализ возвращает отчёт (findings, recommendations, actions, action_groups, fix_packs).
- **Профиль по пути** — автоматическое определение типа проекта (React/Vite, Next.js, Node, Rust, Python) и лимитов (max_actions_per_tx, timeout_sec, max_files). Профиль и лимиты отображаются в форме.
### Применение и откат
- **Транзакционное apply** — перед применением создаётся снимок; после apply выполняется autoCheck (cargo check / npm run build и т.д.) с таймаутом из профиля. При падении проверки — автоматический откат.
- **Лимиты профиля** — в `apply_actions_tx` и `run_batch` проверяется число действий против `max_actions_per_tx`; при превышении возвращается ошибка TOO_MANY_ACTIONS. Таймаут проверок задаётся из профиля.
- **Undo/Redo** — откат последней транзакции и повтор; состояние отображается в UI.
### Безопасность
- **Защита путей** — запрещено изменение служебных путей (.git, node_modules, target, dist и т.д.) и бинарных файлов; разрешены только текстовые расширения (см. guard в коде).
- **Подтверждение** — применение только при явном подтверждении пользователя (user_confirmed).
- **Allowlist команд** — в verify и auto_check выполняются только разрешённые команды с фиксированными аргументами (конфиг в `src-tauri/config/verify_allowlist.json`).
- **Терминал и интернет (личное использование)** — приложение может открывать ссылки в браузере (Chrome и др.) и выполнять ограниченный набор команд (git, npm, cargo и т.д.) через scope в capability `personal-automation`. Подробнее: **`docs/SECURITY_AND_PERSONAL_AUTOMATION.md`**.
### UX
- **Папки и файлы** — выбор папки, прикрепление файлов (с фильтром расширений: .ts, .tsx, .rs, .py, .json, .toml и др.), ручной ввод пути.
- **История сессий** — по выбранному проекту отображается список сессий (дата, количество событий); после agentic run список обновляется.
- **Горячие клавиши** — Ctrl+Enter (Cmd+Enter на Mac): отправить/запустить анализ; Escape: сбросить превью изменений.
- **Тёмная тема** — переключатель в боковой панели; выбор сохраняется в localStorage; поддержка системных настроек темы.
- **Экспорт/импорт настроек** — кнопки «Экспорт» и «Импорт» в боковой панели для сохранения и восстановления всех настроек (проекты, профили, сессии, папки) в JSON-файл.
### Режимы
- **Batch** — анализ → превью → при необходимости применение с проверками (одна команда `run_batch`).
- **Исправить автоматически (agentic run)** — цикл: анализ → план → превью → применение → проверка; при неудаче проверки — откат и повтор в пределах max_attempts.
- **Безопасные исправления в один клик** — генерация безопасных действий по отчёту → превью → применение с проверкой.
- **Предложить исправления** — план по отчёту и цели: при наличии настройки LLM — вызов внешнего API (OpenAI/Ollama и др.), иначе эвристика.
### LLM-планировщик (опционально)
Для кнопки «Предложить исправления» можно включить генерацию плана через OpenAI-совместимый API. Задайте переменные окружения перед запуском приложения:
- **`PAPAYU_LLM_API_URL`** — URL API (обязательно), например:
- OpenAI: `https://api.openai.com/v1/chat/completions`
- Ollama (локально): `http://localhost:11434/v1/chat/completions`
- **`PAPAYU_LLM_API_KEY`** — API-ключ (для OpenAI и облачных API; для Ollama можно не задавать).
- **`PAPAYU_LLM_MODEL`** — модель (по умолчанию `gpt-4o-mini`), для Ollama — например `llama3.2`.
- **`PAPAYU_LLM_STRICT_JSON`** — при `1`/`true` добавляет `response_format: { type: "json_schema", ... }` (OpenAI Structured Outputs; Ollama может не поддерживать).
**Поведение strict / best-effort:**
- Если strict включён: приложение отправляет `response_format` в API; при невалидном ответе — локальная валидация схемы отклоняет и выполняется **1 авто-ретрай** с repair-подсказкой («Верни ТОЛЬКО валидный JSON…»).
- Если strict выключен или провайдер не поддерживает: **best-effort** парсинг (извлечение из markdown), затем локальная валидация схемы; при неудаче — тот же repair-ретрай.
Пример для Ollama (без ключа, локально):
```bash
export PAPAYU_LLM_API_URL="http://localhost:11434/v1/chat/completions"
export PAPAYU_LLM_MODEL="llama3.2"
npm run tauri dev
```
После этого кнопка «Предложить исправления» будет строить план через выбранный LLM.
**Claude и синхронизация с агентом (Claude Code / Cursor):** можно использовать Claude через [OpenRouter](https://openrouter.ai/) (OpenAI-совместимый API): `PAPAYU_LLM_API_URL=https://openrouter.ai/api/v1/chat/completions`, `PAPAYU_LLM_MODEL=anthropic/claude-3.5-sonnet`. При `PAPAYU_AGENT_SYNC=1` после каждого анализа в проекте записывается `.papa-yu/agent-sync.json` для чтения агентом в IDE. Подробнее: `docs/CLAUDE_AND_AGENT_SYNC.md`.
Если `PAPAYU_LLM_API_URL` не задан или пуст, используется встроенная эвристика (README, .gitignore, LICENSE, .env.example по правилам).
### Online Research (опционально)
Команда `research_answer_cmd`: поиск через Tavily → fetch страниц (SSRF-safe) → LLM summarize с источниками. Вызов через `researchAnswer(query)` на фронте.
**Env:**
- **`PAPAYU_ONLINE_RESEARCH=1`** — включить режим (по умолчанию выключен)
- **`PAPAYU_TAVILY_API_KEY`** — API-ключ Tavily (tavily.com)
- **`PAPAYU_ONLINE_MODEL`** — модель для summarize (по умолчанию из PAPAYU_LLM_MODEL)
- **`PAPAYU_ONLINE_MAX_SOURCES`** — макс. результатов поиска (default 5)
- **`PAPAYU_ONLINE_MAX_PAGES`** — макс. страниц для fetch (default 4)
- **`PAPAYU_ONLINE_PAGE_MAX_BYTES`** — лимит размера страницы (default 200000)
- **`PAPAYU_ONLINE_TIMEOUT_SEC`** — таймаут fetch (default 20)
**Use as context:** после online research кнопка «Use as context (once)» добавляет ответ в следующий PLAN/APPLY. Лимиты:
- **`PAPAYU_ONLINE_CONTEXT_MAX_CHARS`** — макс. символов online summary (default 8000)
- **`PAPAYU_ONLINE_CONTEXT_MAX_SOURCES`** — макс. источников (default 10)
- Online summary режется первым при превышении `PAPAYU_CONTEXT_MAX_TOTAL_CHARS`.
**Auto-use (X4):**
- **`PAPAYU_ONLINE_AUTO_USE_AS_CONTEXT=1`** — если включено, online research результат автоматически используется как контекст для повторного `proposeActions` без участия пользователя (default 0).
- Защита от циклов: максимум 1 auto-chain на один запрос (goal).
- UI: при auto-use показывается метка "Auto-used ✓"; кнопка "Disable auto-use" отключает для текущего проекта (сохраняется в localStorage).
**Тренды дизайна и иконок (вкладка в модалке «Тренды и рекомендации»):**
- Поиск трендовых дизайнов сайтов/приложений и иконок **только из безопасных источников** (allowlist доменов: Dribbble, Behance, Figma, Material, Heroicons, Lucide, shadcn, NNGroup и др.).
- Используется тот же **`PAPAYU_TAVILY_API_KEY`**; запросы идут с параметром `include_domains` — только разрешённые домены.
- Результаты показываются в списке и **подмешиваются в контекст ИИ** при «Предложить исправления», чтобы агент мог предлагать передовые дизайнерские решения при создании программ.
### Domain notes (A1A3)
Короткие «domain notes» на проект из online research: хранятся в `.papa-yu/notes/domain_notes.json`, при следующих запросах подмешиваются в prompt (с лимитами), чтобы реже ходить в Tavily и быстрее отвечать.
- **Формат:** `schema_version`, `updated_at`, `notes[]` (id, topic, tags, content_md, sources, confidence, ttl_days, usage_count, last_used_at, pinned).
- **Лимиты:** `PAPAYU_NOTES_MAX_ITEMS=50`, `PAPAYU_NOTES_MAX_CHARS_PER_NOTE=800`, `PAPAYU_NOTES_MAX_TOTAL_CHARS=4000`, `PAPAYU_NOTES_TTL_DAYS=30`.
- **Дистилляция:** после online research можно сохранить заметку через LLM-сжатие (команда `distill_and_save_domain_note_cmd`).
- **Injection:** в `llm_planner` перед ONLINE_RESEARCH и CONTEXT вставляется блок `PROJECT_DOMAIN_NOTES`; отбор заметок по релевантности к goal (token overlap); при использовании обновляются `usage_count` и `last_used_at`.
- **Trace:** `notes_injected`, `notes_count`, `notes_chars`, `notes_ids`.
- **Команды:** load/save/delete/clear_expired/pin domain notes, distill_and_save_domain_note. Подробнее: `docs/IMPLEMENTATION_STATUS_ABC.md`.
### Weekly report proposals (B1B2)
В еженедельном отчёте LLM может возвращать массив **proposals** (prompt_change, setting_change, golden_trace_add, limit_tuning, safety_rule) с полями title, why, risk, steps, expected_impact, evidence. В prompt добавлено правило: предлагать только то, что обосновано bundle + deltas. Секция «Предложения (proposals)» выводится в report_md.
### Тестирование
- **Юнит-тесты (Rust)** — тесты для `detect_project_type`, `get_project_limits`, `is_protected_file`, `is_text_allowed` (см. `src-tauri/src/commands/get_project_profile.rs` и `apply_actions_tx.rs`). Запуск: `cd src-tauri && cargo test`.
- **E2E сценарий** — описание сценария «анализ → применение → undo» и критерии успеха см. в `docs/E2E_SCENARIO.md`.
### Архитектура
- **Фронт:** React 18, Vite 5, TypeScript; типы в `src/lib/types.ts`, единый API-слой в `src/lib/tauri.ts`; компоненты PathSelector, AgenticResult, хук useUndoRedo.
- **Бэкенд:** Tauri 2, Rust; команды в `src-tauri/src/commands/`, транзакции и undo/redo в `tx/`, verify с таймаутом в `verify.rs`.
---
## Документация
- `docs/LIMITS.md` — границы продукта, Critical failures.
- `docs/ARCHITECTURE.md` — обзор архитектуры, модули, границы.
- `docs/RUNBOOK.md` — сборка, запуск, типовые проблемы.
- `docs/adr/` — Architecture Decision Records (Tauri, EDIT_FILE v3, SSRF).
- `docs/TECH_MEMO_FOR_INVESTORS.md` — технический memo для инвесторов.
- `docs/BUYER_QA.md` — вопросы покупателя и ответы.
- `docs/IMPROVEMENTS.md` — рекомендации по улучшениям.
- `docs/E2E_SCENARIO.md` — E2E сценарий и критерии успеха.
- `docs/EDIT_FILE_DEBUG.md` — чеклист отладки v3 EDIT_FILE.
- `docs/INVESTMENT_READY_REPORT.md` — отчёт о готовности к передаче/продаже.
- `docs/ПРЕЗЕНТАЦИЯ_PAPA_YU.md` — полномасштабная презентация программы (25 слайдов).
- `CHANGELOG.md` — история изменений по версиям.
| Документ | Описание |
|----------|----------|
| [docs/РЕЛИЗ_И_ОБНОВЛЕНИЯ.md](docs/РЕЛИЗ_И_ОБНОВЛЕНИЯ.md) | Релиз, теги, секреты, проверка обновлений |
| [docs/CI_ОТЛАДКА_РЕЛИЗА.md](docs/CI_ОТЛАДКА_РЕЛИЗА.md) | Отладка падений `tauri build` в CI |
| [docs/SSH_НАСТРОЙКА.md](docs/SSH_НАСТРОЙКА.md) | Настройка SSH для GitHub |
| [docs/CONTRACTS.md](docs/CONTRACTS.md) | Контракты и архитектура |

74
REFACTORING_CHANGELOG.md Normal file
View File

@ -0,0 +1,74 @@
# PAPA YU — Рефакторинг: Changelog
## Что сделано
### 1. Удалены заглушки (6 файлов)
- `Documents.tsx` — пустая страница «Документы» (32 строки)
- `Finances.tsx` — пустая страница «Финансы» (32 строки)
- `Personnel.tsx` — пустая страница «Персонал» (32 строки)
- `TMCZakupki.tsx` — пустая страница «ТМЦ и закупки» (39 строк)
- `Reglamenty.tsx` — пустая страница «Регламенты» (47 строк)
- `ChatAgent.tsx` — дублировал функционал Tasks.tsx (194 строки)
### 2. Почищены роуты
**Было:** 13 роутов (4 рабочие, 9 заглушек/редиректов)
**Стало:** 7 роутов — все рабочие
| Путь | Страница | Статус |
|------|----------|--------|
| `/` | Tasks (Анализ) | ✅ Главная, анализ проекта |
| `/control-panel` | Dashboard (Безопасность) | ✅ Живые данные из анализа |
| `/policies` | PolicyEngine | ✅ Реальные проверки |
| `/audit` | AuditLogger | ✅ Реальные события |
| `/secrets` | SecretsGuard | ✅ Реальные данные |
| `/updates` | Updates | ✅ Без изменений |
| `/diagnostics` | Diagnostics | ✅ Без изменений |
### 3. Оживлены PolicyEngine, SecretsGuard, AuditLogger
**PolicyEngine** — реальные результаты из анализатора:
- Проверяет .env без .gitignore, README, тесты, глубину вложенности
- Каждая проверка: ✓/✗ на основе findings из Rust-анализатора
- Статус «Безопасно» / «Есть проблемы» на основе реальных данных
**SecretsGuard** — фильтрует security-related findings:
- Findings связанные с .env, secrets, gitignore, tokens
- Security signals из анализатора
- Статистика: критичных/предупреждений/информационных
**AuditLogger** — реальные события сессии:
- `project_analyzed` — каждый анализ с metadata
- `actions_applied` / `actions_apply_failed` — применение исправлений
- Фильтрация по типу события и агенту
- Кнопка очистки журнала
### 4. Обновлён Dashboard
- Карточки показывают реальный статус из анализа
- Сводка `llm_context` — ключевые риски, summary
- CTA «Перейти к анализу» если данных нет
### 5. Обновлён app-store
- Убран захардкоженный `systemStatus`
- Добавлен `lastReport` / `lastPath` — shared state
- Типизированные `AuditEvent` с metadata
### 6. Tasks.tsx — интеграция со store
- Report сохраняется в глобальный store при анализе
- Audit events логируются при apply/undo
## Метрики
| Метрика | До | После |
|---------|------|------|
| TypeScript | ~2 700 строк, 24 файла | ~2 410 строк, 18 файлов |
| Rust | ~1 450 строк, 9 файлов | Без изменений |
| Роутов | 13 (4 рабочие) | 7 (все рабочие) |
| Заглушек | 9 | 0 |
| Мок-данные (setTimeout) | 4 файла | 0 |
## Что НЕ менялось
- Rust бэкенд (все 5 команд, types.rs, анализатор)
- CI/CD (.github/workflows)
- Tauri конфигурация
- Updates.tsx, Diagnostics.tsx, NotFound.tsx
- anime-utils, event-bus, analyze.ts, ErrorBoundary, ErrorDisplay

3
desktop-core/README.md Normal file
View File

@ -0,0 +1,3 @@
# desktop-core (архив)
Каталог оставлен для совместимости. Содержимое `tools/project-auditor/` не используется основным приложением PAPA YU. В будущем здесь могут быть общие утилиты для desktop-сборки.

4
desktop/src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

5449
desktop/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.5.3", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.9.5", features = [] }
tauri-plugin-log = "2"
tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
walkdir = "2"
chrono = "0.4"

View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,15 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default",
"dialog:allow-open",
"core:event:allow-listen",
"updater:default",
"process:allow-restart"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,745 @@
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::time::Instant;
use tauri::Emitter;
use crate::types::{
Action, ActionKind, AnalyzeReport, Finding, LlmContext, ProjectContext, ProjectSignal,
ProjectStructure, Recommendation, ReportStats,
};
const MAX_FILES: u64 = 50_000;
const MAX_DURATION_SECS: u64 = 60;
const TOP_EXTENSIONS_N: usize = 15;
const MAX_DEPTH_WARN: u32 = 6;
const ROOT_FILES_WARN: u64 = 20;
const EXCLUDED_DIRS: &[&str] = &[
".git", "node_modules", "dist", "build", ".next", "target", ".cache", "coverage",
];
const MARKER_README: &[&str] = &["README", "readme", "Readme"];
const MARKER_VITE: &[&str] = &["vite.config.js", "vite.config.ts", "vite.config.mjs"];
#[derive(Default)]
struct ScanState {
file_count: u64,
dir_count: u64,
total_size_bytes: u64,
extensions: HashMap<String, u64>,
has_readme: bool,
has_package_json: bool,
has_cargo_toml: bool,
has_env: bool,
has_docker: bool,
has_tsconfig: bool,
has_vite: bool,
has_next: bool,
has_gitignore: bool,
has_license: bool,
has_eslint: bool,
has_prettier: bool,
has_tests_dir: bool,
has_src: bool,
has_components: bool,
has_pages: bool,
has_requirements_txt: bool,
has_pyproject: bool,
has_setup_py: bool,
package_json_count: u32,
cargo_toml_count: u32,
root_file_count: u64,
root_dirs: HashSet<String>,
max_depth: u32,
}
const PROGRESS_EVENT: &str = "analyze_progress";
#[tauri::command]
pub fn analyze_project(window: tauri::Window, path: String) -> Result<AnalyzeReport, String> {
let root = PathBuf::from(&path);
if !root.exists() {
return Err("Путь не существует".to_string());
}
if !root.is_dir() {
return Err("Путь не является папкой".to_string());
}
let _ = window.emit(PROGRESS_EVENT, "Сканирую структуру…");
let deadline = Instant::now() + std::time::Duration::from_secs(MAX_DURATION_SECS);
let mut state = ScanState::default();
scan_dir(&root, &root, 0, &mut state, &deadline)?;
let _ = window.emit(PROGRESS_EVENT, "Анализирую архитектуру…");
let top_extensions: Vec<(String, u64)> = {
let mut v: Vec<_> = state.extensions.iter().map(|(k, v)| (k.clone(), *v)).collect();
v.sort_by(|a, b| b.1.cmp(&a.1));
v.into_iter().take(TOP_EXTENSIONS_N).collect()
};
let stats = ReportStats {
file_count: state.file_count,
dir_count: state.dir_count,
total_size_bytes: state.total_size_bytes,
top_extensions,
max_depth: state.max_depth as u64,
};
let structure = build_structure(&state);
let mut findings: Vec<Finding> = Vec::new();
let mut recommendations: Vec<Recommendation> = Vec::new();
let mut signals: Vec<ProjectSignal> = Vec::new();
if state.has_env {
findings.push(Finding {
severity: "high".to_string(),
title: "Риск секретов".to_string(),
details: "Обнаружены файлы .env или .env.* — не коммитьте секреты в репозиторий.".to_string(),
});
signals.push(ProjectSignal {
category: "security".to_string(),
level: "high".to_string(),
message: "Есть .env файл — риск утечки секретов.".to_string(),
});
}
if !state.has_readme {
recommendations.push(Recommendation {
title: "Добавить README".to_string(),
details: "Опишите проект и инструкцию по запуску.".to_string(),
priority: "medium".to_string(),
effort: "low".to_string(),
impact: "high".to_string(),
});
signals.push(ProjectSignal {
category: "quality".to_string(),
level: "warn".to_string(),
message: "Нет README.".to_string(),
});
}
if !state.has_gitignore {
recommendations.push(Recommendation {
title: "Добавить .gitignore".to_string(),
details: "Исключите артефакты сборки и зависимости из репозитория.".to_string(),
priority: "medium".to_string(),
effort: "low".to_string(),
impact: "medium".to_string(),
});
signals.push(ProjectSignal {
category: "quality".to_string(),
level: "warn".to_string(),
message: "Нет .gitignore.".to_string(),
});
}
if !state.has_license {
recommendations.push(Recommendation {
title: "Указать лицензию".to_string(),
details: "Добавьте LICENSE или LICENSE.md.".to_string(),
priority: "low".to_string(),
effort: "low".to_string(),
impact: "medium".to_string(),
});
signals.push(ProjectSignal {
category: "quality".to_string(),
level: "info".to_string(),
message: "Нет файла лицензии.".to_string(),
});
}
if state.has_src && !state.has_tests_dir {
recommendations.push(Recommendation {
title: "Добавить тесты".to_string(),
details: "Есть src/, но нет папки tests/ — добавьте базовые тесты.".to_string(),
priority: "high".to_string(),
effort: "medium".to_string(),
impact: "high".to_string(),
});
signals.push(ProjectSignal {
category: "structure".to_string(),
level: "warn".to_string(),
message: "Есть src/, нет tests/.".to_string(),
});
}
if state.has_components && !state.has_pages && state.has_package_json {
recommendations.push(Recommendation {
title: "Проверить структуру фронтенда".to_string(),
details: "Есть components/, но нет pages/ — возможно, маршруты или страницы в другом месте.".to_string(),
priority: "low".to_string(),
effort: "low".to_string(),
impact: "low".to_string(),
});
}
if state.root_file_count >= ROOT_FILES_WARN {
findings.push(Finding {
severity: "warn".to_string(),
title: "Много файлов в корне".to_string(),
details: format!("В корне {} файлов — рассмотрите группировку по папкам.", state.root_file_count),
});
signals.push(ProjectSignal {
category: "structure".to_string(),
level: "warn".to_string(),
message: "Слишком много файлов в корне проекта.".to_string(),
});
}
if state.max_depth >= MAX_DEPTH_WARN {
findings.push(Finding {
severity: "warn".to_string(),
title: "Глубокая вложенность".to_string(),
details: format!("Вложенность до {} уровней — усложняет навигацию.", state.max_depth),
});
signals.push(ProjectSignal {
category: "structure".to_string(),
level: "warn".to_string(),
message: "Глубокая вложенность папок.".to_string(),
});
}
if state.has_package_json && !state.has_eslint && !state.has_cargo_toml {
recommendations.push(Recommendation {
title: "Добавить линтер".to_string(),
details: "Рекомендуется ESLint (и при необходимости Prettier) для JavaScript/TypeScript.".to_string(),
priority: "medium".to_string(),
effort: "low".to_string(),
impact: "medium".to_string(),
});
}
if state.has_cargo_toml && !state.has_eslint {
recommendations.push(Recommendation {
title: "Использовать Clippy".to_string(),
details: "Добавьте в CI или pre-commit: cargo clippy.".to_string(),
priority: "medium".to_string(),
effort: "low".to_string(),
impact: "medium".to_string(),
});
}
if !state.has_package_json && !state.has_cargo_toml && !state.has_pyproject && !state.has_requirements_txt {
findings.push(Finding {
severity: "warn".to_string(),
title: "Неопределён тип проекта".to_string(),
details: "Не найдены привычные манифесты (package.json, Cargo.toml, pyproject.toml).".to_string(),
});
}
if state.file_count > 30_000 || state.dir_count > 5_000 {
recommendations.push(Recommendation {
title: "Сузить область анализа".to_string(),
details: "Очень много файлов или папок — добавьте исключения или выберите подпапку.".to_string(),
priority: "medium".to_string(),
effort: "low".to_string(),
impact: "low".to_string(),
});
}
let _ = window.emit(PROGRESS_EVENT, "Формирую вывод…");
let recommendations = enrich_recommendations(recommendations);
let project_context = build_project_context(&state, &findings, &signals);
let actions = build_actions(state.has_readme, state.has_tests_dir, state.has_gitignore);
let narrative = build_narrative(&state, &structure, &findings, &recommendations);
let report = AnalyzeReport {
path: path.clone(),
narrative: narrative.clone(),
stats: stats.clone(),
structure: structure.clone(),
project_context: project_context.clone(),
findings: findings.clone(),
recommendations: recommendations.clone(),
actions: actions.clone(),
signals: signals.clone(),
report_md: String::new(),
llm_context: LlmContext {
concise_summary: String::new(),
key_risks: Vec::new(),
top_recommendations: Vec::new(),
signals: Vec::new(),
},
};
let report_md = build_markdown_report(&report);
let llm_context = build_llm_context(&report);
Ok(AnalyzeReport {
path: path.clone(),
narrative: report.narrative,
stats: report.stats,
structure: report.structure,
project_context: report.project_context,
findings: report.findings,
recommendations: report.recommendations,
actions: report.actions,
signals: report.signals,
report_md,
llm_context,
})
}
fn build_actions(has_readme: bool, has_tests: bool, has_gitignore: bool) -> Vec<Action> {
let mut actions = vec![];
if !has_readme {
actions.push(Action {
id: "add-readme".into(),
title: "Добавить README.md".into(),
description: "В проекте отсутствует README.md".into(),
kind: ActionKind::CreateFile,
path: "README.md".into(),
content: Some("# Project\n\nDescribe your project.\n".into()),
});
}
if !has_tests {
actions.push(Action {
id: "add-tests-dir".into(),
title: "Создать папку tests/".into(),
description: "В проекте нет tests/ (минимальная структура для тестов)".into(),
kind: ActionKind::CreateDir,
path: "tests".into(),
content: None,
});
}
if !has_gitignore {
actions.push(Action {
id: "add-gitignore".into(),
title: "Добавить .gitignore".into(),
description: "Рекомендуется добавить базовый .gitignore".into(),
kind: ActionKind::CreateFile,
path: ".gitignore".into(),
content: Some("node_modules/\ndist/\nbuild/\n.target/\n.DS_Store\n".into()),
});
}
actions
}
fn enrich_recommendations(mut recs: Vec<Recommendation>) -> Vec<Recommendation> {
for r in &mut recs {
let (p, e, i) = if r.title.contains("README") {
("high", "low", "high")
} else if r.title.contains("тест") || r.title.contains("тесты") || r.title.contains("Add tests") || r.title.contains("tests") {
("high", "medium", "high")
} else if r.title.contains(".gitignore") {
("medium", "low", "medium")
} else if r.title.contains(".env") || r.title.contains("секрет") {
("high", "low", "high")
} else if r.title.contains("лицензи") {
("low", "low", "medium")
} else if r.title.contains("линтер") || r.title.contains("Clippy") {
("medium", "low", "medium")
} else {
(r.priority.as_str(), r.effort.as_str(), r.impact.as_str())
};
r.priority = p.to_string();
r.effort = e.to_string();
r.impact = i.to_string();
}
recs
}
fn build_project_context(
state: &ScanState,
findings: &[Finding],
signals: &[ProjectSignal],
) -> ProjectContext {
let risk_level = if state.has_env
|| signals.iter().any(|s| s.category == "security" && s.level == "high")
{
"High"
} else if findings.len() > 5
|| signals.iter().any(|s| s.level == "warn")
{
"Medium"
} else {
"Low"
};
let complexity = if state.file_count > 5000 || state.dir_count > 500 || state.max_depth > 8 {
"High"
} else if state.file_count > 800 || state.dir_count > 120 {
"Medium"
} else {
"Low"
};
let maturity = if state.has_readme && (state.has_tests_dir || state.has_eslint) {
"Production-like"
} else if state.has_readme {
"MVP"
} else {
"Prototype"
};
let mut stack = Vec::new();
if state.has_package_json {
stack.push("Node.js".to_string());
}
if state.has_cargo_toml {
stack.push("Rust".to_string());
}
if state.has_vite {
stack.push("Vite".to_string());
}
if state.has_next {
stack.push("Next.js".to_string());
}
if state.has_pyproject || state.has_requirements_txt {
stack.push("Python".to_string());
}
if stack.is_empty() {
stack.push("Unknown".to_string());
}
let domain = if state.has_next || state.has_vite {
"frontend"
} else if state.has_cargo_toml {
"systems"
} else if state.has_package_json {
"fullstack"
} else {
"general"
}
.to_string();
ProjectContext {
stack,
domain,
maturity: maturity.to_string(),
complexity: complexity.to_string(),
risk_level: risk_level.to_string(),
}
}
fn build_markdown_report(report: &AnalyzeReport) -> String {
let mut md = String::new();
md.push_str("# PAPA YU — отчёт анализа проекта\n\n");
md.push_str(&report.narrative);
md.push_str("\n\n---\n\n");
md.push_str("## Статистика\n\n");
md.push_str(&format!(
"- Файлов: {}\n- Папок: {}\n- Max depth: {}\n- Размер: {} Б\n\n",
report.stats.file_count,
report.stats.dir_count,
report.stats.max_depth,
report.stats.total_size_bytes
));
md.push_str("## Контекст проекта\n\n");
md.push_str(&format!(
"- Стек: {}\n- Зрелость: {}\n- Сложность: {}\n- Риск: {}\n\n",
report.project_context.stack.join(", "),
report.project_context.maturity,
report.project_context.complexity,
report.project_context.risk_level
));
if !report.findings.is_empty() {
md.push_str("## Находки\n\n");
for f in &report.findings {
md.push_str(&format!("- **{}**: {}\n", f.title, f.details));
}
md.push_str("\n");
}
if !report.recommendations.is_empty() {
md.push_str("## Рекомендации\n\n");
for r in &report.recommendations {
md.push_str(&format!(
"- **{}** [{} / effort:{} / impact:{}]\n {}\n",
r.title, r.priority, r.effort, r.impact, r.details
));
}
}
md
}
fn build_llm_context(report: &AnalyzeReport) -> LlmContext {
let concise_summary = format!(
"{}; {}; {} файлов, {} папок. Риск: {}, зрелость: {}.",
report.structure.project_type,
report.structure.architecture,
report.stats.file_count,
report.stats.dir_count,
report.project_context.risk_level,
report.project_context.maturity
);
let key_risks: Vec<String> = report
.findings
.iter()
.filter(|f| f.severity == "high")
.map(|f| format!("{}: {}", f.title, f.details))
.collect();
let top_recommendations: Vec<String> = report
.recommendations
.iter()
.take(5)
.map(|r| format!("[{}] {}", r.priority, r.title))
.collect();
LlmContext {
concise_summary,
key_risks,
top_recommendations,
signals: report.signals.clone(),
}
}
fn build_structure(state: &ScanState) -> ProjectStructure {
let mut project_type = String::new();
let mut architecture = String::new();
let mut structure_notes: Vec<String> = Vec::new();
let is_monorepo = state.package_json_count > 1 || state.cargo_toml_count > 1;
if state.has_cargo_toml {
project_type = "Rust / Cargo".to_string();
architecture = "Rust-проект".to_string();
if is_monorepo {
project_type = "Rust monorepo".to_string();
}
}
if state.has_package_json {
if !project_type.is_empty() {
project_type = format!("{} + Node", project_type);
} else if state.has_next {
project_type = "Next.js".to_string();
architecture = "React (Next.js) fullstack".to_string();
} else if state.has_vite {
project_type = "React + Vite".to_string();
architecture = "Frontend SPA (Vite)".to_string();
} else {
project_type = "Node.js".to_string();
architecture = "Node / frontend или backend".to_string();
}
if is_monorepo && !project_type.contains("monorepo") {
project_type = format!("{} (monorepo)", project_type);
}
}
if state.has_pyproject || state.has_requirements_txt || state.has_setup_py {
if !project_type.is_empty() {
project_type = format!("{} + Python", project_type);
} else {
project_type = "Python".to_string();
architecture = "Python-проект (Django/FastAPI или скрипты)".to_string();
}
}
if project_type.is_empty() {
project_type = "Неопределён".to_string();
architecture = "Тип по манифестам не определён".to_string();
}
if state.has_src && state.has_tests_dir {
structure_notes.push("Есть src/ и tests/ — хорошее разделение.".to_string());
} else if state.has_src && !state.has_tests_dir {
structure_notes.push("Есть src/, нет tests/ — стоит добавить тесты.".to_string());
}
if state.root_file_count >= ROOT_FILES_WARN {
structure_notes.push("Много файлов в корне — структура упрощённая.".to_string());
}
if state.max_depth >= MAX_DEPTH_WARN {
structure_notes.push("Глубокая вложенность папок.".to_string());
}
if structure_notes.is_empty() {
structure_notes.push("Структура без явного разделения на домены.".to_string());
}
ProjectStructure {
project_type,
architecture,
structure_notes,
}
}
fn build_narrative(
state: &ScanState,
structure: &ProjectStructure,
findings: &[Finding],
recommendations: &[Recommendation],
) -> String {
let mut parts = Vec::new();
parts.push("Я проанализировал ваш проект.".to_string());
parts.push(format!(
"Это {} ({}).",
structure.project_type.to_lowercase(),
structure.architecture
));
if !structure.structure_notes.is_empty() {
parts.push(structure.structure_notes.join(" "));
}
let size_label = if state.file_count < 50 {
"небольшой"
} else if state.file_count < 500 {
"среднего размера"
} else {
"крупный"
};
parts.push(format!(
"По размеру — {} проект: {} файлов, {} папок.",
size_label, state.file_count, state.dir_count
));
if !findings.is_empty() {
parts.push("".to_string());
parts.push("Основные проблемы:".to_string());
for f in findings.iter().take(7) {
parts.push(format!(" {}", f.title));
}
}
if !recommendations.is_empty() {
parts.push("".to_string());
parts.push("Я бы рекомендовал начать с:".to_string());
for (i, r) in recommendations.iter().take(5).enumerate() {
parts.push(format!("{}. {}", i + 1, r.title));
}
}
parts.join("\n\n")
}
fn scan_dir(
root: &Path,
dir: &Path,
depth: u32,
state: &mut ScanState,
deadline: &Instant,
) -> Result<(), String> {
if Instant::now() > *deadline {
return Err("Превышено время анализа (таймаут)".to_string());
}
if state.file_count >= MAX_FILES {
return Err("Превышен лимит количества файлов".to_string());
}
if depth > state.max_depth {
state.max_depth = depth;
}
let is_root = dir == root;
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Ok(()),
};
for entry in entries.flatten() {
let path = entry.path();
let meta = match entry.metadata() {
Ok(m) => m,
Err(_) => continue,
};
if meta.is_symlink() {
continue;
}
if meta.is_dir() {
state.dir_count += 1;
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if is_root {
state.root_dirs.insert(name.to_lowercase());
}
let name_lower = name.to_lowercase();
if EXCLUDED_DIRS.contains(&name) {
continue;
}
if name_lower == "src" {
state.has_src = true;
}
if name_lower == "tests" || name_lower == "test" || name_lower == "__tests__" {
state.has_tests_dir = true;
}
if name_lower == "components" {
state.has_components = true;
}
if name_lower == "pages" || name_lower == "app" {
state.has_pages = true;
}
scan_dir(root, &path, depth + 1, state, deadline)?;
continue;
}
state.file_count += 1;
if state.file_count >= MAX_FILES {
return Err("Превышен лимит количества файлов".to_string());
}
if is_root {
state.root_file_count += 1;
}
state.total_size_bytes = state.total_size_bytes.saturating_add(meta.len());
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
let name_lower = name.to_lowercase();
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_string();
*state.extensions.entry(ext).or_insert(0) += 1;
}
if name_lower == "package.json" {
state.has_package_json = true;
state.package_json_count += 1;
}
if name_lower == "cargo.toml" {
state.has_cargo_toml = true;
state.cargo_toml_count += 1;
}
if name_lower == "tsconfig.json" {
state.has_tsconfig = true;
}
if name_lower == "dockerfile" || name_lower == "docker-compose.yml" {
state.has_docker = true;
}
if name_lower.starts_with(".env") {
state.has_env = true;
}
if name_lower == ".gitignore" {
state.has_gitignore = true;
}
if name_lower == "license" || name_lower == "license.md" || name_lower.starts_with("license.") {
state.has_license = true;
}
if name_lower == "eslint.config.js" || name_lower == ".eslintrc" || name_lower.starts_with(".eslintrc") {
state.has_eslint = true;
}
if name_lower == ".prettierrc" || name_lower == "prettier.config" || name_lower.starts_with("prettier.config") {
state.has_prettier = true;
}
if name_lower == "next.config.js" || name_lower == "next.config.mjs" || name_lower == "next.config.ts" {
state.has_next = true;
}
if name_lower == "requirements.txt" {
state.has_requirements_txt = true;
}
if name_lower == "pyproject.toml" {
state.has_pyproject = true;
}
if name_lower == "setup.py" {
state.has_setup_py = true;
}
for m in MARKER_README {
if name_lower.starts_with(&m.to_lowercase()) {
state.has_readme = true;
break;
}
}
for m in MARKER_VITE {
if name_lower == *m {
state.has_vite = true;
break;
}
}
}
}
Ok(())
}

View File

@ -0,0 +1,249 @@
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, Manager, Window};
use crate::types::{Action, ActionKind, ApplyResult};
const PROGRESS_EVENT: &str = "analyze_progress";
fn app_data_dir(app: &AppHandle) -> Result<PathBuf, String> {
app.path()
.app_data_dir()
.map_err(|_| "app_data_dir_unavailable".to_string())
}
fn safe_join(base: &Path, rel: &str) -> Result<PathBuf, String> {
let rel_path = PathBuf::from(rel);
if rel_path.is_absolute() {
return Err("absolute_path_denied".into());
}
if rel.contains("..") {
return Err("path_traversal_denied".into());
}
Ok(base.join(rel_path))
}
fn snapshot_paths(
session_dir: &Path,
project_root: &Path,
targets: &[PathBuf],
) -> Result<(), String> {
let snap_dir = session_dir.join("snapshot");
fs::create_dir_all(&snap_dir).map_err(|e| e.to_string())?;
for t in targets {
let abs = project_root.join(t);
let snap = snap_dir.join(t);
if abs.is_dir() {
fs::create_dir_all(&snap).map_err(|e| e.to_string())?;
continue;
}
if abs.exists() {
if let Some(parent) = snap.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
fs::copy(&abs, &snap).map_err(|e| e.to_string())?;
} else {
let missing_marker = snap_dir.join(".missing").join(t);
if let Some(parent) = missing_marker.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
fs::write(&missing_marker, b"").map_err(|e| e.to_string())?;
}
}
Ok(())
}
fn revert_snapshot(session_dir: &Path, project_root: &Path) -> Result<Vec<String>, String> {
let snap_dir = session_dir.join("snapshot");
if !snap_dir.exists() {
return Err("snapshot_missing".into());
}
let mut restored = vec![];
for entry in walkdir::WalkDir::new(&snap_dir)
.into_iter()
.filter_map(Result::ok)
{
if entry.file_type().is_dir() {
continue;
}
let snap_path = entry.path().to_path_buf();
let rel = snap_path
.strip_prefix(&snap_dir)
.map_err(|e| e.to_string())?;
let rel_str = rel.to_string_lossy();
if rel_str.starts_with(".missing/") || rel_str.starts_with(".missing\\") {
let orig: &str = rel_str
.strip_prefix(".missing/")
.or_else(|| rel_str.strip_prefix(".missing\\"))
.unwrap_or(&rel_str);
let abs = project_root.join(orig);
if abs.exists() {
fs::remove_file(&abs).map_err(|e| e.to_string())?;
restored.push(orig.to_string());
}
continue;
}
let abs = project_root.join(rel);
if let Some(parent) = abs.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
fs::copy(&snap_path, &abs).map_err(|e| e.to_string())?;
restored.push(rel.to_string_lossy().to_string());
}
Ok(restored)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplyPayload {
pub path: String,
pub actions: Vec<Action>,
}
#[tauri::command]
pub async fn apply_actions(window: Window, app: AppHandle, payload: ApplyPayload) -> ApplyResult {
let project_root = PathBuf::from(&payload.path);
if !project_root.exists() || !project_root.is_dir() {
return ApplyResult {
ok: false,
session_id: String::new(),
applied: vec![],
skipped: payload.actions.iter().map(|a| a.id.clone()).collect(),
error: Some("path_invalid".into()),
error_code: Some("PATH_INVALID".into()),
undo_available: false,
};
}
let data_dir = match app_data_dir(&app) {
Ok(d) => d,
Err(e) => {
return ApplyResult {
ok: false,
session_id: String::new(),
applied: vec![],
skipped: vec![],
error: Some(e),
error_code: Some("APP_DATA_DIR".into()),
undo_available: false,
};
}
};
let session_id = format!("{}", chrono::Utc::now().timestamp_millis());
let session_dir = data_dir.join("history").join(&session_id);
if fs::create_dir_all(&session_dir).is_err() {
return ApplyResult {
ok: false,
session_id: session_id.clone(),
applied: vec![],
skipped: vec![],
error: Some("HISTORY_CREATE_FAILED".into()),
error_code: Some("HISTORY_CREATE_FAILED".into()),
undo_available: false,
};
}
let _ = window.emit(PROGRESS_EVENT, "Готовлю откат (snapshot)…");
let targets: Vec<PathBuf> = payload.actions.iter().map(|a| PathBuf::from(&a.path)).collect();
if let Err(e) = snapshot_paths(&session_dir, &project_root, &targets) {
return ApplyResult {
ok: false,
session_id: session_id.clone(),
applied: vec![],
skipped: payload.actions.iter().map(|a| a.id.clone()).collect(),
error: Some(e),
error_code: Some("SNAPSHOT_FAILED".into()),
undo_available: false,
};
}
let _ = window.emit(PROGRESS_EVENT, "Применяю изменения…");
let mut applied = vec![];
let result_apply = (|| -> Result<(), String> {
for a in &payload.actions {
let abs = safe_join(&project_root, &a.path)?;
match a.kind {
ActionKind::CreateDir => {
fs::create_dir_all(&abs).map_err(|e| e.to_string())?;
}
ActionKind::DeleteDir => {
if abs.exists() {
fs::remove_dir_all(&abs).map_err(|e| e.to_string())?;
}
}
ActionKind::CreateFile | ActionKind::UpdateFile => {
let content = a
.content
.clone()
.ok_or_else(|| "content_missing".to_string())?;
if let Some(parent) = abs.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
fs::write(&abs, content.as_bytes()).map_err(|e| e.to_string())?;
}
ActionKind::DeleteFile => {
if abs.exists() {
fs::remove_file(&abs).map_err(|e| e.to_string())?;
}
}
}
applied.push(a.id.clone());
}
Ok(())
})();
if let Err(err) = result_apply {
let _ = window.emit(PROGRESS_EVENT, "Обнаружена ошибка. Откатываю изменения…");
let _ = revert_snapshot(&session_dir, &project_root);
let skipped: Vec<String> = payload
.actions
.iter()
.map(|a| a.id.clone())
.filter(|id| !applied.contains(id))
.collect();
return ApplyResult {
ok: false,
session_id,
applied,
skipped,
error: Some(err),
error_code: Some("APPLY_FAILED_ROLLED_BACK".into()),
undo_available: false,
};
}
let _ = fs::write(
data_dir.join("history").join("last_session.txt"),
session_id.as_bytes(),
);
let _ = window.emit(PROGRESS_EVENT, "Готово. Изменения применены.");
ApplyResult {
ok: true,
session_id,
applied,
skipped: vec![],
error: None,
error_code: None,
undo_available: true,
}
}

View File

@ -0,0 +1,21 @@
use serde::Serialize;
use tauri::{AppHandle, Manager};
#[derive(Debug, Serialize)]
pub struct AppInfo {
pub version: String,
pub app_data_dir: Option<String>,
pub app_config_dir: Option<String>,
}
#[tauri::command]
pub fn get_app_info(app: AppHandle) -> AppInfo {
let version = app.package_info().version.to_string();
let app_data_dir = app.path().app_data_dir().ok().map(|p| p.to_string_lossy().into_owned());
let app_config_dir = app.path().app_config_dir().ok().map(|p| p.to_string_lossy().into_owned());
AppInfo {
version,
app_data_dir,
app_config_dir,
}
}

View File

@ -0,0 +1,11 @@
mod analyze_project;
mod apply_actions;
mod get_app_info;
mod preview_actions;
mod undo_last;
pub use analyze_project::analyze_project;
pub use apply_actions::apply_actions;
pub use get_app_info::get_app_info;
pub use preview_actions::preview_actions;
pub use undo_last::undo_last;

View File

@ -0,0 +1,138 @@
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter, Window};
use crate::types::{Action, ActionKind, DiffItem, PreviewResult};
const PROGRESS_EVENT: &str = "analyze_progress";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreviewPayload {
pub path: String,
pub actions: Vec<Action>,
}
fn safe_join(base: &Path, rel: &str) -> Result<PathBuf, String> {
let rel_path = PathBuf::from(rel);
if rel_path.is_absolute() {
return Err("absolute_path_denied".into());
}
if rel.contains("..") {
return Err("path_traversal_denied".into());
}
Ok(base.join(rel_path))
}
fn read_text_if_exists(p: &Path) -> Option<String> {
if !p.exists() || p.is_dir() {
return None;
}
let bytes = fs::read(p).ok()?;
if bytes.len() > 200_000 {
return Some("[слишком большой файл для предпросмотра]".into());
}
String::from_utf8(bytes).ok()
}
fn summarize(kind: &str, path: &str) -> String {
match kind {
"create" => format!("Создать файл {}", path),
"update" => format!("Обновить файл {}", path),
"delete" => format!("Удалить файл {}", path),
"mkdir" => format!("Создать папку {}", path),
"rmdir" => format!("Удалить папку {}", path),
_ => format!("Изменение {}", path),
}
}
#[tauri::command]
pub async fn preview_actions(
window: Window,
_app: AppHandle,
payload: PreviewPayload,
) -> PreviewResult {
let project_root = PathBuf::from(&payload.path);
if !project_root.exists() || !project_root.is_dir() {
return PreviewResult {
ok: false,
diffs: vec![],
error: Some("path_invalid".into()),
error_code: Some("PATH_INVALID".into()),
};
}
let _ = window.emit(PROGRESS_EVENT, "Готовлю предпросмотр изменений…");
let mut diffs: Vec<DiffItem> = vec![];
for a in payload.actions {
let abs = match safe_join(&project_root, &a.path) {
Ok(p) => p,
Err(e) => {
return PreviewResult {
ok: false,
diffs: vec![],
error: Some(e),
error_code: Some("PATH_DENIED".into()),
};
}
};
match a.kind {
ActionKind::CreateDir => {
diffs.push(DiffItem {
path: a.path.clone(),
kind: "mkdir".into(),
before: None,
after: None,
summary: summarize("mkdir", &a.path),
});
}
ActionKind::DeleteDir => {
diffs.push(DiffItem {
path: a.path.clone(),
kind: "rmdir".into(),
before: None,
after: None,
summary: summarize("rmdir", &a.path),
});
}
ActionKind::CreateFile => {
diffs.push(DiffItem {
path: a.path.clone(),
kind: "create".into(),
before: None,
after: a.content.clone(),
summary: summarize("create", &a.path),
});
}
ActionKind::UpdateFile => {
diffs.push(DiffItem {
path: a.path.clone(),
kind: "update".into(),
before: read_text_if_exists(&abs),
after: a.content.clone(),
summary: summarize("update", &a.path),
});
}
ActionKind::DeleteFile => {
diffs.push(DiffItem {
path: a.path.clone(),
kind: "delete".into(),
before: read_text_if_exists(&abs),
after: None,
summary: summarize("delete", &a.path),
});
}
}
}
PreviewResult {
ok: true,
diffs,
error: None,
error_code: None,
}
}

View File

@ -0,0 +1,124 @@
use std::fs;
use std::path::PathBuf;
use tauri::{AppHandle, Emitter, Manager, Window};
use crate::types::UndoResult;
const PROGRESS_EVENT: &str = "analyze_progress";
fn app_data_dir(app: &AppHandle) -> Result<PathBuf, String> {
app.path()
.app_data_dir()
.map_err(|_| "app_data_dir_unavailable".to_string())
}
fn revert_snapshot(
session_dir: &PathBuf,
project_root: &PathBuf,
) -> Result<Vec<String>, String> {
let snap_dir = session_dir.join("snapshot");
if !snap_dir.exists() {
return Err("snapshot_missing".into());
}
let mut restored = vec![];
for entry in walkdir::WalkDir::new(&snap_dir)
.into_iter()
.filter_map(Result::ok)
{
if entry.file_type().is_dir() {
continue;
}
let snap_path = entry.path().to_path_buf();
let rel = snap_path
.strip_prefix(&snap_dir)
.map_err(|e| e.to_string())?;
let rel_str = rel.to_string_lossy();
if rel_str.starts_with(".missing/") || rel_str.starts_with(".missing\\") {
let orig: &str = rel_str
.strip_prefix(".missing/")
.or_else(|| rel_str.strip_prefix(".missing\\"))
.unwrap_or(&rel_str);
let abs = project_root.join(orig);
if abs.exists() {
fs::remove_file(&abs).map_err(|e| e.to_string())?;
restored.push(orig.to_string());
}
continue;
}
let abs = project_root.join(rel);
if let Some(parent) = abs.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
fs::copy(&snap_path, &abs).map_err(|e| e.to_string())?;
restored.push(rel.to_string_lossy().to_string());
}
Ok(restored)
}
#[tauri::command]
pub async fn undo_last(window: Window, app: AppHandle, path: String) -> UndoResult {
let project_root = PathBuf::from(&path);
if !project_root.exists() || !project_root.is_dir() {
return UndoResult {
ok: false,
session_id: String::new(),
restored: vec![],
error: Some("path_invalid".into()),
error_code: Some("PATH_INVALID".into()),
};
}
let data_dir = match app_data_dir(&app) {
Ok(d) => d,
Err(e) => {
return UndoResult {
ok: false,
session_id: String::new(),
restored: vec![],
error: Some(e),
error_code: Some("APP_DATA_DIR".into()),
};
}
};
let last_path = data_dir.join("history").join("last_session.txt");
let session_id = match fs::read_to_string(&last_path) {
Ok(s) => s.trim().to_string(),
Err(_) => {
return UndoResult {
ok: false,
session_id: String::new(),
restored: vec![],
error: Some("no_undo_available".into()),
error_code: Some("UNDO_NOT_AVAILABLE".into()),
};
}
};
let session_dir = data_dir.join("history").join(&session_id);
let _ = window.emit(PROGRESS_EVENT, "Откатываю изменения…");
match revert_snapshot(&session_dir, &project_root) {
Ok(restored) => UndoResult {
ok: true,
session_id,
restored,
error: None,
error_code: None,
},
Err(e) => UndoResult {
ok: false,
session_id,
restored: vec![],
error: Some(e),
error_code: Some("UNDO_FAILED".into()),
},
}
}

View File

@ -0,0 +1,31 @@
mod commands;
mod types;
use commands::{analyze_project, apply_actions, get_app_info, preview_actions, undo_last};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_process::init())
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.invoke_handler(tauri::generate_handler![
analyze_project,
preview_actions,
apply_actions,
undo_last,
get_app_info,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

View File

@ -0,0 +1,129 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ActionKind {
CreateFile,
UpdateFile,
DeleteFile,
CreateDir,
DeleteDir,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
pub id: String,
pub title: String,
pub description: String,
pub kind: ActionKind,
pub path: String,
pub content: Option<String>, // для create/update
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplyResult {
pub ok: bool,
pub session_id: String,
pub applied: Vec<String>,
pub skipped: Vec<String>,
pub error: Option<String>,
pub error_code: Option<String>,
pub undo_available: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UndoResult {
pub ok: bool,
pub session_id: String,
pub restored: Vec<String>,
pub error: Option<String>,
pub error_code: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiffItem {
pub path: String,
pub kind: String, // "create" | "update" | "delete" | "mkdir" | "rmdir"
pub before: Option<String>,
pub after: Option<String>,
pub summary: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PreviewResult {
pub ok: bool,
pub diffs: Vec<DiffItem>,
pub error: Option<String>,
pub error_code: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ProjectContext {
pub stack: Vec<String>,
pub domain: String,
pub maturity: String, // Prototype | MVP | Production-like
pub complexity: String, // Low | Medium | High
pub risk_level: String, // Low | Medium | High
}
#[derive(Debug, Clone, Serialize)]
pub struct LlmContext {
pub concise_summary: String,
pub key_risks: Vec<String>,
pub top_recommendations: Vec<String>,
pub signals: Vec<ProjectSignal>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ReportStats {
pub file_count: u64,
pub dir_count: u64,
pub total_size_bytes: u64,
pub top_extensions: Vec<(String, u64)>,
pub max_depth: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct Finding {
pub severity: String, // info|warn|high
pub title: String,
pub details: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct Recommendation {
pub title: String,
pub details: String,
pub priority: String, // high|medium|low
pub effort: String, // low|medium|high
pub impact: String, // low|medium|high
}
#[derive(Debug, Clone, Serialize)]
pub struct ProjectStructure {
pub project_type: String,
pub architecture: String,
pub structure_notes: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ProjectSignal {
pub category: String, // security|quality|structure
pub level: String, // info|warn|high
pub message: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct AnalyzeReport {
pub path: String,
pub narrative: String,
pub stats: ReportStats,
pub structure: ProjectStructure,
pub signals: Vec<ProjectSignal>,
pub findings: Vec<Finding>,
pub recommendations: Vec<Recommendation>,
pub actions: Vec<Action>,
pub project_context: ProjectContext,
pub report_md: String,
pub llm_context: LlmContext,
}

View File

@ -0,0 +1,49 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "PAPA YU",
"version": "0.1.0",
"identifier": "com.papa-yu",
"plugins": {
"updater": {
"endpoints": [
"https://github.com/yrippert-maker/papayu/releases/latest/download/latest.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQwOUFBRjlEQzMwODc5QjQKUldTMGVRakRuYSthMEM1RVp2aHpYbTduNG5zcTR5N0tYOFdqQzI0VXB3QlhRT0F6OEFpOVE5djYK"
}
},
"build": {
"beforeDevCommand": "cd \"${PAPAYU_PROJECT_ROOT:-$HOME/PAPA-YU}/desktop/ui\" && npm run dev -- --port 5173",
"beforeBuildCommand": "cd \"${PAPAYU_PROJECT_ROOT:-$HOME/PAPA-YU}/desktop/ui\" && npm run build",
"devUrl": "http://localhost:5173",
"frontendDist": "../ui/dist"
},
"app": {
"windows": [
{
"title": "PAPA YU",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

View File

@ -0,0 +1,356 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Tauri App PAPA-YU",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"build": {
"beforeDevCommand": "cd ../ui && npm run dev -- --port 5173",
"beforeBuildCommand": "cd ../ui && npm run build",
"devPath": "http://localhost:5173",
"distDir": "../ui/dist"
}
"app": {
"windows": [
{
"title": "Tauri PAPA-YU",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Tauri App PAPA-YU",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"build": {
"beforeDevCommand": "cd ../ui && npm run dev -- --port 5173",
"beforeBuildCommand": "cd ../ui && npm run build",
"devPath": "http://localhost:5173",
"distDir": "../ui/dist"
}
},
"app": {
"windows": [
{
"title": "Tauri PAPA-YU",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Tauri App PAPA-YU",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"build": {
"beforeDevCommand": "cd ../ui && npm run dev -- --port 5173",
"beforeBuildCommand": "cd ../ui && npm run build",
"devPath": "http://localhost:5173",
"distDir": "../ui/dist"
}
},
"app": {
"windows": [
{
"title": "Tauri PAPA-YU",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Tauri App PAPA-YU",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"build": {
"beforeDevCommand": "cd ../ui && npm run dev -- --port 5173",
"beforeBuildCommand": "cd ../ui && npm run build",
"devPath": "http://localhost:5173",
"distDir": "../ui/dist"
}
},
"app": {
"windows": [
{
"title": "Tauri PAPA-YU",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Tauri App PAPA-YU",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"build": {
"beforeDevCommand": "cd ../ui && npm run dev -- --port 5173",
"beforeBuildCommand": "cd ../ui && npm run build",
"devPath": "http://localhost:5173",
"distDir": "../ui/dist"
}
},
"app": {
"windows": [
{
"title": "Tauri PAPA-YU",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Tauri App PAPA-YU",
"version": "0.1.0",
"identifier": "com.tauri.dev",
"build": {
"beforeDevCommand": "cd ../ui && npm run dev -- --port 5173",
"beforeBuildCommand": "cd ../ui && npm run build",
"devPath": "http://localhost:5173",
"distDir": "../ui/dist"
}
},
"app": {
"windows": [
{
"title": "Tauri PAPA-YU",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}
"bundle": {
"active": true,

24
desktop/ui/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

73
desktop/ui/README.md Normal file
View File

@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

View File

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@ -1,12 +1,10 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PAPA YU</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>

4297
desktop/ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
desktop/ui/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.9.0",
"animejs": "^4.3.5",
"lucide-react": "^0.460.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^6.28.0",
"zustand": "^5.0.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.0",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
desktop/ui/src/App.css Normal file
View File

@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

50
desktop/ui/src/App.tsx Normal file
View File

@ -0,0 +1,50 @@
import { HashRouter, Routes, Route, useLocation } from 'react-router-dom';
import { useEffect } from 'react';
import { Dashboard } from './pages/Dashboard';
import { Tasks } from './pages/Tasks';
import { PolicyEngine } from './pages/PolicyEngine';
import { AuditLogger } from './pages/AuditLogger';
import { SecretsGuard } from './pages/SecretsGuard';
import { Updates } from './pages/Updates';
import { Diagnostics } from './pages/Diagnostics';
import { Layout } from './components/layout/Layout';
import { ErrorBoundary } from './components/ErrorBoundary';
import { ErrorDisplay } from './components/ErrorDisplay';
import { NotFound } from './pages/NotFound';
import { ROUTES } from './config/routes';
import { useAppStore } from './store/app-store';
function RouteTracker() {
const location = useLocation();
useEffect(() => {
try {
useAppStore.getState().setCurrentRoute(location.pathname);
} catch (_) {}
}, [location.pathname]);
return null;
}
function App() {
return (
<ErrorBoundary>
<HashRouter>
<RouteTracker />
<ErrorDisplay />
<Layout>
<Routes>
<Route path={ROUTES.TASKS.path} element={<Tasks />} />
<Route path={ROUTES.CONTROL_PANEL.path} element={<Dashboard />} />
<Route path={ROUTES.POLICY_ENGINE.path} element={<PolicyEngine />} />
<Route path={ROUTES.AUDIT_LOGGER.path} element={<AuditLogger />} />
<Route path={ROUTES.SECRETS_GUARD.path} element={<SecretsGuard />} />
<Route path={ROUTES.UPDATES.path} element={<Updates />} />
<Route path={ROUTES.DIAGNOSTICS.path} element={<Diagnostics />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</HashRouter>
</ErrorBoundary>
);
}
export default App;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,63 @@
import { Component, type ErrorInfo, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
export class ErrorBoundary extends Component<Props, State> {
state: State = {
hasError: false,
error: null,
errorInfo: null,
};
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({ error, errorInfo });
console.error('ErrorBoundary:', error, errorInfo);
}
render() {
if (this.state.hasError && this.state.error) {
if (this.props.fallback) return this.props.fallback;
return (
<div className="min-h-screen flex items-center justify-center bg-background p-8">
<div className="max-w-2xl w-full bg-card p-8 rounded-xl border">
<div className="text-center mb-6">
<div className="text-6xl mb-4"></div>
<h1 className="text-3xl font-bold mb-2">Произошла ошибка</h1>
<p className="text-muted-foreground">Приложение столкнулось с неожиданной ошибкой</p>
</div>
{import.meta.env.DEV && (
<div className="mt-6 p-4 bg-muted rounded-lg">
<pre className="text-xs overflow-auto">{this.state.error.toString()}</pre>
</div>
)}
<div className="mt-6 flex gap-4 justify-center">
<button
onClick={() => this.setState({ hasError: false, error: null, errorInfo: null })}
className="px-6 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
>
Вернуться
</button>
<button onClick={() => window.location.reload()} className="px-6 py-2 border rounded-md hover:bg-muted">
Перезагрузить
</button>
</div>
</div>
</div>
);
}
return this.props.children;
}
}

Some files were not shown because too many files have changed in this diff Show More