Проби та вмілості
Модуль відстежує складання проб і здобуття вмілостей членами куреня. Каталог (вимоги проб,
перелік вмілостей, зображення) не зберігається в базі: його віддає сервіс каталогу
(https://catalog.lileyka.app, опис API — /docs).
Джерело каталогу
Section titled “Джерело каталогу”Секція Catalog в appsettings:
BaseUrl— адреса сервісу каталогу;Krai— лише записи цього краю (?krai=); порожнє — увесь каталог (як це поєднується з профілем краю — у розділі «Вмілості куреня»);RefreshMinutes— як часто питати сервіс знову (незмінений каталог коштує відповідь 304);TimeoutSeconds— тайм-аут одного запиту;SnapshotPath— копія останньої вдалої відповіді на диску (відносний шлях — від теки застосунку).
CatalogSyncBackgroundService під час старту читає копію з диска; якщо її немає, чекає першої
відповіді сервісу, щоб API не відкрився з порожнім каталогом. Якщо й сервіс не відповів, API бере
копію, вбудовану в збірку (ProjectK.Infrastructure/Services/Catalog/Seed/catalog.json). Ця копія
ніколи не пишеться на диск. Невдалий запит лишає те, що вже завантажено, а відповідь без жодної
вмілості й проби не замінює наявний каталог.
Поки сервіс не відповів жодного разу (а копії з диска немає), API питає знову частіше: за хвилину,
далі вдвічі довше після кожної невдачі, але не рідше за RefreshMinutes. Після першої вдалої
відповіді — звичайний інтервал.
Вбудовану копію оновлюй перед релізом: bash scripts/update-catalog-seed.sh (або
pwsh scripts/update-catalog-seed.ps1). Скрипт перевіряє, що каталог говорить схемою 2.
Сервіс каталогу стоїть за Cloudflare. Bot Fight Mode відповідає запитам з адрес дата-центрів
(Azure, раннери GitHub) перевіркою, яку HttpClient не пройде, тож API лишається без каталогу.
Тому на обох зонах, lileyka.app і rostyslav-mukha.dev, він вимкнений: правилами WAF його не
обійти, пропускають повз нього лише IP Access Rules.
Зображення вмілостей — абсолютні адреси каталогу, браузер бере їх напряму. Зображення відзначень
API віддає за старою адресою /api/awards/images/{level}, беручи їх із каталогу.
Вмілості куреня
Section titled “Вмілості куреня”Які вмілості курінь може почати, вирішує профіль його краю: Catalog.BadgeSources — з яких
країв брати вмілості (за міткою краю в каталозі; без значення — лише власний край), і
Catalog.HiddenBadges — окремі вмілості, які край прибирає. Вмілість без мітки краю не
пропонується нікому. Проби профіль краю не фільтрує — їх розставляє черга проб (нижче).
Каталог мітить вмілості лише як ua чи pl. Тому засіяні краї мають BadgeSources у профілі:
Польща читає pl, решта — ua. Без цього край зі своєю міткою (скажімо, de) не побачив би жодної
вмілості. Україна й Чехія засіяні з порожнім профілем, тож Чехія так само лишається без вмілостей,
доки адміністратор не задасть їй джерело.
Два фільтри стоять один за одним. Catalog:Krai в appsettings звужує те, що інстанс узагалі
завантажує з сервісу; профіль краю вибирає вже з завантаженого. Тож інстанс із Catalog:Krai=ua не
покаже польському краю польських вмілостей, хоч би що стояло в його BadgeSources — для кількох
країв Catalog:Krai лишають порожнім.
IKurinCatalog (ProbesAndBadgesModule/Services/KurinCatalog.cs) збирає цей перелік. Його читає
KurinCatalogController (GET api/kurin/{kurinKey}/catalog/badges, Kurin:Read; з нього картка
члена бере, які вмілості можна почати) і SubmitBadgeProgressCommand: новий початок вмілості, якої край не пропонує,
відхиляється з 400 BadgeNotOffered. Вмілість, уже взята в роботу, лишається за людиною, навіть
якщо профіль її більше не пропонує, а все, що людина вже має, читається з повного каталогу
(api/catalog/badges).
Редакції програми й черга проб
Section titled “Редакції програми й черга проб”Каталог може тримати кілька редакцій програми поруч: нова — це нові проби з новими id і спільним
edition (напр. upyu-2026), а старі лишаються, бо на них лежить поступ. order — місце проби в
її редакції (знак прихильника — 0), і проби відкриваються одна за одною в цьому порядку. Кожна
проба несе ще KraiCode і StatusCode (approved / trial); у api/catalog/probes це поля
edition, order, kraiCode, statusCode.
Назву редакції задає каталог: окремий запис у editions (id, title, ulad, krai), який
редагують в адмінці каталогу на вкладці «Редакції проб». CatalogMapper кладе її в
Probe.EditionTitle, а клієнти показують як є, нічого не виводячи з id. Звичайна проба (без
edition) запису в каталозі не має, і «Звичайна проба» — єдина назва, яку дає сама Лілейка
(REGULAR_PROBE_LABEL). Журнал дій записує назву, яку бачили на момент зміни, а не id: пізніше
перейменування в каталозі історію не переписує.
Редакцію обирає гурток: Group.ProbeEdition (null — редакція без назви, якою курені ходять від
початку). Налаштовується це в налаштуваннях куреня, не на сторінці гуртка (розділ
kurin-probe-editions, видно лише в юнацькому курені, коли пропонується більше однієї редакції).
GET api/kurin/{kurinKey}/probe-editions (Kurin:Update, як і решта налаштувань) віддає
редакції, які каталог пропонує краю куреня (іменована — лише коли всі її проби мають KraiCode
куреня), і для кожного гуртка його редакцію, canChange і кожного юнака з тим, що визначає його
редакцію. Редакція гуртка в цій відповіді — фактична: якщо обраної редакції в каталозі вже немає,
це null, як і в черзі проб. Перемикає PUT api/group/{groupKey}/probe-edition з { edition }. Перемикати може право куреня
ChooseProbeEdition (GroupProbeEdition:Update, за замовчуванням лише Звʼязковий); зміна йде в
Журнал дій як Settings.GroupProbeEditionChanged.
Яку пробу людина бачить на якому місці, вирішує ProbeLine (ProbesAndBadgesModule/Services):
- редакція людини — редакція її гуртка (
IGroupProbeEditions), якщо вона не закрила пробу, далі за перше місце (order ≥ 1, тобто першу чи пізнішу), в іншій редакції. Тоді вона дозакриває ту редакцію, і стара перша ніколи не веде в нову другу, де точки переставлено; - на місці, якого редакція людини не має (знак прихильника в старій), показується проба, де людина вже має поступ;
- розпочата, але не закрита проба іншої редакції на місці, яке редакція людини має, з черги зникає. Підписи лишаються в журналі, а повернення гуртка до попередньої редакції повертає й пробу.
IMemberProbeLine читає чергу людини: для клієнтів (GET api/member/{memberKey}/probes/line), для
дашборда (GetMyGrowthQuery) і для сторожі черговості. Підпис першої точки чи зміна статусу ще не
розпочатої проби відхиляються з 409: ProbeNotInLine — проба з іншої редакції;
ProbeWaitsForPrevious — попередня проба в черзі не закрита. Розпочата проба йде далі без перевірки.
IProbeEditionHolds каже модулю куреня, де стоїть кожен юнак, щоб налаштування показали, хто
лишиться на своїй редакції, а хто перейде, покинувши розпочату пробу. Це поступ у пробах, тому
маршрут, що його віддає, закритий так само, як решта налаштувань куреня.
Ні веб, ні мобільний не знають id проб і правила не повторюють: картка бере чергу з
probes/line. Лише коли маршруту немає (старий API, записане демо), вони беруть каталог і
запасну чергу (probeLine у member-probe-rows-view-mapper.function.ts і kurin.labels.ts) —
редакцію без назви. Наступна проба відкривається, коли закрито попередню або коли в ній уже є
поступ; проба без точок у каталозі (зараз третя) показується, але поточково не відстежується й не
закривається.
Пастка на 2028 рік: проби зі статусом retired сервіс каталогу не віддає взагалі, тож коли стару
редакцію виведуть, разом із нею з картки зникнуть і закриті на ній проби. До того треба навчити
Лілейку показувати поступ на пробі, якої в каталозі вже немає.
Контролери
Section titled “Контролери”ProbesCatalogController— каталог проб;BadgesCatalogController— каталог вмілостей;KurinCatalogController— вмілості, які пропонує курінь (див. вище);MemberProgressController— проби й вмілості конкретного члена;- черга розгляду вмілостей куреня —
KurinController.GetBadgeReviewQueue.
Сутності
Section titled “Сутності”- ProbeProgress — проба члена; ProbePointProgress — кожна точка проби з підписом того, хто її прийняв;
- BadgeProgress — вмілість;
- ProbeProgressAuditEvent / BadgeProgressAuditEvent — журнал змін.
Права: підписати точку і розглянути вмілість може впорядник у своїх гуртках (OwnGroups) або
Звʼязковий у всьому курені (KurinWide) — гранти на ProbeProgress і BadgeProgress у
RolePermissionMap мають лише вони й адміністратор. Провід куреня (курінний, писар, скарбник) цих
грантів не має: усі записи MemberProgressController захищені ResourceAuthorize по учаснику.
Фронтенд
Section titled “Фронтенд”Екрани в kurinModule: member-probe-page (проба з точками), skills-review-page (черга на
розгляд), картка члена (member-card) із вкладками проби й вмілостей.
Кнопка «Деталі» стоїть зліва від кнопок дій біля кожної вмілості: у діалогах «Усі вмілості» й
«Додати вмілість» та в черзі розгляду. Вона відкриває badge-details-dialog — картку вмілості, як на
сайті каталогу: край, рівень, улад, спеціалізація, вимоги по пунктах, вимоги до інструктора, автор і
дата оновлення. Картка бере вмілість за badgeId із кешованого BadgesCatalogService.getById.
Позначки **…** з каталогу показуються жирним без innerHTML. Поля requirements, author і
ulady API додає до вмілості окремо; seekerRequirements лишається тим самим суцільним текстом
для старих клієнтів.