Решта адмінських сценаріїв
«Свій адмінський клієнт» доводить до керування чергою й завантаження треку. Тут — усе інше, що вміє панель: редактор пісень, тексти, налаштування, групи, журнал аудиту та облікові записи.
Передбачається, що сокет уже підвищено і ви знаєте свій набір привілеїв із admin_confirmed.
Спершу спитайте, що доступно
Привілеї кажуть, чи має право цей адмін. Це не те саме, що чи вміє це сервер: та сама панель на іншій конфігурації матиме менше можливостей, і жоден привілей цього не змінить.
const { capabilities } = await fetch(`${SERVER}/api/public/config`).then(r => r.json());
// { uploadTracks, editTrackMetadata, moveTrackMode, deleteTracks, artistArts,
// helperAdmins, adminAccount, ipModeration, editSettings, auditLog, history,
// jingles, backgroundMusic, phrases }Кнопку варто показувати лише коли виконуються обидві умови:
const canShowEditor = privileges.includes('editor_meta') && capabilities.editTrackMetadata;Можливість буває вимкнена з двох різних причин. Або конфігурації бракує чогось технічного — джингли без STREAM_MODE вставляти нікуди. Або сховище не переживає перезапуск: на хостингу з EPHEMERAL_STORAGE=true і локальними файлами завантаження спрацювало б, а потім зникло при наступному деплої, тож сервер його не пропонує. Подробиці — у «Ефемерний хостинг».
Це не лише про інтерфейс
Роути перевіряють те саме незалежно від клієнта. Прихована кнопка нічого не захищає — недоступна можливість дає 400 з поясненням у error. Тож capabilities — це спосіб не показувати те, що однаково не спрацює, а не механізм захисту.
Редактор пісень
Договір ендпоінта коротко: POST /api/admin/song-editor/save приймає metadata, lyricsEntry і offset разом із трьома прапорцями (metadataChanged, lyricsChanged, offsetChanged) — сервер перевіряє привілей саме за тим, що справді змінюється (editor_meta для метаданих, editor_lyrics або editor_meta для тексту), а зайнятий ефіром трек (поточний або наступний) відхиляється 409 з поясненням у error.localized.
Повна реалізація — з тим, як саме клієнт вирішує, що з трьох речей змінилось, як поводиться з форматом таймкодів і що показує на 409 — описана окремо: «Адмінський редактор пісень».
Тексти й таймкоди
POST /api/admin/upload-song-lyrics шукає текст за title, artist, необов'язковими album і duration. Доступ: upload_songs або editor_lyrics; editor_meta окремо не дозволяє пошук. Відповідь містить lyricsEntry та lyricsStatus (none, plain, synced). Пошук сам по собі текст не зберігає. Групове оновлення використовує цей пошук і POST /api/admin/song-editor/save.
Тексти живуть окремо від треків і ключуються парою виконавець + назва, а не ідентифікатором треку. Через це перейменування виконавця в метаданих переносить і текст.
| Що потрібно | Ендпоінт |
|---|---|
| Легкий перелік, для чого текст уже є | GET /api/admin/lyrics/cache-index |
| Текст одного треку | GET /api/admin/lyrics/cache-entry |
| Зберегти відредагований | PUT /api/admin/lyrics/cache |
| Забути (щоб перезапитати) | DELETE /api/admin/lyrics/cache |
| Усі зсуви | GET /api/admin/lyrics/offsets |
GET /api/admin/lyrics/cache-full віддає геть усе одним шматком — на великій фонотеці це важка відповідь, тож для списку беріть cache-index.
Для звірки таймкодів є GET /api/admin/lyrics/audio-preview: повертає посилання, яким можна годувати <audio src> напряму. У хмарі це тимчасове посилання на бакет, локально — посилання на адмінський стрім-роут із токеном у запиті. Клієнту різниця не видна.
Зсув зберігається окремо, і в його шляху немає /admin, хоча адмінський токен потрібен:
await post('/api/lyrics/offset', { title, artist, offset: -0.4 });Зсув у секундах і може бути від'ємним.
Налаштування
GET /api/admin/settings доступний будь-якому автентифікованому адміну. POST — теж, але авторизація йде посекційно, за тим, що реально відрізняється від збереженого:
| Секція | Привілей |
|---|---|
branding | settings_branding |
generation | settings_algorithm |
radioHosts | radio_moderator |
Панель завжди шле всі секції цілком, тому перевірка не може спиратися на те, що надіслано — інакше кожен адмін мусив би мати всі три привілеї. Порівнюється саме зміст.
Поле songGroups у цьому тілі ігнорується: групи мають власні ендпоінти.
Якщо capabilities.editSettings вимкнено, GET усе одно працює. Це навмисно: бачити, що радіо робить зараз, корисно й тоді, коли змінити нічого не можна — референсна панель у такому разі показує форму лише для читання.
Групи пісень
Група — це іменований набір треків одного режиму, який можна поставити в чергу одним рухом.
await post('/api/admin/song-groups', {
name: 'Ранковий блок',
mode: 'day',
songs: ['day/Artist - Title.mp3', '…'],
});Читати групи може будь-який адмін; змінювати — з settings_groups. Для пошуку треків при наповненні є GET /api/admin/song-groups/library з посторінковістю.
Постановка групи в чергу потребує обох привілеїв — queue_manage і settings_groups, — бо дія торкається і ефіру, і визначення груп. Одного не досить, і подія admin_insert_song_group робить те саме з тими самими вимогами, тож правило не обійти через сокет.
Журнал аудиту
Формат розгорнутих змін «Було / Стало», перелік операцій і доповнення контрактів API описані в детальному журналі адміністрування.
const { entries, total } = await get('/api/admin/audit?window=24h&offset=0&limit=50');Вікна: 1h, 6h, 24h, 7d. Журнал читається з кеша в пам'яті, тож посторінковість дешева.
Записи створюються сервером самостійно — окремого «запиши в аудит» ендпоінта немає й бути не може. Кожен запис має operationType, adminLogin і data; склад data залежить від типу операції.
Розділ вимикається цілком, коли сховище не переживає перезапуск: журнал писався б у те, що зникне при наступному деплої, і показував би оманливу частину того, що насправді відбувалося.
Історія програвань
const { entries, total } = await get('/api/admin/history');На відміну від журналу аудиту, тут потрібен привілей stats — окремого під історію не заводили, це той самий, що й для статистики. Пагінації немає: обсяг і так обмежений тим самим LOG_RETENTION_DAYS, що й аудит.
Є і публічний варіант без жодної авторизації — останні 10 записів, ним користується і сам клієнт радіо, і сторонні застосунки:
const recent = await fetch(`${SERVER}/api/history`).then(r => r.json());Вимикається за тим самим правилом, що й аудит: писати в те, що не переживе перезапуск, немає сенсу.
Облікові записи
Дві незалежні гілки з різними вимогами.
Помічники — лише супер-адмін
const { admins, allPrivileges } = await get('/api/admin/admins');
await post('/api/admin/admins', { login: 'helper', password: 'Temp1234', privileges: ['stats'] });Пароль тут тимчасовий. Обліковий запис створюється з authorized: false і не вміє нічого, доки помічник сам його не активує. Хеші паролів у відповідях не повертаються ніколи.
Зміна привілеїв застосовується без повторного входу: якщо помічник онлайн, він одразу отримує подію privileges_updated. Видалення надсилає йому force_logout з reason: "admin_deleted", і сесія завершується негайно.
Власний обліковий запис
await post('/api/admin/admins/self/activate', { tempPassword, newPassword });
await put('/api/admin/admins/self/login', { newLogin, currentPassword });
await put('/api/admin/admins/self/password', { currentPassword, newPassword });Активація — єдина дія, доступна помічнику з authorized: false. Після успіху приходить подія admin_authorized.
Супер-адмін тут виняток
Його логін і пароль задаються змінними оточення (ADMIN_LOGIN, ADMIN_PASS), тож гілка /self для нього відповідає 400. Змінити їх можна лише в конфігурації сервера.
Донати
Донат — це платне замовлення пісні, що оминає модерацію: чергу двигає лише сервер після підтвердженої оплати, ніколи клієнт напряму. Розділ вимикається цілком, коли capabilities.donations не вмикається — жодного активного провайдера не налаштовано, або сховище не переживає перезапуск, за тим самим принципом, що й журнал аудиту.
Читання налаштувань доступне будь-якому адміну, як і GET /api/admin/settings; запис, і перегляд історії — лише з привілеєм donations_manage:
const { settings, provider, historyCurrencies } = await get('/api/admin/donations/settings');
await post('/api/admin/donations/settings', {
...settings,
tiersEnabled: true,
tierCeiling: 5,
});Два типи провайдерів, дві поведінки клієнта
provider (з тієї самої відповіді, і з donationInfo у /api/public/config) каже, чого чекати від POST /api/public/donations/create — гілкуйтеся за flowType, а не за конкретним provider.id, бо реалізацій буде більше:
flowType | Провайдери | Що робити з відповіддю |
|---|---|---|
checkout | LiqPay, Stripe | одразу location.href = redirectUrl |
matching | Donatello, Ko-fi | показати pageUrl і matchCode, чекати на donation_result або опитувати /donations/:id/status |
matching існує, бо ці два сервіси не мають API «створити платіж на суму X»: донатер сам платить на своїй сторінці й сам вписує код у коментар. Точної суми ніхто не форсує, тож донат приймається за принципом «не менше очікуваного», а не «рівно стільки».
Черги — це не окремий вибір, а множник
Коли tiersEnabled, кожна наступна черга коштує вдвічі дорожче за попередню, а ціна першої — це fixedPrice або pricePerSecond × тривалість, залежно від pricingMode. tierCeiling — стеля кількості черг (2-10), і сервер сам знижує її при збереженні, якщо найдорожча черга перевищує ліміт транзакції провайдера — про це скаже clamped: true у відповіді на POST.
Історія
const { entries, total } = await get('/api/admin/donations/history?window=7d&limit=50');Вікна ті самі за формою, що й у журналі аудиту (24h, 7d), плюс 30d і max — останнє обмежене DONATION_RETENTION_DAYS (типово 365 днів, а не LOG_RETENTION_DAYS: донати зберігаються довше за журнал дій). Кожен запис має status: pending, paid, paid_unqueued, failed або expired — paid_unqueued означає, що оплату прийнято, але вставити пісню в чергу не вдалося, і це вимагає ручного розбору.
Донат і пауза черги (RADIO_HOSTS_MODE) взаємно виключні
Поки в черзі є хоч одна донатна пісня, host_pause_queue відхиляється — ведучий побачить host_queue_pause_state з denied: true. І навпаки: якщо черга вже на паузі, а прапорець blockDonationsWhileChatting вимкнено (типово), успішний донат сам знімає паузу, щоб оплачена пісня не чекала «Just Chatting» без кінця.
Що далі
- Повна конфігурація — від чого залежить кожна можливість
- Привілеї — повний перелік і що саме кожен відкриває
- REST API — точні тіла запитів, відповідей і кодів помилок