feat: Настройки › Автообновление - clients compare their version with the server's every N and update themselves; the server looks at GitLab every N and, when a newer release is out, offers it to an administrator at login (after the PIN) with «Обновить» and «Отказаться»; the words of a made picture's result are the model's only
This commit is contained in:
1 parent
916a49958b
commit
3d2f886f04
22 files changed
+1074
-7
No files matched your search
@@ -721,12 +721,15 @@
|
||||
// the next question (or by note, when done by hand).
|
||||
function showResult(text) {
|
||||
const el = add('result', '');
|
||||
el.innerHTML = '<details><summary>Результат</summary><pre></pre></details>';
|
||||
el.querySelector('pre').textContent = text;
|
||||
// A picture made: shown here, from its marker (kept with the talk, so
|
||||
// it comes back with it) - and what the next edit works on.
|
||||
// it comes back with it) - and what the next edit works on. Its words
|
||||
// are the model's alone: the user sees the picture.
|
||||
const m = /\[картинка: (https?:\/\/[^\]\s]+)\]/.exec(text);
|
||||
if (!m) return;
|
||||
if (!m) {
|
||||
el.innerHTML = '<details><summary>Результат</summary><pre></pre></details>';
|
||||
el.querySelector('pre').textContent = text;
|
||||
return;
|
||||
}
|
||||
lastPic = m[1];
|
||||
const follow = nearBottom();
|
||||
const link = document.createElement('a'), img = document.createElement('img');
|
||||
@@ -736,7 +739,7 @@
|
||||
img.onerror = () => { link.classList.add('gone'); link.textContent = 'Картинка больше не открывается'; };
|
||||
link.appendChild(img);
|
||||
el.classList.add('pic');
|
||||
el.insertBefore(link, el.firstChild);
|
||||
el.appendChild(link);
|
||||
}
|
||||
// makeImage: a picture asked for in an answer - put at the image service
|
||||
// through the server, asked about until it is done. What comes back is
|
||||
|
||||
@@ -963,7 +963,7 @@ dialog.uidlg { width:fit-content; min-width:min(300px, 92vw); max-width:min(560p
|
||||
.ai-pic { display:block; overflow:hidden; border:1px solid var(--border); border-radius:12px; background:#0c0f13; line-height:0; }
|
||||
.ai-pic img { display:block; width:100%; height:auto; max-height:440px; object-fit:contain; background:#0c0f13; }
|
||||
.ai-pic.gone { padding:18px 12px; line-height:1.4; text-align:center; color:var(--muted); font-size:12.5px; }
|
||||
.ai-msg.result.pic details { padding:0 4px; }
|
||||
|
||||
.ai-msg.result pre { margin:6px 0 2px; max-height:220px; overflow:auto; white-space:pre-wrap; font-size:12px; }
|
||||
.ai-cap { font-size:12px; margin-bottom:4px; }
|
||||
/* Modes as pills: on - lit in the accent, off - a quiet outline. */
|
||||
@@ -1156,6 +1156,13 @@ body.no-motion *, body.no-motion *::before, body.no-motion *::after, body.no-mot
|
||||
.ai-model-sub { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--muted); font-size:12.5px; }
|
||||
.ai-model-sub:not(:empty)::before { content:'· '; }
|
||||
.ai-model-body { display:grid; gap:10px; min-width:0; padding:0 12px 12px; }
|
||||
.ai-form label.check { display:inline-flex; align-items:center; gap:8px; margin:0; color:var(--text); font-size:14px; cursor:pointer; }
|
||||
.ai-form label.check input { margin:0; }
|
||||
.auto-every { display:flex; align-items:center; gap:8px; }
|
||||
.auto-every input[type=number] { width:84px; flex:none; }
|
||||
.auto-every select { width:auto; flex:none; }
|
||||
.auto-status { font-size:13px; line-height:1.5; }
|
||||
.auto-status .bad { color:var(--danger); }
|
||||
.ai-model-head { display:flex; align-items:flex-end; gap:8px; margin:2px 0 4px; }
|
||||
.ai-model-head .ai-model-name { flex:1; min-width:0; margin:0; }
|
||||
.ai-model-head .ai-model-short { flex:none; width:96px; margin:0; }
|
||||
|
||||
@@ -23,6 +23,30 @@
|
||||
}, e => alert('Не скопировалось: ' + e.message));
|
||||
});
|
||||
if (window.UsersTable) window.UsersTable.mount(root);
|
||||
// Автообновление: look at GitLab now, and update now.
|
||||
const au = root.querySelector('#autoupdate .auto-check');
|
||||
if (au) {
|
||||
const st = root.querySelector('#autoupdate .auto-status'), run = root.querySelector('#autoupdate .auto-run');
|
||||
const post = u => fetch(u, {method: 'POST', credentials: 'same-origin'}).then(r => r.json()).catch(() => ({ok: false, error: 'нет связи с сервером'}));
|
||||
const say = (text, bad) => { st.textContent = ''; const s = document.createElement('span'); s.textContent = text; if (bad) s.className = 'bad'; st.appendChild(s); };
|
||||
au.onclick = async () => {
|
||||
au.disabled = true; say('Проверяю…');
|
||||
const r = await post('/api/panel/update/check');
|
||||
au.disabled = false;
|
||||
if (!r.ok) { say(r.error, true); return; }
|
||||
say('Версия сервера: ' + r.current + ' · на GitLab: ' + r.latest + (r.newer ? ' - вышла новая' : ' - это последняя'));
|
||||
run.hidden = !r.newer;
|
||||
};
|
||||
run.onclick = async () => {
|
||||
if (!await UI.confirm('Обновить сервер сейчас? Панель будет недоступна около минуты.', {ok: 'Обновить'})) return;
|
||||
run.disabled = true; say('Запускаю обновление…');
|
||||
const r = await post('/api/panel/update/run');
|
||||
run.disabled = false;
|
||||
if (!r.ok) { say(r.error, true); return; }
|
||||
run.hidden = true;
|
||||
say('Обновление до ' + r.latest + ' запущено: сервер перезапустится в течение минуты - потом обновите страницу.');
|
||||
};
|
||||
}
|
||||
// ИИ-ассистент: a card per model, added from the blank one and taken
|
||||
// away by its «Удалить» (the last one stays).
|
||||
const list = root.querySelector('.ai-models'), blank = root.querySelector('template.ai-model-new');
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
input.value = o.value || '';
|
||||
yes.textContent = o.ok || 'OK';
|
||||
yes.className = 'ui-yes' + (o.danger ? ' btn danger' : '');
|
||||
dlg.querySelector('.ui-no').textContent = o.cancel || 'Отмена';
|
||||
// «Отмена» red, unless «да» is the red one (a delete): never two reds.
|
||||
dlg.querySelector('.ui-no').classList.toggle('close', !o.danger);
|
||||
return new Promise(done => {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// A new release of the server: an administrator is offered it when they come
|
||||
// in - after the PIN, or at once when there is none - once for each tab. The
|
||||
// window says the versions and has «Обновить» and «Отказаться»; a refusal is
|
||||
// kept for that release (a still newer one asks again). Only when the server
|
||||
// is set to look for releases (Настройки › Автообновление).
|
||||
(function () {
|
||||
'use strict';
|
||||
var root = document.documentElement;
|
||||
var SHOWN = 'lpra.upd.shown';
|
||||
|
||||
// The PIN lock hides the page until it is unlocked.
|
||||
function whenUnlocked(fn) {
|
||||
if (!root.classList.contains('pin-wait')) { fn(); return; }
|
||||
var mo = new MutationObserver(function () {
|
||||
if (!root.classList.contains('pin-wait')) { mo.disconnect(); fn(); }
|
||||
});
|
||||
mo.observe(root, {attributes: true, attributeFilter: ['class']});
|
||||
}
|
||||
function toast(text, bad) {
|
||||
if (window.FileViews && FileViews.toast) FileViews.toast(text, bad);
|
||||
}
|
||||
function post(url, body) {
|
||||
return fetch(url, {method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin',
|
||||
body: JSON.stringify(body || {})}).then(function (r) { return r.json(); })
|
||||
.catch(function () { return {ok: false, error: 'нет связи с сервером'}; });
|
||||
}
|
||||
|
||||
function offer() {
|
||||
try { if (sessionStorage.getItem(SHOWN) === '1') return; } catch (e) { /* offered again */ }
|
||||
fetch('/api/panel/update/offer', {cache: 'no-store', credentials: 'same-origin'})
|
||||
.then(function (r) { return r.ok ? r.json() : {}; })
|
||||
.then(function (d) {
|
||||
if (!d.offer || !window.UI) return;
|
||||
try { sessionStorage.setItem(SHOWN, '1'); } catch (e) { /* not remembered */ }
|
||||
UI.confirm('Вышла новая версия LPRA {chip}\nОбновить сервер сейчас? Панель будет недоступна около минуты.',
|
||||
{ok: 'Обновить', cancel: 'Отказаться', chip: d.current + ' → ' + d.latest}).then(function (yes) {
|
||||
if (!yes) { post('/api/panel/update/decline', {version: d.latest}); return; }
|
||||
post('/api/panel/update/run').then(function (r) {
|
||||
if (r.ok) toast('Обновление до ' + r.latest + ' запущено: сервер перезапустится в течение минуты - потом обновите страницу');
|
||||
else toast(r.error || 'обновить не получилось', true);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function () { /* no offer */ });
|
||||
}
|
||||
whenUnlocked(offer);
|
||||
})();
|
||||
@@ -213,6 +213,7 @@
|
||||
<script src="{{asset "account.js"}}"></script>
|
||||
<script src="{{asset "machine.js"}}"></script>
|
||||
<script src="{{asset "ai.js"}}"></script>
|
||||
<script src="{{asset "updatenote.js"}}"></script>
|
||||
<script>App.start();</script>
|
||||
</body>
|
||||
</html>{{end}}
|
||||
@@ -212,6 +212,36 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" id="autoupdate">
|
||||
<h2>Автообновление</h2>
|
||||
<form method="post" action="/settings/autoupdate" class="ai-form">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
{{/* The same spoilers as the assistant's. */}}
|
||||
<details class="ai-search"><summary class="ai-model-sum"><span class="ai-model-title">Автообновление клиентов</span><span class="ai-model-sub">{{if .Auto.AgentOn}}каждые {{.Auto.AgentEvery}} {{if eq .Auto.AgentUnit "d"}}сут.{{else if eq .Auto.AgentUnit "h"}}ч.{{else}}мин.{{end}}{{else}}выключено{{end}}</span></summary>
|
||||
<div class="ai-model-body">
|
||||
<p class="muted" style="margin:0">Клиенты раз в указанное время сверяют свою версию с версией сервера и, если у сервера новее, обновляются сами - так же, как по кнопке «Обновить». Нужны права root или sudo без пароля; агентам старше 0.2.5 один раз обновиться придётся вручную.</p>
|
||||
<label class="check"><input type="checkbox" name="agent_auto" value="1"{{if .Auto.AgentOn}} checked{{end}}> Включить автообновление клиентов</label>
|
||||
<div class="auto-every"><span class="muted">Проверять раз в</span>
|
||||
<input type="number" name="agent_every" min="1" max="999" value="{{.Auto.AgentEvery}}">
|
||||
<select name="agent_unit"><option value="min"{{if eq .Auto.AgentUnit "min"}} selected{{end}}>минут</option><option value="h"{{if eq .Auto.AgentUnit "h"}} selected{{end}}>часов</option><option value="d"{{if eq .Auto.AgentUnit "d"}} selected{{end}}>суток</option></select></div>
|
||||
</div></details>
|
||||
<details class="ai-search"><summary class="ai-model-sum"><span class="ai-model-title">Автообновление сервера</span><span class="ai-model-sub">{{if .Auto.ServerOn}}каждые {{.Auto.ServerEvery}} {{if eq .Auto.ServerUnit "d"}}сут.{{else if eq .Auto.ServerUnit "h"}}ч.{{else}}мин.{{end}}{{else}}выключено{{end}} · сейчас {{.Auto.Status.Current}}</span></summary>
|
||||
<div class="ai-model-body">
|
||||
<p class="muted" style="margin:0">Сервер раз в указанное время сверяет свою версию с последним релизом на GitLab. Если вышел новый, администратору при входе (после PIN) показывается окно: версия и кнопки «Обновить» и «Отказаться». «Обновить» запускает тот же скрипт, что и вручную: <code>wget -qO- https://gitlab.com/itzredtea/lpra/-/raw/main/server-install.sh | sudo sh</code> - от root, поэтому нужны root и systemd; панель при этом недоступна около минуты.</p>
|
||||
<label class="check"><input type="checkbox" name="server_auto" value="1"{{if .Auto.ServerOn}} checked{{end}}> Предлагать обновление сервера при входе</label>
|
||||
<div class="auto-every"><span class="muted">Проверять раз в</span>
|
||||
<input type="number" name="server_every" min="1" max="999" value="{{.Auto.ServerEvery}}">
|
||||
<select name="server_unit"><option value="min"{{if eq .Auto.ServerUnit "min"}} selected{{end}}>минут</option><option value="h"{{if eq .Auto.ServerUnit "h"}} selected{{end}}>часов</option><option value="d"{{if eq .Auto.ServerUnit "d"}} selected{{end}}>суток</option></select></div>
|
||||
<div class="auto-status muted" aria-live="polite">Версия сервера: <b>{{.Auto.Status.Current}}</b>{{if .Auto.Status.Latest}} · на GitLab: <b>{{.Auto.Status.Latest}}</b>{{end}}{{if .Auto.Status.Checked}} · проверено {{.Auto.Status.Checked}}{{end}}{{if .Auto.Status.Error}}<br><span class="bad">{{.Auto.Status.Error}}</span>{{end}}</div>
|
||||
<div class="row">
|
||||
<button type="button" class="btn ghost small auto-check">Проверить сейчас</button>
|
||||
<button type="button" class="btn small auto-run"{{if not .Auto.Status.Newer}} hidden{{end}}>Обновить сейчас</button>
|
||||
</div>
|
||||
</div></details>
|
||||
<div><button type="submit">Сохранить</button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Enroll-секрет</h2>
|
||||
<p class="muted">Общий секрет для регистрации новых агентов.</p>
|
||||
|
||||
Reference in new issue
Block a user