From e4b4dca53ac858cab1809490093528318a292866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 5 Sep 2026 13:24:00 +0300 Subject: [PATCH] Document picker code and architecture --- HISTORY/2026-09-05-code-documentation.md | 23 +++ README.md | 3 + docs/CODE-REFERENCE.md | 169 +++++++++++++++++++ site/app.py | 5 + site/static/style.css | 11 ++ site/templates/index.html | 10 ++ upload/README.md | 5 +- upload/__init__.py | 6 +- upload/frontend/table/add_file_with_dedup.js | 16 ++ upload/frontend/table/esc.js | 9 + upload/frontend/table/fs.js | 6 + upload/frontend/table/init_upload_table.js | 35 ++++ upload/frontend/table/on_files_change.js | 29 ++++ upload/frontend/table/on_folder_change.js | 20 +++ upload/frontend/table/rebase_tree.js | 10 ++ upload/frontend/table/render.js | 37 ++++ upload/frontend/table/set_status.js | 11 ++ upload/frontend/zip/list_zip_files.js | 34 ++++ 18 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 HISTORY/2026-09-05-code-documentation.md create mode 100644 docs/CODE-REFERENCE.md diff --git a/HISTORY/2026-09-05-code-documentation.md b/HISTORY/2026-09-05-code-documentation.md new file mode 100644 index 0000000..a9e9c74 --- /dev/null +++ b/HISTORY/2026-09-05-code-documentation.md @@ -0,0 +1,23 @@ +# Документация кода picker-only + +## Что сделано + +- Добавлены подробные комментарии и JSDoc в собственные JS-модули picker-а. +- Добавлены пояснения к Flask entrypoint, HTML-шаблону, CSS и пакетному + `upload/__init__.py`. +- Добавлен `docs/CODE-REFERENCE.md` со схемой архитектуры, моделью узлов, + состоянием, публичным API, функциями модулей и описанием legacy-кода. +- Ссылки на справочник добавлены в корневой и модульный README. +- Legacy `set_status.js` документирован отдельно; его поведение не менялось. + +## Границы + +- Поведение picker-а намеренно не изменялось. +- `site/static/vendor/fflate.min.js` не редактировался. +- Старые backend/VM-файлы и `.pyc` не удалялись. + +## Проверка + +- `node --input-type=module --check` для собственных JS-файлов: PASS. +- `python3 -m py_compile site/app.py upload/__init__.py`: PASS. +- `git diff --check`: PASS. \ No newline at end of file diff --git a/README.md b/README.md index 4434561..aadeeb4 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,6 @@ ZIP-файлы используются как контейнеры и раск совместимости с предыдущими этапами проекта. Текущая picker-only интеграция их не импортирует и не требует Flask API для обработки файлов. Переиспользуемая инструкция находится в `upload/README.md`. + +Полный справочник функций, состояния, DOM-контрактов и ограничений находится в +[`docs/CODE-REFERENCE.md`](docs/CODE-REFERENCE.md). diff --git a/docs/CODE-REFERENCE.md b/docs/CODE-REFERENCE.md new file mode 100644 index 0000000..57a6fdb --- /dev/null +++ b/docs/CODE-REFERENCE.md @@ -0,0 +1,169 @@ +# Справочник кода Upload Platform + +Документ описывает актуальную picker-only реализацию. VM upload, backend sessions +и API загрузки являются legacy и в текущем коде не используются. + +## Архитектура + +```text +site/app.py + -> site/templates/index.html + -> upload/frontend/table/init_upload_table.js + -> table/on_files_change.js + -> table/on_folder_change.js + -> table/add_file_with_dedup.js + -> table/render.js + -> zip/list_zip_files.js +``` + +Flask отдаёт страницу и ES-модули. Все выбранные файлы остаются в памяти браузера; +сервер не принимает содержимое файлов. + +## Модель данных + +Каждый узел дерева имеет поля: + +| Поле | Тип | Назначение | +|---|---|---| +| `id` | `string` | Уникальный идентификатор для DOM-кнопок и поиска. | +| `kind` | `file`, `folder`, `zip` | Тип узла. `file` является leaf. | +| `name` | `string` | Имя отображения в дереве. | +| `path` | `string` | Полный логический путь внутри выбора/архива. | +| `file` | `File \| null` | Исходный или созданный из ZIP browser File для leaf. | +| `children` | `Array` | Дочерние узлы группы. | +| `expanded` | `boolean` | Видимость дочерних строк в таблице. | + +Внутреннее состояние `initUploadTable`: + +| Поле | Назначение | +|---|---| +| `nodes` | Корневые узлы дерева. | +| `fileKeys` | Set ключей `path + NUL + size` для дедупликации. | +| `busy` | Защита от параллельной обработки async change-событий. | + +## `upload/frontend/table` + +### `add_file_with_dedup.js` + +| Функция | Вход | Выход | Побочные эффекты | +|---|---|---|---| +| `addFileWithDedup(state, fileNode)` | state и корневой file/folder/zip-узел | `boolean` | Фильтрует дубли и пустые группы, добавляет узел и ключи в state. | + +Дедупликация выполняется рекурсивно. Один и тот же путь с другим размером не +считается дублем. + +### `esc.js` + +| Функция | Вход | Выход | +|---|---|---| +| `esc(value)` | Любое значение | HTML-экранированная строка | + +Используется перед `innerHTML`, `data-path`, `aria-label` и текстом имени. + +### `fs.js` + +| Функция | Вход | Выход | +|---|---|---| +| `fs(bytes)` | Размер в байтах | Строка в `B`, `KB` или `MB` | + +### `rebase_tree.js` + +| Функция | Вход | Выход | Побочные эффекты | +|---|---|---|---| +| `rebaseTree(root, prefix)` | Дерево и префикс папки | То же дерево | Рекурсивно меняет `path`; для leaf создаёт File с новым именем. | + +Модуль является единственной реализацией rebasing для ZIP внутри выбранной папки +и для nested ZIP. + +### `on_files_change.js` + +| Функция | Вход | Выход | Назначение | +|---|---|---|---| +| `addFiles(state, cfg, files, elements)` | state, `allowedExt`, FileList/Array, DOM | `Promise` | Фильтрует обычные файлы, раскрывает ZIP, вызывает render. | +| `onFilesChange(state, cfg, elements)` | state, config, DOM | async callback | Блокирует повторные события через `busy` и вызывает `addFiles`. | + +Ошибка отдельного ZIP пропускается. `finally` обязательно освобождает `busy`. + +### `on_folder_change.js` + +| Функция | Вход | Выход | Назначение | +|---|---|---|---| +| `onFolderChange(state, cfg, elements)` | state, `allowedExt`, folder input и DOM | async callback | Строит дерево по `webkitRelativePath`, раскрывает ZIP и рендерит. | + +Корень пути становится folder-узлом. Вложенные папки создаются по сегментам +пути, затем корень проходит общую дедупликацию. + +### `render.js` + +| Функция | Вход | Выход | Назначение | +|---|---|---|---| +| `renderNode(node, depth)` | Узел и глубина | HTML | Рекурсивно создаёт строки дерева. Внутренняя функция. | +| `render(state, elements)` | state и DOM | `void` | Перерисовывает таблицу и счётчик. | +| `findNode(nodes, id)` | Массив узлов и id | `{node, nodes}` или `null` | Ищет узел и его родительский массив. | +| `flattenFiles(nodes, result)` | Дерево и аккумулятор | Массив leaf-файлов | Преобразует дерево для API приложения. | + +`render` считает все leaf-файлы, включая свернутые группы. `renderNode` показывает +дочерние строки только при `expanded === true`. + +### `init_upload_table.js` + +| Функция | Вход | Выход | Назначение | +|---|---|---|---| +| `removeFileMeta(state, node)` | state и удаляемое поддерево | `void` | Рекурсивно удаляет dedup-ключи. Внутренняя функция. | +| `removeNode(state, id)` | state и id | `void` | Удаляет узел из родительского массива. Внутренняя функция. | +| `initUploadTable(cfg)` | `allowedExt` и четыре DOM-элемента | API-объект | Создаёт state, события, render и публичные операции. | + +Публичный API: + +| Метод | Вход | Результат | +|---|---|---| +| `pickFiles()` | нет | Открывает обычный file input. | +| `pickFolder()` | нет | Открывает folder input. | +| `addFiles(files)` | FileList/Array | Добавляет файлы с фильтрацией и ZIP-разбором. | +| `getFiles()` | нет | `{path, name, size, file}[]` только для leaf. | +| `remove(id)` | id узла | Удаляет узел и его dedup-ключи. | +| `render()` | нет | Перерисовывает текущий state. | +| `clear()` | нет | Очищает дерево и dedup Set. | + +## `upload/frontend/zip` + +### `list_zip_files.js` + +| Функция | Вход | Выход | Назначение | +|---|---|---|---| +| `extensionAllowed(name, allowedExt)` | Имя и расширения | boolean | Проверяет расширение. Внутренняя. | +| `makeFile(data, name)` | Байты и путь | `File` | Создаёт browser File. Внутренняя. | +| `safeEntryParts(entryName)` | Сырой ZIP-путь | сегменты или null | Отбрасывает traversal и опасные пути. Внутренняя. | +| `node(kind, name, path, children, file)` | Метаданные узла | node | Создаёт узел. Внутренняя. | +| `addPath(root, parts, fileNode)` | Дерево, сегменты, узел | `void` | Создаёт папки и вставляет узел. Внутренняя. | +| `listEntries(data, zipName, allowedExt, depth)` | ZIP bytes и контекст | `Promise` | Рекурсивно обрабатывает ZIP. Внутренняя. | +| `listZipFiles(file, allowedExt)` | browser File и расширения | `Promise` | Публичная точка входа ZIP-парсера. | + +Безопасность: отклоняются абсолютные пути, backslash, пустые сегменты, `.` и +`..`; глубина nested ZIP ограничена 20. ZIP без разрешённых leaf-файлов возвращает +`null` и не создаёт пустую группу. + +## Flask и HTML + +### `site/app.py` + +| Маршрут/объект | Вход | Выход | +|---|---|---| +| `CONFIG` | `config.json` | JSON-конфигурация picker | +| `VERSION` | константа | Версия страницы | +| `index()` / `/` | HTTP GET | HTML с version и config | +| `health()` / `/health` | HTTP GET | `ok`, HTTP 200 | +| `upload_frontend(filename)` | HTTP GET и относительный путь | ES/CSS-файл из `upload/frontend` | + +### `site/templates/index.html` + +Шаблон создаёт два скрытых input: обычный file input и `webkitdirectory`, +передаёт четыре DOM-элемента в `initUploadTable`, подключает локальный `fflate` +до ES-модуля и связывает три кнопки с `pickFiles`, `pickFolder` и `clear`. + +## Legacy + +`upload/frontend/table/set_status.js` не импортируется актуальной страницей и +сохранён как legacy-заготовка. Vendor `site/static/vendor/fflate.min.js` не +документируется построчно: это внешняя библиотека, используемая как готовый +runtime dependency. diff --git a/site/app.py b/site/app.py index 1229bb5..24a4e85 100644 --- a/site/app.py +++ b/site/app.py @@ -5,27 +5,32 @@ from flask import Flask, render_template, send_from_directory ROOT = Path(__file__).resolve().parent.parent +# Конфигурация демо содержит только разрешённые расширения для browser picker. with (ROOT / "config.json").open(encoding="utf-8") as config_file: CONFIG = json.load(config_file) VERSION = "0.1.9" +# Flask нужен здесь только как статический сервер HTML, CSS и ES-модулей. app = Flask(__name__, template_folder="templates", static_folder="static") @app.route("/") def index(): + """Возвращает demo-страницу и передаёт в неё версию и JSON-конфигурацию.""" return render_template("index.html", version=VERSION, config=CONFIG) @app.route("/health") def health(): + """Возвращает 200 для liveness-проверки платформы.""" return "ok", 200 @app.get("/upload-frontend/") def upload_frontend(filename): + """Отдаёт один frontend-файл из изолированного каталога upload/frontend.""" return send_from_directory(ROOT / "upload" / "frontend", filename) diff --git a/site/static/style.css b/site/static/style.css index ca050f2..861fbfc 100644 --- a/site/static/style.css +++ b/site/static/style.css @@ -1,3 +1,4 @@ +/* Базовые цвета и типографика всего demo-picker. */ :root { color-scheme: light; font-family: Georgia, 'Times New Roman', serif; @@ -5,22 +6,29 @@ background: #e9eee8; } +/* Фон страницы намеренно отделён от карточной логики: picker не требует backend UI. */ body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 10% 0%, #f8fbf4, transparent 38%), #e9eee8; } +/* Ограничивает ширину рабочей области и сохраняет адаптивные боковые поля. */ .page { max-width: 980px; margin: 0 auto; padding: 64px 24px; } +/* Служебный идентификатор версии приложения. */ .eyebrow { color: #56745f; font: 700 12px/1.2 sans-serif; letter-spacing: 2px; } +/* Заголовок и пояснение вводят пользователя в сценарий выбора документов. */ h1 { max-width: 620px; margin: 12px 0; font-size: clamp(38px, 7vw, 72px); line-height: .95; font-weight: 400; } .lede { max-width: 560px; color: #5d6a61; font: 16px/1.6 sans-serif; } +/* Группа действий: выбор файлов, папки и очистка состояния. */ .toolbar { display: flex; flex-wrap: wrap; gap: 10px; margin: 36px 0 16px; } button { border: 0; border-radius: 4px; padding: 12px 17px; color: #f7faf4; background: #1f5a3b; font: 700 13px sans-serif; cursor: pointer; } button.secondary { color: #1f5a3b; background: #cbdcc9; } button.quiet { color: #5d6a61; background: transparent; } button:disabled { opacity: .45; cursor: not-allowed; } +/* Резервная область сообщения интегратора сохраняет высоту и не двигает таблицу. */ .status { min-height: 22px; color: #56745f; font: 13px sans-serif; } +/* Горизонтальный scroll на узких экранах не ломает таблицу дерева. */ .table-wrap { overflow-x: auto; border-top: 1px solid #b9c7ba; } table { width: 100%; border-collapse: collapse; font: 14px/1.4 sans-serif; } th, td { padding: 14px 10px; border-bottom: 1px solid #cbd5cc; text-align: left; } @@ -31,14 +39,17 @@ td:nth-child(2) { width: 140px; color: #68766c; } .tree-folder { color: #1f5a3b; } .tree-zip { color: #8a5a20; } .tree-chevron { display: inline-block; width: 12px; color: #78917d; font-size: 10px; } +/* Удаление работает для leaf и групп и обрабатывается делегированием в initUploadTable. */ .remove-btn { padding: 2px 7px; color: #8c5148; background: transparent; font: 20px/1 sans-serif; } .remove-btn:hover { color: #b32f22; } .tree-row td:first-child { white-space: nowrap; } .count { color: #5d6a61; font: 13px sans-serif; } +/* Legacy-стили результата сохранены для совместимости, текущий picker их не использует. */ .result { margin-top: 44px; padding-top: 18px; border-top: 2px solid #1f5a3b; } .result h2 { font-size: 26px; font-weight: 400; } .result li { margin: 8px 0; font: 14px sans-serif; } @media (max-width: 600px) { + /* На мобильном экране кнопки получают равномерную ширину. */ .page { padding: 36px 16px; } .toolbar button { flex: 1 1 42%; } } \ No newline at end of file diff --git a/site/templates/index.html b/site/templates/index.html index da73452..e305353 100644 --- a/site/templates/index.html +++ b/site/templates/index.html @@ -7,12 +7,15 @@ +
+

FILE INTAKE / {{ version }}

Загрузка документов

Выберите отдельные файлы или целую папку. Архивы будут раскрыты автоматически.

+
@@ -20,17 +23,22 @@
+

+
ПутьРазмерСтатус
+

+