Перейти до вмісту

Проби та вмілості

Модуль відстежує складання проб і здобуття вмілостей членами куреня. Каталог (вимоги проб, перелік вмілостей, зображення) не зберігається в базі: його віддає сервіс каталогу (https://catalog.lileyka.app, опис API — /docs).

Секція 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}, беручи їх із каталогу.

Які вмілості курінь може почати, вирішує профіль його краю: 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 сервіс каталогу не віддає взагалі, тож коли стару редакцію виведуть, разом із нею з картки зникнуть і закриті на ній проби. До того треба навчити Лілейку показувати поступ на пробі, якої в каталозі вже немає.

  • ProbesCatalogController — каталог проб;
  • BadgesCatalogController — каталог вмілостей;
  • KurinCatalogController — вмілості, які пропонує курінь (див. вище);
  • MemberProgressController — проби й вмілості конкретного члена;
  • черга розгляду вмілостей куреня — KurinController.GetBadgeReviewQueue.
  • ProbeProgress — проба члена; ProbePointProgress — кожна точка проби з підписом того, хто її прийняв;
  • BadgeProgress — вмілість;
  • ProbeProgressAuditEvent / BadgeProgressAuditEvent — журнал змін.

Права: підписати точку і розглянути вмілість може впорядник у своїх гуртках (OwnGroups) або Звʼязковий у всьому курені (KurinWide) — гранти на ProbeProgress і BadgeProgress у RolePermissionMap мають лише вони й адміністратор. Провід куреня (курінний, писар, скарбник) цих грантів не має: усі записи MemberProgressController захищені ResourceAuthorize по учаснику.

Екрани в kurinModule: member-probe-page (проба з точками), skills-review-page (черга на розгляд), картка члена (member-card) із вкладками проби й вмілостей.

Кнопка «Деталі» стоїть зліва від кнопок дій біля кожної вмілості: у діалогах «Усі вмілості» й «Додати вмілість» та в черзі розгляду. Вона відкриває badge-details-dialog — картку вмілості, як на сайті каталогу: край, рівень, улад, спеціалізація, вимоги по пунктах, вимоги до інструктора, автор і дата оновлення. Картка бере вмілість за badgeId із кешованого BadgesCatalogService.getById. Позначки **…** з каталогу показуються жирним без innerHTML. Поля requirements, author і ulady API додає до вмілості окремо; seekerRequirements лишається тим самим суцільним текстом для старих клієнтів.