#794: Telegram-Client-Anmeldung ueber die WebUI

Code und 2FA-Passwort wurden nur im Desktop-Dialog abgefragt; eine Instanz
ohne Fenster liess die Anmeldung aus. Jetzt haelt die Instanz die Frage
(TelegramLoginBroker), die Einstellungsseite beantwortet sie. Die Anmeldung
laeuft im Hintergrund, damit die HTTP-Flaeche, ueber die der Code kommt,
nicht auf ihn wartet. Eine offene Frage verfaellt nach 15 Minuten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-10-02 18:59:37 +02:00
co-authored by Claude Opus 5.5
parent 93395c9e01
commit 4e41fc12d7
9 changed files with 385 additions and 9 deletions
@@ -669,6 +669,16 @@
"logs.empty": "Noch keine Protokolleinträge vorhanden.",
"settings.title": "Einstellungen & Systemarchitektur",
"settings.telegramCard": "Telegram-Client",
"settings.telegramState.Connecting": "Verbindet",
"settings.telegramState.NeedsCode": "Code nötig",
"settings.telegramState.NeedsPassword": "Passwort nötig",
"settings.telegramState.Connected": "Angemeldet",
"settings.telegramState.Failed": "Nicht angemeldet",
"settings.telegramCode": "Anmeldecode von Telegram",
"settings.telegramPassword": "Passwort der zweistufigen Bestätigung",
"settings.telegramSend": "Senden",
"settings.telegramRetry": "Erneut anmelden",
"settings.license": "Lizenz",
"settings.licenseState": "Zustand",
"settings.licenseValid": "Gültig",
@@ -669,6 +669,16 @@
"logs.empty": "No log messages yet.",
"settings.title": "Settings & System Architecture",
"settings.telegramCard": "Telegram client",
"settings.telegramState.Connecting": "Connecting",
"settings.telegramState.NeedsCode": "Code needed",
"settings.telegramState.NeedsPassword": "Password needed",
"settings.telegramState.Connected": "Logged in",
"settings.telegramState.Failed": "Not logged in",
"settings.telegramCode": "Login code from Telegram",
"settings.telegramPassword": "Two-step verification password",
"settings.telegramSend": "Send",
"settings.telegramRetry": "Log in again",
"settings.license": "License",
"settings.licenseState": "State",
"settings.licenseValid": "Valid",
@@ -214,10 +214,11 @@ async function instanceSection() {
return el;
}
const [settings, status, info] = await Promise.all([
const [settings, status, info, telegram] = await Promise.all([
api("/settings").catch(() => null),
api("/status").catch(() => null),
api("/info").catch(() => null)
api("/info").catch(() => null),
api("/telegram-client").catch(() => null)
]);
// The instance file belongs to the instance process: it is only written while it runs.
@@ -230,6 +231,7 @@ async function instanceSection() {
const grid = erstelleElement("div", { klasse: "settings-grid" });
grid.append(instanceCard(settings), openRouterCard(settings, status), applicationCard(settings));
if (telegram && telegram.state !== "NotConfigured") grid.append(telegramCard(telegram));
if (info) grid.append(subsystemsCard(info));
el.append(grid);
@@ -332,3 +334,89 @@ function subsystemsCard(info) {
[t("settings.dcSubToken"), badge(info.deploymentcenterActive)]
])]);
}
// ─── Telegram client login (#794) ───
//
// The login code and the two-step password used to be asked in a desktop dialog. Now the
// instance holds the question and this card answers it. While a login is under way the card
// asks again every few seconds — Telegram's code arrives on the account's other devices, not
// here, so there is no event to wait for. While a question is open it does not ask: the
// answer itself moves the state on.
const TELEGRAM_BADGE = {
Connected: "erfolg", Failed: "gefahr", NeedsCode: "warnung", NeedsPassword: "warnung", Connecting: "neutral"
};
function telegramCard(initial) {
const body = erstelleElement("div", { klasse: "formular-raster" });
const card = erstelleKarte(t("settings.telegramCard"), [body]);
let timer = null;
function render(state) {
body.replaceChildren(
erstelleElement("div", { klasse: "gruppe-horizontal" },
erstellePlakette(t(`settings.telegramState.${state.state}`), TELEGRAM_BADGE[state.state] ?? "neutral")),
erstelleElement("p", { klasse: "gedaempft text-klein", text: state.message }));
if (state.state === "NeedsCode" || state.state === "NeedsPassword") {
const input = erstelleElement("input", {
type: state.state === "NeedsPassword" ? "password" : "text",
autocomplete: "one-time-code",
inputmode: state.state === "NeedsCode" ? "numeric" : null
});
const send = erstelleElement("button", { type: "submit", klasse: "schaltflaeche primaer klein", text: t("settings.telegramSend") });
const form = erstelleElement("form", { klasse: "formular-raster" },
erstelleElement("label", { text: state.state === "NeedsCode" ? t("settings.telegramCode") : t("settings.telegramPassword") }),
input, erstelleElement("div", {}, send));
form.addEventListener("submit", async (e) => {
e.preventDefault();
send.disabled = true;
try {
await api("/telegram-client/answer", { method: "POST", body: { value: input.value } });
input.value = "";
await refreshTelegram();
} catch (err) {
zeigeMeldung(err.message, "gefahr");
send.disabled = false;
}
});
body.append(form);
}
if (state.state === "Failed") {
body.append(erstelleElement("div", {}, erstelleElement("button", {
type: "button",
klasse: "schaltflaeche klein",
text: t("settings.telegramRetry"),
onclick: async () => {
try {
await api("/telegram-client/connect", { method: "POST" });
await refreshTelegram();
} catch (err) {
zeigeMeldung(err.message, "gefahr");
}
}
})));
}
clearTimeout(timer);
// Only while connecting: re-rendering during a question would wipe what is being typed.
if (state.state === "Connecting") {
timer = setTimeout(() => {
// Stop asking once the card is gone (another view, or the page was rebuilt).
if (card.isConnected) refreshTelegram();
}, 3000);
}
}
async function refreshTelegram() {
try {
render(await api("/telegram-client"));
} catch {
clearTimeout(timer);
}
}
render(initial);
return card;
}