feat: human error states, operator toasts and a queue/origin view in the omp UI

- classify transport and API failures into a short state with the next step
- count undelivered messages and show them next to the sent counter
- toast on owner paired, connection recovered and reply not delivered (throttled)
- second status line previews the next queued turns
- host spinner names the Telegram prompt while its turn runs
- sanitize every Telegram-controlled string that reaches the widget
This commit is contained in:
evandrodevbr committed 2026-10-01 17:33:37 -03:00
1 parent 9435ff709b
commit 057281a3bb
19 files changed
+862 -22

No files matched your search

+64
View File
@@ -639,6 +639,70 @@ test("Poll loop keeps the paired user when the offset is committed after the ini
assert.equal(saved.lastUpdateId, 2);
});
test("Poll loop reports recovery once, only after a failed attempt", async () => {
const run = async (failures: number): Promise<string[]> => {
const events: string[] = [];
let calls = 0;
const runPollLoop = createTelegramPollLoopRunner({
getConfig: () => ({ botToken: "123:abc", lastUpdateId: 1 }),
deleteWebhook: async () => {},
getUpdates: async () => {
calls += 1;
if (calls <= failures) throw new Error("network down");
if (calls <= failures + 2) return [];
throw new DOMException("stop", "AbortError");
},
persistConfig: async () => {},
handleUpdate: async () => {},
updateStatus: (_ctx: string, message?: string) => {
events.push(`status:${message ?? "ok"}`);
},
onRecovered: (ctx: string) => {
events.push(`recovered:${ctx}`);
},
sleep: async () => {},
});
await runPollLoop("ctx", new AbortController().signal);
return events;
};
assert.deepEqual(await run(2), [
"status:network down",
"status:ok",
"status:network down",
"status:ok",
"recovered:ctx",
]);
assert.deepEqual(await run(0), []);
});
test("Poll loop keeps polling when the recovery notice itself fails", async () => {
const recorded: string[] = [];
let calls = 0;
const runPollLoop = createTelegramPollLoopRunner({
getConfig: () => ({ botToken: "123:abc", lastUpdateId: 1 }),
deleteWebhook: async () => {},
getUpdates: async () => {
calls += 1;
if (calls === 1) throw new Error("network down");
if (calls === 2) return [];
throw new DOMException("stop", "AbortError");
},
persistConfig: async () => {},
handleUpdate: async () => {},
updateStatus: () => {},
onRecovered: () => {
throw new Error("toast broke");
},
sleep: async () => {},
recordRuntimeEvent: (category, error, details) => {
recorded.push(`${category}:${(error as Error).message}:${details?.phase}`);
},
});
await runPollLoop("ctx", new AbortController().signal);
assert.equal(calls, 3);
assert.ok(recorded.includes("polling:toast broke:recovered-notice"), recorded.join("|"));
});
test("Poll loop runner ignores stale-context status failures while retrying", async () => {
const config = { botToken: "123:abc", lastUpdateId: 1 };
const events: string[] = [];