feat: long names in questions cut with … and the window as wide as its words; SSH dialog hides «Вход» for a saved host and says «подключение…» quietly; zip and unzip on SSH hosts without them through Python's zipfile

This commit is contained in:
redtea committed 2026-10-16 09:31:21 +03:00
1 parent 6db78399ac
commit 434900a168
5 files changed
+40 -20

No files matched your search

+14 -12
View File
@@ -573,7 +573,7 @@
'<div class="frow"><label class="f grow"><span>Хост</span><input type="text" name="host" placeholder="example.com или 10.0.0.5" required autocapitalize="off" spellcheck="false"></label>' +
'<label class="f port"><span>Порт</span><input type="number" name="port" min="1" max="65535" value="22" required></label></div>' +
'<label class="f"><span>Пользователь</span><input type="text" name="user" placeholder="root" required autocapitalize="off" spellcheck="false"></label>' +
'<div class="f"><span>Вход</span><div class="seg" role="tablist">' +
'<div class="f auth-row"><span>Вход</span><div class="seg" role="tablist">' +
'<button type="button" data-auth="password" class="on">Пароль</button><button type="button" data-auth="key">Ключ</button><button type="button" data-auth="agent">Ключи агента</button></div></div>' +
'<div class="auth" data-for="password"><input type="password" name="password" placeholder="пароль" autocomplete="off"></div>' +
'<div class="auth" data-for="key" hidden>' +
@@ -588,6 +588,8 @@
'</div></form>';
document.body.appendChild(d);
const f = d.querySelector('form'), msg = d.querySelector('.sshmsg');
// say: a line under the form - red for what went wrong, quiet for what goes on.
const say = (t, going) => { msg.textContent = t; msg.classList.toggle('going', !!going); };
let auth = 'password';
const setAuth = a => {
auth = a;
@@ -601,8 +603,8 @@
file.onchange = () => {
const fl = file.files[0];
if (!fl) return;
if (fl.size > 32768) { msg.textContent = 'файл великоват для ключа'; return; }
fl.text().then(t => { f.key.value = t.trim() + '\n'; d.querySelector('.keyname').textContent = fl.name; msg.textContent = ''; });
if (fl.size > 32768) { say('файл великоват для ключа'); return; }
fl.text().then(t => { f.key.value = t.trim() + '\n'; d.querySelector('.keyname').textContent = fl.name; say(''); });
file.value = '';
};
// then runs once the dialog is shut and focus is back where it was, so
@@ -618,7 +620,7 @@
const useSaved = () => {
const h = saved.find(x => String(x.id) === f.saved.value);
d.querySelector('[data-delsaved]').hidden = !h;
d.querySelectorAll('.seg, .auth, .save-row').forEach(el => { el.style.display = h ? 'none' : ''; });
d.querySelectorAll('.auth-row, .auth, .save-row').forEach(el => { el.style.display = h ? 'none' : ''; });
if (!h) { setAuth(auth); return; }
f.host.value = h.host; f.port.value = h.port; f.user.value = h.user;
};
@@ -642,15 +644,15 @@
f.onsubmit = async ev => {
ev.preventDefault();
const a = agents.find(x => x.id === f.agent.value);
if (!a) { msg.textContent = 'выберите машину'; return; }
if (!a) { say('выберите машину'); return; }
const t = { host: f.host.value.trim(), port: Number(f.port.value) || 22, user: f.user.value.trim() };
if (f.saved.value) {
t.saved = Number(f.saved.value);
} else if (auth === 'password') {
if (!f.password.value) { msg.textContent = 'введите пароль'; f.password.focus(); return; }
if (!f.password.value) { say('введите пароль'); f.password.focus(); return; }
t.password = f.password.value;
} else if (auth === 'key') {
if (!/PRIVATE KEY/.test(f.key.value)) { msg.textContent = 'вставьте закрытый ключ или загрузите файл'; return; }
if (!/PRIVATE KEY/.test(f.key.value)) { say('вставьте закрытый ключ или загрузите файл'); return; }
t.key = f.key.value;
if (f.passphrase.value) t.passphrase = f.passphrase.value;
}
@@ -658,7 +660,7 @@
// Saved first, then used by its id like any saved host.
const r = await fetch('/api/panel/ssh/hosts', { method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(Object.assign({ auth: auth }, t)) }).then(x => x.json()).catch(() => ({ ok: false, error: 'ошибка сети' }));
if (!r.ok) { msg.textContent = r.error || 'не сохранилось'; return; }
if (!r.ok) { say(r.error || 'не сохранилось'); return; }
f.save.checked = false;
loadSaved();
}
@@ -669,11 +671,11 @@
// The machine connects first; the pane opens once it is in.
const btn = f.querySelector('button[type=submit]');
btn.disabled = true;
msg.textContent = 'подключение…';
say('подключение…', true);
window.Workspace.sshFiles(a, t, sshInto).then(r => {
btn.disabled = false;
if (!r.ok) { msg.textContent = r.error || 'не подключилось'; return; }
msg.textContent = '';
if (!r.ok) { say(r.error || 'не подключилось'); return; }
say('');
close();
});
return;
@@ -685,7 +687,7 @@
sshMode = mode;
sshInto = into || null;
d.querySelector('.dlg-h strong').textContent = mode === 'files' ? 'Файлы по SSH' : 'SSH-подключение';
msg.textContent = '';
say('');
let last = {};
try { last = JSON.parse(localStorage.getItem('lpra.ssh.last') || '{}'); } catch (e) { /* none */ }
agents = here ? [here] : [];