AAYMA
RU
Войти

Граф денег

AML-инструмент: по edges/nodes/transactions.parquet строит направленный граф переводов, присваивает объяснимые роли и типологии, выделяет…

Рабочий MVP
Команда «Калифорнийское бюро расследований» 4💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: Калифорнийское бюро расследований
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Рабочий MVP

Задача кейса: «Граф денег»: восстановить структуру организованной группы по сети транзакций (AML)

Суть

AML-инструмент: по edges/nodes/transactions.parquet строит направленный граф переводов, присваивает объяснимые роли и типологии, выделяет сообщества, ранжирует клиентов и выгружает nodes_roles.csv, clusters.csv, top_nodes.csv; веб-граф с загрузкой датасета.

Как устроено

Pipeline на pandas + NetworkX проверяет входные Parquet, строит направленный граф, по порогам THRESHOLDS (число плательщиков/получателей и т.п.) присваивает роли с role_score и evidence, выделяет кластеры с гипотезами и считает priority_score для топ-25. Дополнительно — типологии и resilience.json (оценка удаления топ-N узлов). FastAPI отдаёт граф и экспорт; фронтенд на Vue 3 + vis-network работает офлайн, форма загрузки сверяет агрегаты рёбер с транзакциями до активации набора.

Ключевые решения

  • NetworkX направленный граф
  • rule-based роли с порогами
  • AML-типологии
  • выделение сообществ
  • приоритизация узлов
  • анализ устойчивости (удаление топ-N)

Особенности

Загрузка датасета через браузер с версионированием и проверкой согласованности рёбер и транзакций до активации; полностью офлайн-интерфейс без CDN.

Стек

Python 3.12 · FastAPI · Uvicorn · Pydantic · pandas · NetworkX · PyArrow · Vue 3 · vis-network · Docker Compose · pytest


Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-4cea2b8e-team.

README

README.md

Граф денег

Инструмент для AML-аналитика банка: по обезличенной выгрузке переводов строит направленный граф, присваивает объяснимые роли, выделяет сообщества и ранжирует клиентов для углублённой проверки. Основной вопрос: кого смотреть первым и почему?

HackAlem AI, 23.09.2026. Команда «Калифорнийское бюро расследований»: Аяган Елнур, Кайсарбеков Аргын, Пермин Константин.

Роли и типологии — гипотезы по наблюдаемым данным, не утверждения о виновности. Внешние сведения о клиентах не используются. GPU, API-ключ и платные сервисы для расчёта и интерфейса не нужны.

1. Быстрый запуск через Docker

На Windows рекомендуем этот способ: отдельно устанавливать Python и активировать .venv не требуется. Нужен работающий Docker Desktop в режиме Linux containers; на Windows — с WSL 2. Официальная инструкция установки. Compose входит в Docker Desktop: документация.

Скачайте репозиторий через Git либо распакуйте ZIP. Все команды выполняются из папки с docker-compose.yml:

git clone https://github.com/BAITC-Hacks/hack-4cea2b8e-team.git
cd hack-4cea2b8e-team
docker --version
docker compose version
docker info

Если репозиторий уже скачан, повторять клонирование не нужно. Запустите Docker Desktop до выполнения команд. Поместите три файла организаторов в data/:

data/
  edges.parquet
  nodes.parquet
  transactions.parquet

Запуск приложения одной командой:

docker compose up --build -d

Откройте http://localhost:8000/ — сразу появится граф. Если файлов пока нет, сервер всё равно запустится и предложит форму загрузки. Для замены набора нажмите «⇧ Загрузить датасет» рядом со «Сводкой». Форма принимает три непустых Parquet-файла, каждый размером до 100 МиБ.

Первая сборка требует интернета для базового образа и зависимостей. После сборки анализ и интерфейс работают локально без интернета. .env и LLM-ключ не нужны.

Проверка готовности и управление

docker compose ps
docker compose logs --tail=100 app

В ответе health ожидаются status: "ok", а после загрузки данных — data_ready: true и graph_loaded: true. Для выборки трека n_nodes равно 2248. Статус контейнера healthy сам по себе подтверждает доступность сервера, но не наличие данных.

docker compose stop
docker compose start

После изменения кода повторите docker compose up --build -d.

Если порт 8000 занят

Выберите свободный порт, например 8002. PowerShell:

$env:GRAPH_PORT = "8002"
docker compose up --build -d

macOS/Linux:

GRAPH_PORT=8002 docker compose up --build -d

Откройте http://localhost:8002/; в остальных URL также замените 8000 на 8002. Внутренний порт контейнера остаётся 8000. При последующих up сохраняйте выбранное значение GRAPH_PORT. Публикация порта ограничена локальным компьютером.

Если нужно сохранить старые контейнеры этого же проекта, запускайте отдельную копию через docker compose -p money-graph-current up --build -d. Для этой копии используйте -p money-graph-current и в командах ps, logs, exec, cp, stop, start.

2. Один запуск от Parquet до CSV

После сборки и запуска контейнера, при наличии исходных файлов в data/:

docker compose exec app python -m pipeline.run --data /app/data --out /app/out

Команда проверяет входные данные, рассчитывает роли и кластеры, создаёт все три обязательные выгрузки и дополнительный resilience.json. По умолчанию топ содержит 25 узлов. --top N меняет размер; на достаточно большой сети минимум — 20.

Сохраните обязательные результаты на компьютер:

docker compose cp app:/app/out/nodes_roles.csv ./nodes_roles.csv
docker compose cp app:/app/out/clusters.csv ./clusters.csv
docker compose cp app:/app/out/top_nodes.csv ./top_nodes.csv

/app/out находится внутри контейнера: скопируйте результаты до его пересоздания. Не добавляйте полученные CSV в коммит автоматически — сдайте их отдельно по правилам организаторов. Каталоги data/ и out/ исключены из Git.

Если набор загружен через браузер

Форма проверяет схемы, сверяет агрегаты рёбер с транзакциями и выполняет расчёт до активации. При ошибке прежний набор остаётся доступным. Версии сохраняются в data/.uploads/<dataset_id>/; указатель data/.active-dataset выбирает активную версию. Папка data/ подключена к контейнеру и сохраняется после перезапуска.

CLI читает только каталог --data, а HTTP-экспорт — активный набор интерфейса. Для загруженного через форму набора скачайте обязательные файлы по ссылкам:

Либо передайте CLI каталог /app/data/.uploads/<dataset_id> вместо /app/data. Не сравнивайте граф одного набора с CSV другого. Замена исходных файлов в data/ не отменяет уже выбранную загруженную версию. Другую открытую вкладку графа после смены набора нужно обновить. Автоматической очистки старых версий пока нет.

3. Версии и запуск без Docker

КомпонентВерсия
DockerDocker Desktop с Linux containers и плагином docker compose (v2 или новее)
Python3.12 в Docker (python:3.12-slim); без Docker — 3.11 или 3.12
FastAPI / Uvicorn / Pydantic0.115.6 / 0.34.0 / 2.10.4
pandas / NetworkX / PyArrow2.2.3 / 3.4.2 / 18.1.0
pytest / httpx8.3.4 / 0.28.1
Vue / vis-network3.5.22 / 9.1.9, локальные файлы в web/vendor/

Все прямые Python-зависимости — в requirements.txt. Образ python:3.12-slim обновляемый: patch-версия и digest не закреплены; транзитивные Python-зависимости также не зафиксированы отдельным lock-файлом. Сборка не заявляется побитово воспроизводимой. Node.js и npm для запуска не нужны.

Windows PowerShell

При установленном Python 3.12, из корня проекта:

py -3.12 -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
python -m pipeline.run --data data --out out
$env:DEMO_MODE = "1"
python -m uvicorn app.main:app --host 127.0.0.1 --port 8000

В Windows CMD активация выполняется командой .venv\Scripts\activate.bat. Если PowerShell запрещает Activate.ps1, активация необязательна: вместо python во всех последующих командах используйте .\.venv\Scripts\python.exe. Не переносите готовую .venv с macOS на Windows — создайте её заново.

macOS/Linux

python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python -m pipeline.run --data data --out out
DEMO_MODE=1 python -m uvicorn app.main:app --host 127.0.0.1 --port 8000

При Python 3.11 замените первую команду на python3.11 -m venv .venv. Python 3.9 не подходит. Откройте главную страницу. Адреса /graph и /graph.html также работают. Остановка — Ctrl+C. DEMO_MODE=1 относится только к стартовому LLM-модулю: граф всегда рассчитывается по Parquet, без демонстрационной подмены.

Для повторного запуска достаточно активировать окружение и запустить Uvicorn. DATA_DIR задаёт каталог данных сервера, DB_PATH — SQLite стартового модуля; CLI использует собственный --data. Запускайте один процесс, без --workers.

Автономная установка без Docker

На машине с теми же ОС, архитектурой и версией Python заранее подготовьте пакеты:

python -m pip download --only-binary=:all: -r requirements.txt -d wheelhouse

Перенесите репозиторий, данные и wheelhouse на целевую машину:

python -m pip install --no-index --find-links=wheelhouse -r requirements.txt

Граф, загрузка, поиск и сводка не используют CDN. Swagger /docs — вспомогательная страница с внешними ресурсами; автономный контракт доступен через /openapi.json и app/API.md.

4. Вход и точная схема результатов

Данные трека: внутрибанковские переводы за 1–31 июля 2026, исходящий обход от 81 seed-клиента до четырёх колен, операции от 5 000 ₸.

ФайлОбязательные поляОбъём
edges.parquetsrc, dst, sum_kzt, n_tx, depth3 119 рёбер
nodes.parquetgid, depth, is_seed2 248 узлов
transactions.parquetsrc, dst, date, sum_kzt4 840 операций

Три обязательных CSV содержат только следующие колонки в указанном порядке:

ФайлКолонки
nodes_roles.csvgid, role, role_score, cluster_id, priority_score, evidence
clusters.csvcluster_id, n_nodes, n_seed, sum_kzt_internal, top_gids, hypothesis
top_nodes.csvrank, gid, role, priority_score, why

gid сохраняется как int64; cluster_id, счётчики и ранг — целые числа, оценки и суммы — числовые, остальные поля — текстовые. role_score и priority_score находятся в диапазоне 0–1; evidence непустой, до 200 символов. top_gids — до пяти приоритетных gid кластера, разделённых |. При импорте в Excel задавайте gid текстом, чтобы не потерять точность длинных номеров. В JSON API идентификаторы передаются строками, их нельзя преобразовывать в JavaScript Number.

Дополнительные файлы по флагу --extended:

  • nodes_roles_extended.csv: поля обязательной таблицы узлов плюс typologies, typology_evidence.
  • top_nodes_extended.csv: поля обязательного топа плюс typologies.

Например: python -m pipeline.run --data data --out out --extended. Флаг не меняет схему обязательных CSV. Без него расширенные файлы не создаются и не обновляются. Типологии разделены |; пустая строка означает отсутствие срабатываний. Расширенные версии доступны также через /api/export/{filename}. resilience.json содержит оценку удаления топ-N узлов из наблюдаемого графа.

5. Критерии ролей и пороги

Все пороги — в THRESHOLDS файла pipeline/roles.py. Обозначения: P — число разных плательщиков, R — число разных получателей, K = sum_out / sum_in в пределах выборки.

truncated означает глубину ≥4 при отсутствии исходящих. ratio_reliable истинен только для не-seed, не-обрезанного узла с положительными входящими. Это разрешение применить отношение в эвристике, а не гарантия полного баланса. Правила проверяются сверху вниз, назначается первая подходящая роль:

ПорядокРольУсловиеrole_score
1peripheraltruncated: дальнейшие исходящие не видны0,20
2coordinatorP ≥5 и R ≥50,70
3consolidatorP ≥8, ratio_reliable и K ≤0,50,80
4distributorR ≥200,80
5transitratio_reliable и 0,8 ≤K ≤1,20,75
6terminalratio_reliable, K ≤0,1, sum_in ≥1 000 000 ₸0,75
7peripheralостальные случаи0,30

evidence объясняет сработавшее правило конкретными метриками клиента. role_score — заданная эвристическая оценка правила, не обученная вероятность. Размеченных ролей нет, accuracy не заявляется. Пороги требуют экспертной проверки; несрабатывание правил не означает отсутствия риска.

6. Ранжирование, типологии и кластеры

priority_score = 0,5 × вес роли
               + 0,3 × (sum_in + sum_out) / максимальный оборот клиента
               + 0,2 × (P + R) / максимальное число связей клиента

Вес роли: coordinator 1,0; consolidator 0,9; distributor 0,7; terminal 0,6; transit 0,5; peripheral 0,1. При нулевом максимуме соответствующее слагаемое равно нулю. Оценка округляется до четырёх знаков; сортировка — по убыванию оценки, затем gid по возрастанию. API раскрывает слагаемые в priority_breakdown. why объясняет роль; формула выше объясняет место в приоритете.

В текущем топ-25: 18 coordinator, 4 consolidator, 3 distributor. Перекос вызван порядком правил и весами, а не доказывает наличие 18 организаторов. Проверка чувствительности порогов и пересечения правил ещё не выполнена.

Типологии независимы от основной роли, у узла может быть несколько признаков:

ПризнакПравило
structuring≥4 операции одной пары, коэффициент вариации сумм ≤0,35, весь период пары ≤7 дней
layering≥70% наблюдаемых входящих сопоставлены с исходящими за 0–2 дня; FIFO без повторного расходования суммы
synchronized-inflow≥3 разных плательщиков в один день
round-trippingучастие в направленных циклах длиной до 4

Совпадение дат не доказывает движение тех же денег; время внутри дня неизвестно. Правило повторных сумм может пропустить короткий всплеск внутри длинного периода. Циклы показывают возвратные связи, но не доказанный возврат средств.

Кластеры рассчитываются взвешенным Louvain с seed=42 на неориентированной проекции: встречные суммы складываются. Изолированные узлы сохраняются. sum_kzt_internal включает только рёбра с обоими концами внутри кластера. Гипотеза назначения сообщества строится по составу ролей.

7. Сценарий аналитика и демо на 5 минут

  1. 0:00–1:00. Запустить пайплайн, показать время и три обязательных CSV.
  2. 1:00–2:00. Открыть граф, выбрать клиента из топа или найти gid. Объяснить роль по числам в карточке и её отличие от приоритета проверки.
  3. 2:00–3:00. Нажать на стрелку, показать сумму и операции пары; перейти к отправителю/получателю, воспользоваться историей.
  4. 3:00–4:00. Разобрать seed с «Н/Д» и узел четвёртого колена: неполный баланс и отсутствие видимых исходящих не доказывают удержание денег.
  5. 4:00–5:00. Показать кластер, фильтры и «Сводку». Сформулировать, какие дополнительные данные потребуются для проверки гипотезы.

Подготовьте зависимости и исходные файлы заранее. Репетируйте поиск трёх произвольных gid, а не только заранее выбранных примеров.

Иконка и цвет обозначают роль. Число у иконки — входящие плюс исходящие операции в выборке, не число соседей. Полный gid виден при наведении/выделении и в карточке. Стрелки прямые; после построения физика выключается, узлы можно перетаскивать. Подграф ограничен 150 узлами, при ограничении отображается предупреждение. Фильтры меняют видимость, а не исходные данные.

«Сводка» показывает всю активную выгрузку независимо от фильтров: роли, оборот, крупнейшие связи, примеры клиентов и потоки между ролями. Ссылки gid открывают узлы. Сумма переводов не равна уникальному объёму денег: они могли пройти несколько звеньев. Входящие и исходящие — две стороны тех же операций.

8. Проверки и соответствие обязательным требованиям

В Docker:

docker compose exec -e DEMO_MODE=1 app python -m pytest -q

Без Docker, в окружении проекта: python -m pytest -q.

Требование ТЗРеализация и проверка
Один запуск до трёх CSV, ≤5 минутpipeline.run; команда в разделе 2
Роль, оценка и обоснование для каждого узлаnodes_roles.csv; проверки заполненности, диапазонов, длины evidence
Объяснимые критерииФормальные правила и порядок в разделе 5, метрики в карточке
Кластеризацияcluster_id каждого узла; размер, seed, оборот, top_gids и гипотеза в clusters.csv
Топ ≥20 и экран сетиТоп-25, направление связей, роли, фильтры кластеров, поиск по gid

Последний локальный прогон: 20 тестов прошли. Проверяются также точная схема CSV через CLI и HTTP, сохранность int64 gid, детерминизм и сводка. Есть предупреждения об устаревающих API зависимостей. Тесты не покрывают все пользовательские действия; ручные проверки описаны в контракте API.

Контрольные результаты данных трека: 2 248 узлов, 3 119 рёбер, 4 840 операций, 91 кластер, 25 строк топа, сумма переводов 365 890 012,01 ₸. Локальные замеры расчёта в CLI на Python 3.12 — 0,7–4,3 секунды, без установки пакетов. Время зависит от машины. В Docker на macOS (Python 3.12.14) также прошли 20 тестов и CLI-прогон за 0,7 секунды; приложение загружает 2 248 узлов и отдаёт граф по /. Это не заменяет независимого прогона на Windows перед защитой.

9. Ограничения данных и подхода

  • 444 узла на четвёртом колене имеют обрезанные исходящие: они не получают terminal только из-за нуля исходящих. Для определения роли нужен более глубокий обход.
  • У seed входящие неполны: отношение потоков не участвует в правилах консолидации, транзита и конечного получателя. В карточке показано «Н/Д» с причиной.
  • Полный баланс неизвестен для всех клиентов; превышение исходящих над входящими не доказывает транзит или происхождение средств.
  • 19 изолированных seed сохранены; всего 31 seed без исходящих. Граф содержит 35 компонент: 16 с рёбрами и 19 изолятов. Это объясняет отличие от 16 компонент в описании связанной части исходных данных.
  • Переводы ниже 5 000 ₸, межбанковские операции и события вне месяца не видны. ФИО, ИИН, возраст, организация и остатки не восстанавливаются и не выдумываются.
  • Роли, веса и типологии эвристические; экспертной валидации и размеченного эталона нет. Кратчайший путь и циклы не учитывают доказанное перемещение конкретных средств.
  • AI-ассистент с ответами по графу не реализован. /api/generate — отдельный стартовый модуль без доступа к данным расследования; он не участвует в расчёте.
  • Плотные подграфы могут быть перегружены. Ограничение экрана не означает отсутствие остальных связей в исходных данных.
  • Нет авторизации, аудита, распределённой синхронизации и автоматического удаления версий датасетов. Реализация предназначена для локального однопроцессного демо.

10. Масштабирование до миллиона узлов

Сейчас pandas, NetworkX и результат анализа размещаются в памяти одного процесса. Миллион узлов не тестировался; ниже план развития, а не готовая возможность:

  1. Хранить операции в Parquet по датам; агрегировать пакетно через DuckDB/Polars либо SQL. Планировать нагрузку по рёбрам и операциям, не только по числу узлов.
  2. Перенести графовые расчёты на CSR/компактные массивы или igraph; измерить RAM и время на промежуточных объёмах.
  3. Отделить фоновый расчёт от API; сохранять версию результата, индексировать соседства и даты, добавить кэш и пагинацию.
  4. Метрики обновлять инкрементально, сообщества — периодически. Глобальную нормировку приоритета пересчитывать согласованно для всей версии.
  5. Ограничивать поиск циклов компонентами и вычислительным бюджетом; на плотных графах число циклов резко растёт.
  6. Передавать в браузер агрегаты сообществ и небольшие подграфы; добавить отмену запросов и измерение задержек. Не отображать миллион узлов одновременно.

11. Архитектура и материалы для сдачи

flowchart LR
    A[Три Parquet] --> B[Проверка схем и агрегатов]
    B --> C[Граф, метрики и Louvain]
    C --> D[Типологии, правила ролей и приоритет]
    D --> E[Три CSV и resilience.json]
    D --> F[GraphStore и FastAPI]
    F --> G[Vue и vis-network: граф, карточки, сводка]

Текстовая схема: данные → валидация → метрики/сообщества → роли/приоритет → CSV и интерфейс.

КаталогНазначение
pipeline/Загрузка, валидация, граф, роли, типологии, кластеры и CLI
app/GraphStore, API, загрузка датасетов, сериализация и сводка
web/Граф, форма загрузки, сводка, локальные библиотеки
tests/Проверки ролей, сводки, CSV и приложения
docs/Архитектура и сторонние компоненты

Для сдачи нужны репозиторий с README, три обязательных CSV, схема решения и пятиминутное демо. Расширенные CSV и resilience.json — дополнительные материалы. Перед защитой выполните тесты, пересоздайте CSV из нужного набора, проверьте запуск на другой машине и работу без интернета. Не публикуйте .env, ключи и данные трека вне разрешённого организаторами способа передачи.

12. Частые проблемы

СимптомРешение
docker не найденУстановить Docker Desktop и открыть новый терминал
Cannot connect to Docker daemonЗапустить Docker Desktop и проверить docker info
Port is already allocatedВыбрать свободный GRAPH_PORT, как в разделе 1
После обновления старый экранDocker: docker compose up --build -d; без Docker: перезапустить Uvicorn; обновить страницу
Ошибка пути к Python 3.14 / Python 3.9Использовать Docker либо установить 3.12 и создать новое окружение
pytest не найденАктивировать окружение, установить requirements и запускать python -m pytest -q
Запрещён Activate.ps1Использовать .\.venv\Scripts\python.exe напрямую
Нет данных / API возвращает 503Загрузить три согласованных файла; проверить сообщение формы и журнал сервера
После замены data/ остался старый графАктивна версия из загрузки; заменить её через форму либо явно выбрать нужный каталог
Все узлы скрыты фильтрамиВключить роли/кластеры обратно либо выбрать клиента заново
Звёзды
⭐ 0
Форки
0
Язык
Python
Последний push
в прошлом месяце
Коммиты
18
Авторы
4
Файлы
43
Ветки
1

Авторы коммитов

Kostya 9yelnurayagan 6Argyn Kaisarbekov 2Strickbeck 1

История коммитов

  • 2026-09-23 17:33fix: привести CSV к ТЗ, обновить запуск и документацию, объединить фронтендyelnurayagan
  • 2026-09-23 17:20fix: animationsKostya
  • 2026-09-23 17:03fixKostya
  • 2026-09-23 17:00Merge branch 'main' of https://github.com/BAITC-Hacks/hack-4cea2b8e-teamKostya
  • 2026-09-23 16:59fix: merge conflictKostya
  • 2026-09-23 16:58feat: объединить загрузку, сводку и выделение графа, обеспечить автономностьyelnurayagan
  • 2026-09-23 16:49Merge branch 'main' of https://github.com/BAITC-Hacks/hack-4cea2b8e-teamKostya
  • 2026-09-23 16:48feat(front): add animationsKostya
Все коммиты (18)
  • 2026-09-23 16:45moduleStrickbeck
  • 2026-09-23 16:24feat(front): Add upload pageKostya
  • 2026-09-23 15:58fix: исправить навигацию графа и добавить просмотр операцийyelnurayagan
  • 2026-09-23 15:47feat: добавить аналитику графа и APIyelnurayagan
  • 2026-09-23 15:47fix: улучшить читаемость графа и карточекyelnurayagan
  • 2026-09-23 14:57Merge branch 'main' of https://github.com/BAITC-Hacks/hack-4cea2b8e-teamKostya
  • 2026-09-23 14:55feat: light theme + unicode role icons for graph.htmlKostya
  • 2026-09-23 13:54Fix team member name order in READMEArgyn Kaisarbekov
  • 2026-09-23 13:53Revise hackathon team and participants in READMEArgyn Kaisarbekov
  • 2026-09-23 13:50chore: зафиксировать стартовый каркас проектаyelnurayagan
Структура репозитория (43 файлов)
.env.example
.githooks/pre-commit
.gitignore
.gitmessage
AGENTS.md
Dockerfile
Makefile
README.md
README.orig.md
app/API.md
app/__init__.py
app/config.py
app/db.py
app/graph_store.py
app/llm.py
app/main.py
app/responses.py
app/summary.py
app_fast.py
docker-compose.yml
docs/ARCHITECTURE.md
docs/THIRD_PARTY.md
pipeline/__init__.py
pipeline/analysis.py
pipeline/graph.py
pipeline/roles.py
pipeline/run.py
pipeline/typologies.py
requirements.txt
scripts/explore.py
scripts/hourly.sh
scripts/setup-git.sh
scripts/smoke.sh
tests/test_exports.py
tests/test_pipeline.py
tests/test_smoke.py
web/app.js
web/graph.html
web/index.html
web/styles.css
web/summary.css
web/summary.js
web/upload.html

Обсуждение 0 комментариев

Войдите, чтобы комментировать

Пока нет комментариев — будьте первым.