Інтерактивний React + TypeScript застосунок для розрахунків метрик програмної інженерії
👉 vscrm.github.io/Lab-system — розгорнута версія на GitHub Pages
Комплексний веб-застосунок для автоматизації розрахунків з 6 лабораторних робіт з курсу "Оцінка вартості програмних систем": динамічні обчислення за моделями COCOMO (Basic, Detailed, II Post-Architecture), методом функціональних точок (FPA, з AI-аналізом зображень), оцінка чисельності персоналу та розміру програмної частини ІС.
Продакшн-орієнтована архітектура: строга типізація без any, Zod-валідація всіх полів, 111 автотестів, UK/EN-переклад, моніторинг помилок і бюджет розміру бандла.
Кожна з 6 лабораторних робіт реалізована як окремий інтерактивний, лениво завантажуваний модуль із покроковими розрахунками, формулами в реальному часі та валідацією введення.
Автор: Кручкевич Б.В., група ІП-23-1 Університет: Івано-Франківський національний технічний університет нафти і газу (ІФНТУНГ) Рік: 2026
- Основні можливості
- Лабораторні роботи
- Швидкий старт
- Технологічний стек
- Архітектура
- Валідація введених даних
- Інтернаціоналізація (i18n)
- Тестування
- Безпека
- Повна структура проекту
- AI-аналіз зображень у ЛР №4
- Дизайн-система
- Змінні середовища
- Розгортання на GitHub Pages
- Відомі обмеження й що можна покращити далі
- Ліцензія та автор
- Корисні посилання
- ✅ Динамічні розрахунки — усі формули перераховуються в реальному часі при зміні будь-якого поля
- ✅ Покрокова візуалізація — кожна лабораторна розбита на логічні кроки з проміжними обчисленнями
- ✅ Математичні формули — відображення підстановок значень у формулу, а не лише кінцевого результату
- ✅ Zod-валідація — усі числові поля (включно з 22 факторами/драйверами COCOMO II у ЛР №5/6), інлайн-помилки
- ✅ Адаптивний дизайн — комп'ютери, планшети, телефони
- ✅ UK/EN локалізація — перемикач мови в шапці, повний переклад усіх 6 лабораторних
- ✅ AI-аналіз зображень — автоматичне визначення DET/RET/FTR з фото (ЛР №4)
- ✅ 111 автотестів, строгий TypeScript, моніторинг помилок
| Лабораторна | Назва | Ключові метрики |
|---|---|---|
| ЛР №1 | Трудомісткість розробки ПП | ФР, РК, E, Z, T (COCOMO II + Функціональні точки) |
| ЛР №2 | Чисельність виконавців проекту | V₃, Tₕ, Kc, Kт, Kₕ, T₃ |
| ЛР №3 | Оцінка вартості за COCOMO | E, TDEV, SS, P (базовий/деталізований рівні) |
| ЛР №4 | Метод функціональних точок + 🤖 AI | ILF, EQ, DET, RET, FTR, UFT, AFT |
| ЛР №5 | Засоби оцінки вартості ПЗ | COCOMO II Post-Architecture (5 факторів масштабу + 17 драйверів) |
| ЛР №6 | Розмір програмної частини ІС | KSLOC, DM, CM, IM, ESLOC |
Кожна лаба — окремий React-компонент з код-сплітингом (React.lazy), доступний за прямим посиланням /Lab-system/lab/<slug> і коректно обробляється при F5 (детальніше — фолбек dist/404.html, scripts/copy-404.mjs).
Потрібен Node.js ^20.19.0 або >=22.12.0 (вимога Vite 7).
git clone https://github.com/VSCRM/Lab-system.git
cd Lab-system
npm install
npm run devnpm run dev сам відкриє браузер на http://localhost:5173/Lab-system/.
| Команда | Що робить |
|---|---|
npm run dev |
Dev-сервер із hot reload, автовідкриттям браузера |
npm run build |
tsc -b && vite build, потім автоматично копіює index.html → 404.html (для GitHub Pages) |
npm run preview |
Локальний перегляд production-збірки |
npm run typecheck |
Перевірка типів без збірки (tsc -b --noEmit) |
npm run lint |
ESLint (strict, no-explicit-any заборонено) |
npm run format |
Prettier --write на весь проект |
npm run format:check |
Prettier --check, без змін файлів |
npm run test |
Vitest, один прогін (111 тестів) |
npm run test:watch |
Vitest у watch-режимі |
npm run coverage |
Vitest з покриттям коду |
npm run size |
Збірка + перевірка бюджету розміру бандла (size-limit) |
npm run audit:check |
npm audit лише по production-залежностях |
npm run deploy |
Публікація dist/ на GitHub Pages (gh-pages) |
Frontend: React 19 (function components, hooks)
Мова: TypeScript 5.9, strict mode, без `any` (ESLint no-explicit-any: error)
Валідація: Zod — усі числові поля форм, без винятків
Маршрутизація: React Router 8 (уніфікований пакет `react-router`, глибокі посилання /lab/:slug, 404-сторінка)
Стилі: Tailwind CSS 3 (JIT), єдина система utility-класів
Іконки: Lucide React
Інтернаціоналізація: i18next + react-i18next (UK за замовчуванням, EN — повна підтримка)
Build: Vite 7 + @vitejs/plugin-react
Тести: Vitest 4 (jsdom) + Testing Library (React, jest-dom, user-event)
Лінт/формат: ESLint 9 (typescript-eslint strict) + Prettier + eslint-config-prettier
Моніторинг помилок: Sentry (@sentry/react), опційно, lazy-loaded, вимкнено за замовчуванням
Бюджет бандла: size-limit (@size-limit/file)
Деплой: вручну через `npm run deploy` (gh-pages)
AI-інтеграція: OpenRouter API (безкоштовні vision-моделі з failover та таймаутом) — лише ЛР №4
Проєкт побудований за принципами SOLID з чіткою декомпозицією за відповідальністю:
lib/calculations/* — чисті функції-формули. Без React, без DOM, без i18n.
Мовонезалежні (напр. рівні складності FPA повертають
"low"/"medium"/"high", а не готовий текст мовою).
Тестуються ізольовано (69 з 111 тестів — саме тут).
lib/validation/* — Zod-схеми полів, одна фабрика (`numberField`) для всіх лаб.
lib/services/* — зовнішні API (OpenRouter), з таймаутом і failover.
lib/config.ts — конфігурація (URL, ліміти, таймаути) в одному місці.
lib/monitoring.ts — обгортка над Sentry, lazy-loaded, safe no-op без DSN.
hooks/* — стан форм і повторна логіка (валідоване число, таблиця персоналу,
фактори COCOMO, завантаження/аналіз зображень).
components/ui/* — переюзабельні примітиви (NumberField, SelectField, Button, Alert...).
components/cocomo/* — спільні блоки для ЛР №5 і №6 (ScaleFactorGrid, CostDriverGrid,
StaffAllocationTable, FinalMetricsPanel) — усувають дублювання
між двома дуже схожими лабораторними.
components/layout/* — каркас застосунку, ErrorBoundary (на двох рівнях: навколо
всього App і навколо кожної окремої лаби), перемикач мови.
components/labs/* — картки/навігація списку лаб.
features/labN/ — кожна лаба: LabN.tsx — це ЛИШЕ оркестратор (стан через хуки,
результат через lib/calculations), уся розмітка — в
components/*Step.tsx. Жодна лаба не є одним великим файлом.
pages/* — маршрути (Home, Lab, NotFound), код-сплітинг лаб через React.lazy.
constants/* — єдине джерело метаданих лаб (slug/ікона/колір) і коефіцієнтів COCOMO.
i18n/* — переклади (детальніше нижче).
Кожне числове поле в кожній лабораторній — включно з 5 факторами масштабу та 17 драйверами витрат COCOMO II у ЛР №5/6 — проходить через Zod-схему з lib/validation/schemas.ts, а не голий Number(e.target.value). Помилка показується інлайн під полем (aria-invalid, aria-describedby, role="alert"), розрахунок при цьому не падає — використовується безпечне значення-заглушка, поки користувач не виправить ввід.
API-ключ OpenRouter (ЛР №4) також валідується окремою Zod-схемою (формат, довжина, відсутність пробілів) до відправки запиту.
Повна підтримка української (за замовчуванням) та англійської мов — перемикач у шапці сайту. Реалізовано на i18next + react-i18next:
- 8 неймспейсів перекладу:
common,lab1–lab6,cocomo(спільні блоки ЛР №5/6) — 16 JSON-файлів (src/i18n/locales/{uk,en}/*.json). - Мова визначається автоматично (
i18next-browser-languagedetector) і зберігається вlocalStorage. - Обчислювальні модулі мовонезалежні. Наприклад, рівні складності FPA (
ilfComplexity/eqComplexityу ЛР №4) повертають нейтральні значення"low" | "medium" | "high", а не рядки конкретною мовою — переклад відбувається лише в UI-шарі. Це свідоме архітектурне рішення: логіка розрахунків ніколи не повинна знати, якою мовою говорить користувач. - Промпти до AI-моделі в ЛР №4 також перекладаються разом з інтерфейсом.
111 автотестів (Vitest + Testing Library), усі проходять чисто:
npm run test
Test Files 20 passed (20)
Tests 111 passed (111)
Розподіл:
- Чисті функції (
lib/calculations/*,lib/validation/*,lib/format.ts) — формули COCOMO, IFPUG-таблиці складності, парсинг JSON з відповіді AI, Zod-схеми. - Хуки (
useValidatedNumber,useStaffAllocation,useCocomoDrivers,useVisionAnalysis) — черезrenderHook, включно з edge-кейсами (порожнє поле, вихід за межі діапазону, race condition при зміні зображення в ЛР №4). - Компоненти (
NumberField,Button,Alert,SelectField,LabCard,ErrorBoundary) — рендер, доступність (role,aria-*), взаємодія користувача черезuser-event. - Smoke-тест повного user-flow (
Lab1.test.tsx) — рендер реальної форми, введення значення, перевірка, що результат перерахувався, і що некоректне значення показує помилку, а не ламає сторінку.
- Content-Security-Policy (мета-тег в
index.html):script-src 'self'(без CDN, без inline-скриптів),connect-srcобмеженоself+openrouter.ai+ Sentry ingest-домени,object-src 'none'.frame-ancestorsнавмисно не заданий у мета-тегу — браузери тихо ігнорують цю директиву, якщо CSP доставлено через<meta>(працює лише через справжній HTTP-заголовок); тримати її там — це лише помилка в консолі без жодного реального захисту. Для захисту від clickjacking потрібен хостинг з підтримкою кастомних заголовків (GitHub Pages — ні; Cloudflare Pages/Netlify — так). - Referrer-Policy: strict-origin-when-cross-origin.
- Таймаут 30с з
AbortControllerна кожному запиті до OpenRouter API — зависла відповідь не блокує інтерфейс назавжди. - Валідація завантажених зображень: дозволені лише PNG/JPEG/WEBP/GIF, ліміт 8 МБ — до читання файлу в пам'ять браузера.
- API-ключ ніде не зберігається (ні
localStorage, ні бекенд — бекенда немає) і надсилається лише напряму з браузера користувача доopenrouter.ai; про це прямо попереджено у формі вводу ключа. rel="noopener noreferrer"на всіх зовнішніх посиланнях.npm audit: 0 вразливостей. Застосунок на React 19 і уніфікованомуreact-router@8(замістьreact-router-dom@7), що закриває GHSA-qwww-vcr4-c8h2. Ланцюжокbrace-expansion/minimatchчерез ESLint закрито черезoverrides(minimatch@^10.2.5) уpackage.json, без мажорного апгрейду ESLint. Vite навмисно лишено на 7-й гілці (стабільна), а не experimental Vite 8/rolldown.- Моніторинг помилок:
ErrorBoundary(на рівні застосунку й на рівні кожної лаби) реально відправляє помилки в Sentry, якщо заданийVITE_SENTRY_DSN; без DSN — безпечний no-op, і сам SDK Sentry довантажується лише за потреби (await import(...)), тобто 0 байт у бандлі, коли моніторинг не налаштований.
Lab-system/
├── public/
│ ├── manifest.json
│ └── vite.svg
├── scripts/
│ └── copy-404.mjs
├── src/
│ ├── components/
│ │ ├── cocomo/
│ │ │ ├── CostDriverGrid.tsx
│ │ │ ├── FinalMetricsPanel.tsx
│ │ │ ├── ScaleFactorGrid.tsx
│ │ │ └── StaffAllocationTable.tsx
│ │ ├── labs/
│ │ │ ├── BackButton.tsx
│ │ │ ├── LabCard.test.tsx
│ │ │ ├── LabCard.tsx
│ │ │ ├── LabGrid.tsx
│ │ │ └── LabHeader.tsx
│ │ ├── layout/
│ │ │ ├── AppShell.tsx
│ │ │ ├── ErrorBoundary.test.tsx
│ │ │ ├── ErrorBoundary.tsx
│ │ │ └── LanguageSwitcher.tsx
│ │ └── ui/
│ │ ├── Alert.test.tsx
│ │ ├── Alert.tsx
│ │ ├── Button.test.tsx
│ │ ├── Button.tsx
│ │ ├── FormulaBlock.tsx
│ │ ├── NumberField.test.tsx
│ │ ├── NumberField.tsx
│ │ ├── ResultStat.tsx
│ │ ├── Section.tsx
│ │ ├── SelectField.test.tsx
│ │ ├── SelectField.tsx
│ │ └── StepIndicator.tsx
│ ├── constants/
│ │ ├── cocomo.ts
│ │ └── labs.ts
│ ├── features/
│ │ ├── lab1/
│ │ │ ├── components/
│ │ │ │ ├── ClassifiersStep.tsx
│ │ │ │ ├── CostFactorAndResult.tsx
│ │ │ │ ├── RatingsGrid.tsx
│ │ │ │ ├── RatingsStep.tsx
│ │ │ │ └── SizeSteps.tsx
│ │ │ ├── Lab1.test.tsx
│ │ │ └── Lab1.tsx
│ │ ├── lab2/
│ │ │ ├── components/
│ │ │ │ ├── CatalogStep.tsx
│ │ │ │ ├── CorrectionFactorsStep.tsx
│ │ │ │ ├── FunctionCatalogList.tsx
│ │ │ │ ├── NormativeLaborStep.tsx
│ │ │ │ ├── ResultSummary.tsx
│ │ │ │ └── StaffCountStep.tsx
│ │ │ └── Lab2.tsx
│ │ ├── lab3/
│ │ │ ├── components/
│ │ │ │ ├── CostDriversStep.tsx
│ │ │ │ ├── ModeAndSizeStep.tsx
│ │ │ │ ├── ModeExplanation.tsx
│ │ │ │ └── ResultsSection.tsx
│ │ │ └── Lab3.tsx
│ │ ├── lab4/
│ │ │ ├── components/
│ │ │ │ ├── ApiKeySetup.tsx
│ │ │ │ ├── ComplexityAnalysisStep.tsx
│ │ │ │ └── SummaryStep.tsx
│ │ │ └── Lab4.tsx
│ │ ├── lab5/
│ │ │ └── Lab5.tsx
│ │ └── lab6/
│ │ ├── components/
│ │ │ ├── EffortDurationSteps.tsx
│ │ │ ├── ModificationAndEslocStep.tsx
│ │ │ ├── ProjectSummaryFooter.tsx
│ │ │ ├── VolumeStep.tsx
│ │ │ └── VolumeSummary.tsx
│ │ └── Lab6.tsx
│ ├── hooks/
│ │ ├── useCocomoDrivers.test.ts
│ │ ├── useCocomoDrivers.ts
│ │ ├── useStaffAllocation.test.ts
│ │ ├── useStaffAllocation.ts
│ │ ├── useValidatedNumber.test.tsx
│ │ ├── useValidatedNumber.ts
│ │ ├── useVisionAnalysis.test.tsx
│ │ └── useVisionAnalysis.ts
│ ├── i18n/
│ │ ├── locales/
│ │ │ ├── en/
│ │ │ │ ├── cocomo.json
│ │ │ │ ├── common.json
│ │ │ │ ├── lab1.json
│ │ │ │ ├── lab2.json
│ │ │ │ ├── lab3.json
│ │ │ │ ├── lab4.json
│ │ │ │ ├── lab5.json
│ │ │ │ └── lab6.json
│ │ │ └── uk/
│ │ │ ├── cocomo.json
│ │ │ ├── common.json
│ │ │ ├── lab1.json
│ │ │ ├── lab2.json
│ │ │ ├── lab3.json
│ │ │ ├── lab4.json
│ │ │ ├── lab5.json
│ │ │ └── lab6.json
│ │ └── index.ts
│ ├── lib/
│ │ ├── calculations/
│ │ │ ├── cocomoII.test.ts
│ │ │ ├── cocomoII.ts
│ │ │ ├── lab1.test.ts
│ │ │ ├── lab1.ts
│ │ │ ├── lab2.test.ts
│ │ │ ├── lab2.ts
│ │ │ ├── lab3.test.ts
│ │ │ ├── lab3.ts
│ │ │ ├── lab4.test.ts
│ │ │ ├── lab4.ts
│ │ │ ├── lab5.test.ts
│ │ │ ├── lab5.ts
│ │ │ ├── lab6.test.ts
│ │ │ └── lab6.ts
│ │ ├── services/
│ │ │ └── openRouterVision.ts
│ │ ├── validation/
│ │ │ ├── common.test.ts
│ │ │ ├── common.ts
│ │ │ └── schemas.ts
│ │ ├── config.ts
│ │ ├── format.test.ts
│ │ ├── format.ts
│ │ └── monitoring.ts
│ ├── pages/
│ │ ├── HomePage.tsx
│ │ ├── LabPage.tsx
│ │ └── NotFoundPage.tsx
│ ├── test/
│ │ └── setup.ts
│ ├── App.tsx
│ ├── index.css
│ ├── main.tsx
│ └── vite-env.d.ts
├── .env.example
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── eslint.config.js
├── index.html
├── package.json
├── postcss.config.js
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── vitest.config.ts
(node_modules/, dist/, coverage/, package-lock.json не показані — стандартні, генеровані)
| Шлях | Призначення |
|---|---|
src/main.tsx |
Точка входу: ініціалізує i18n і моніторинг помилок, монтує <App/> |
src/App.tsx |
Маршрутизація (React Router) + top-level ErrorBoundary |
src/constants/ |
Метадані лаб (slug/ікона/колір) і коефіцієнти COCOMO — мовонезалежні |
src/i18n/ |
Ініціалізація i18next + 16 JSON-файлів перекладу (uk/en × 8 неймспейсів) |
src/lib/calculations/ |
Чисті формули розрахунків + тести — серце застосунку |
src/lib/validation/ |
Zod-схеми валідації полів |
src/lib/services/ |
Клієнт OpenRouter API (AI-аналіз зображень) |
src/lib/config.ts |
Централізована конфігурація (URL, ліміти, таймаути) |
src/lib/monitoring.ts |
Обгортка над Sentry (lazy-loaded, safe no-op) |
src/hooks/ |
Повторно використовувана логіка стану форм |
src/components/ui/ |
Дрібні переюзабельні примітиви форм |
src/components/cocomo/ |
Спільні блоки для ЛР №5/6 |
src/components/layout/ |
Каркас застосунку, ErrorBoundary, перемикач мови |
src/components/labs/ |
Картки й навігація списку лаб |
src/features/labN/ |
Кожна лаба: оркестратор + власні components/*Step.tsx |
src/pages/ |
Маршрути (Home, Lab з код-сплітингом, NotFound) |
scripts/copy-404.mjs |
Postbuild-скрипт: SPA-фолбек для GitHub Pages |
Лабораторна №4 містить покроковий AI-асистент для автоматичного аналізу зображень на базі OpenRouter API.
-
Крок 0 — API ключ
- Отримай ключ на openrouter.ai/settings/keys
- Введи вручну або завантаж
.txtфайл з ключем - Натисни "Почати аналіз"
-
Крок 1 — ILF
- Завантаж рис.1 (таблиця або ER-діаграма «Студент»)
- Натисни "Аналізувати рис.1" — AI визначить DET і RET
- Перевір і за потреби скоригуй значення вручну
- Натисни "Далі → Крок 2"
-
Крок 2 — EQ
- Завантаж рис.2 (скріншот діалогового вікна)
- Натисни "Аналізувати рис.2" — AI визначить DET і FTR
- Перевір значення
- Натисни "Об'єднати розрахунок"
-
Крок 3 — Підсумок
- Переглянь зведену таблицю ILF + EQ
- Введи VAF (фактор вирівнювання, 0.65–1.35)
- Отримай фінальний результат AFT
Підтримуються два способи введення ключа:
Варіант А — вручну:
- Перейди на openrouter.ai/settings/keys
- Зареєструйся (безкоштовно, без картки)
- Натисни "Create Key"
- Скопіюй ключ (
sk-or-v1-...) і встав у поле в програмі
Варіант Б — з файлу:
- Збережи ключ у текстовий файл
key.txt(лише один рядок з ключем) - Натисни "Вибрати файл з ключем" і обери файл
- Ключ зчитається автоматично
⚠️ Ключ не зберігається між сесіями — після перезавантаження сторінки потрібно ввести знову.
Якщо одна модель недоступна — програма автоматично переходить до наступної:
| # | Модель | Провайдер |
|---|---|---|
| 1 | nvidia/nemotron-nano-12b-v2-vl:free |
NVIDIA |
| 2 | mistralai/mistral-small-3.1-24b-instruct:free |
Mistral AI |
| 3 | google/gemma-3-27b-it:free |
|
| 4 | google/gemma-3-4b-it:free |
|
| 5 | google/gemma-3-12b-it:free |
Всі моделі безкоштовні та підтримують аналіз зображень (vision).
Primary (Синій): #0057B7
Secondary (Жовтий): #FFD700
AI-секція: purple-900/30
Фон: slate-900 → blue-900 (градієнт)Жодна змінна середовища не є обов'язковою — застосунок повністю працює "з коробки". Дивись .env.example:
# VITE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0npm run build # створює dist/, включно з dist/404.html
npm run deploy # публікує dist/ через gh-pagesMIT License — LICENSE
Кручкевич Богдан Вікторович, GitHub: github.com/VSCRM