fix(api): watchdog con timeout su tutte le richieste — mai piu' attese infinite (v0.5.1)
Sintomo sul device: l'app restava sullo splash in caricamento per sempre. Causa: QNetworkAccessManager non applica timeout — una richiesta che non riceve risposta (rete del telefono, server occupato) lasciava la UI in attesa infinita, senza errori visibili. - ApiClient::armTimeout(): watchdog per reply (abort -> normale percorso d'errore del chiamante). Applicato a: checkAuth (12 s), login (20 s), refreshSessions e openSession (60 s), newSession/chat-start/switchProfile (20-30 s), fetchProfiles (20 s), transcribe (300 s), tts (120 s). Nessun watchdog sullo stream SSE (longevo per design). - checkAuth: imposta lastError anche in errore (messaggio tradotto "Server not reachable (timeout)") e logga indirizzo ed esito - refreshSessions: log della durata (misurato sul probe: il primo fetch dopo un cambio profilo puo' prendere 11-16 s lato server, anche su DB minuscolo — l'app ora lo gestisce senza sembrare bloccata) - SplashPage: rete di sicurezza (se authChecked non arriva entro 15 s si va comunque al login) + log di instradamento - i18n: nuova stringa tradotta it/fr/de
This commit is contained in:
@@ -76,6 +76,7 @@ speaks (validated live against a real instance).
|
||||
|
||||
## Status
|
||||
|
||||
Version 0.5.0 — protocol and C++ core validated against a real `hermes-webui`
|
||||
Version 0.5.1 — protocol and C++ core validated against a real `hermes-webui`
|
||||
(login, sessions, streaming chat, TTS, STT, voice dialog, profile selection);
|
||||
UI in English, Italian, French and German; on-device testing still ongoing.
|
||||
UI in English, Italian, French and German; every request has a watchdog
|
||||
timeout (no infinite waits); on-device testing still ongoing.
|
||||
|
||||
+3
-3
@@ -76,12 +76,12 @@ richiede il probe WebUI su :8899). Esito 12/09/2026: unit 9/9, live completo OK.
|
||||
|
||||
```bash
|
||||
cd ~/workspace
|
||||
tar czf harbour-hermes-0.5.0.tar.gz \
|
||||
--transform 's,^harbour-hermes,harbour-hermes-0.5.0,' \
|
||||
tar czf harbour-hermes-0.5.1.tar.gz \
|
||||
--transform 's,^harbour-hermes,harbour-hermes-0.5.1,' \
|
||||
--exclude='.git' harbour-hermes
|
||||
```
|
||||
|
||||
Lo spec si aspetta la directory `harbour-hermes-0.5.0/` (pattern degli altri
|
||||
Lo spec si aspetta la directory `harbour-hermes-0.5.1/` (pattern degli altri
|
||||
progetti: cercato in modo robusto anche per i sorgenti live di sfdk).
|
||||
|
||||
## Traduzioni
|
||||
|
||||
@@ -30,9 +30,18 @@ Page {
|
||||
onAuthChecked: page.route(loggedIn)
|
||||
}
|
||||
|
||||
// Rete di sicurezza: se il controllo d'avvio non risponde entro 15 s
|
||||
// (bug o rete muta), si va comunque al login invece di restare qui.
|
||||
Timer {
|
||||
interval: 15000
|
||||
running: true
|
||||
onTriggered: page.route(false)
|
||||
}
|
||||
|
||||
Component.onCompleted: api.checkAuth()
|
||||
|
||||
function route(loggedIn) {
|
||||
console.log("[hermes] route: loggedIn=" + loggedIn)
|
||||
if (loggedIn)
|
||||
pageStack.replace(Qt.resolvedUrl("SessionsPage.qml"))
|
||||
else
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: harbour-hermes
|
||||
Summary: Client for the Hermes Web UI with voice
|
||||
Version: 0.5.0
|
||||
Version: 0.5.1
|
||||
Release: 1
|
||||
Group: Qt/Qt
|
||||
License: MIT
|
||||
@@ -25,6 +25,13 @@ Sessions list, chat with live streaming replies, voice dictation
|
||||
(server-side TTS), plus a hands-free conversation mode.
|
||||
|
||||
%changelog
|
||||
* Sun Sep 13 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.5.1-1
|
||||
- Fix blocco sullo splash all'avvio: watchdog con timeout su tutte le
|
||||
richieste (QNAM non ne applica: una richiesta senza risposta lasciava
|
||||
la UI in attesa infinita). checkAuth imposta un errore visibile
|
||||
("Server non raggiungibile (timeout)") e logga esito e indirizzo;
|
||||
refreshSessions logga la durata; splash con rete di sicurezza verso il
|
||||
login dopo 15 s; nessun watchdog sullo stream SSE (longevo per design)
|
||||
* Sat Sep 12 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.5.0-1
|
||||
- Sezione Sessions nelle Impostazioni: selettore profilo (le sessioni
|
||||
mostrate sono quelle del profilo scelto). Switch per-client via
|
||||
|
||||
+39
-1
@@ -10,6 +10,7 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
#include <QUrlQuery>
|
||||
|
||||
ApiClient::ApiClient(Settings *settings, QObject *parent)
|
||||
@@ -35,6 +36,25 @@ ApiClient::ApiClient(Settings *settings, QObject *parent)
|
||||
connect(m_settings, &Settings::profileChanged, this, &ApiClient::profileChanged);
|
||||
}
|
||||
|
||||
// QNetworkAccessManager non applica timeout: senza watchdog una richiesta
|
||||
// senza risposta (rete assente, server occupato) lascerebbe la UI in attesa
|
||||
// per sempre. armTimeout aborta la reply dopo N secondi: poi si attiva il
|
||||
// normale percorso d'errore del chiamante (messaggio visibile, stato resettato).
|
||||
void ApiClient::armTimeout(QNetworkReply *reply, int seconds)
|
||||
{
|
||||
QTimer *timer = new QTimer(reply);
|
||||
timer->setSingleShot(true);
|
||||
timer->setInterval(seconds * 1000);
|
||||
connect(timer, &QTimer::timeout, reply, [reply, seconds]() {
|
||||
if (reply->isRunning()) {
|
||||
qDebug() << "[api] timeout dopo" << seconds << "s su" << reply->url().path();
|
||||
reply->abort();
|
||||
}
|
||||
});
|
||||
connect(reply, &QNetworkReply::finished, timer, &QTimer::stop);
|
||||
timer->start();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- helpers --
|
||||
|
||||
QUrl ApiClient::apiUrl(const QString &path) const
|
||||
@@ -132,10 +152,16 @@ void ApiClient::checkAuth()
|
||||
return;
|
||||
}
|
||||
QNetworkReply *reply = m_nam.get(jsonRequest(QStringLiteral("/api/auth/status")));
|
||||
armTimeout(reply, 12); // il controllo d'avvio deve risolversi, sempre
|
||||
qDebug() << "[api] checkAuth ->" << m_settings->baseUrl();
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
const QByteArray body = reply->readAll();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
const bool wasTimeout = (reply->error() == QNetworkReply::OperationCanceledError);
|
||||
setLastError(wasTimeout ? tr("Server not reachable (timeout)")
|
||||
: errorFromReply(reply, body));
|
||||
qDebug() << "[api] checkAuth FALLITO:" << m_lastError;
|
||||
setLoggedIn(false);
|
||||
emit authChecked(false);
|
||||
return;
|
||||
@@ -149,6 +175,7 @@ void ApiClient::checkAuth()
|
||||
} else {
|
||||
setLoggedIn(false);
|
||||
}
|
||||
qDebug() << "[api] checkAuth:" << (m_loggedIn ? "sessione valida" : "serve il login");
|
||||
emit authChecked(m_loggedIn);
|
||||
});
|
||||
}
|
||||
@@ -165,6 +192,7 @@ void ApiClient::login(const QString &password)
|
||||
body.insert(QStringLiteral("password"), password);
|
||||
QNetworkReply *reply = m_nam.post(jsonRequest(QStringLiteral("/api/auth/login")),
|
||||
QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||
armTimeout(reply, 20);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
setBusy(false);
|
||||
@@ -210,9 +238,12 @@ void ApiClient::refreshSessions()
|
||||
q.addQueryItem(QStringLiteral("exclude_hidden"), QStringLiteral("1"));
|
||||
url.setQuery(q);
|
||||
QNetworkReply *reply = m_nam.get(QNetworkRequest(url));
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
armTimeout(reply, 60); // il primo fetch dopo un cambio profilo puo' essere lento
|
||||
const qint64 t0 = QDateTime::currentMSecsSinceEpoch();
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, t0]() {
|
||||
reply->deleteLater();
|
||||
setBusy(false);
|
||||
qDebug() << "[api] sessions in" << (QDateTime::currentMSecsSinceEpoch() - t0) << "ms";
|
||||
const QByteArray body = reply->readAll();
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
setLastError(errorFromReply(reply, body));
|
||||
@@ -238,6 +269,7 @@ QString ApiClient::profile() const
|
||||
void ApiClient::fetchProfiles()
|
||||
{
|
||||
QNetworkReply *reply = m_nam.get(jsonRequest(QStringLiteral("/api/profiles")));
|
||||
armTimeout(reply, 20);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
const QByteArray data = reply->readAll();
|
||||
@@ -279,6 +311,7 @@ void ApiClient::switchProfile(const QString &name)
|
||||
body.insert(QStringLiteral("name"), target);
|
||||
QNetworkReply *reply = m_nam.post(jsonRequest(QStringLiteral("/api/profile/switch")),
|
||||
QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||
armTimeout(reply, 20);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, target]() {
|
||||
reply->deleteLater();
|
||||
const QByteArray data = reply->readAll();
|
||||
@@ -307,6 +340,7 @@ void ApiClient::openSession(const QString &id)
|
||||
q.addQueryItem(QStringLiteral("session_id"), id);
|
||||
url.setQuery(q);
|
||||
QNetworkReply *reply = m_nam.get(QNetworkRequest(url));
|
||||
armTimeout(reply, 60);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
setBusy(false);
|
||||
@@ -337,6 +371,7 @@ void ApiClient::newSession()
|
||||
body.insert(QStringLiteral("workspace"), ws);
|
||||
QNetworkReply *reply = m_nam.post(jsonRequest(QStringLiteral("/api/session/new")),
|
||||
QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||
armTimeout(reply, 30);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
setBusy(false);
|
||||
@@ -374,6 +409,7 @@ void ApiClient::startChat(const QString &sessionId, const QString &message)
|
||||
|
||||
QNetworkReply *reply = m_nam.post(jsonRequest(QStringLiteral("/api/chat/start")),
|
||||
QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||
armTimeout(reply, 30);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
handleStartReply(reply);
|
||||
});
|
||||
@@ -540,6 +576,7 @@ void ApiClient::transcribeFile(const QString &path)
|
||||
QNetworkRequest req(apiUrl(QStringLiteral("/api/transcribe")));
|
||||
QNetworkReply *reply = m_nam.post(req, multiPart);
|
||||
multiPart->setParent(reply);
|
||||
armTimeout(reply, 300); // la trascrizione puo' richiedere tempo
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
@@ -584,6 +621,7 @@ void ApiClient::speak(const QString &text)
|
||||
body.insert(QStringLiteral("engine"), m_settings->ttsEngine());
|
||||
QNetworkReply *reply = m_nam.post(jsonRequest(QStringLiteral("/api/tts")),
|
||||
QJsonDocument(body).toJson(QJsonDocument::Compact));
|
||||
armTimeout(reply, 120);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
reply->deleteLater();
|
||||
const QByteArray body = reply->readAll();
|
||||
|
||||
@@ -112,6 +112,8 @@ private:
|
||||
void setTranscribing(bool on);
|
||||
void setLastError(const QString &error);
|
||||
void setLoggedIn(bool on);
|
||||
// Watchdog: aborta la reply dopo N secondi (QNAM non applica timeout).
|
||||
void armTimeout(QNetworkReply *reply, int seconds);
|
||||
|
||||
QUrl apiUrl(const QString &path) const;
|
||||
QNetworkRequest jsonRequest(const QString &path, bool post = false) const;
|
||||
|
||||
+2
-2
@@ -21,9 +21,9 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
|
||||
// AppConfigLocation = ~/.config/harbour/hermes (zona persistente della sandbox).
|
||||
app->setOrganizationName(QStringLiteral("harbour"));
|
||||
app->setApplicationName(QStringLiteral("hermes"));
|
||||
app->setApplicationVersion(QStringLiteral("0.5.0"));
|
||||
app->setApplicationVersion(QStringLiteral("0.5.1"));
|
||||
|
||||
qDebug() << "harbour-hermes v0.5.0 build" << __DATE__ << __TIME__;
|
||||
qDebug() << "harbour-hermes v0.5.1 build" << __DATE__ << __TIME__;
|
||||
|
||||
// Traduzioni: it/fr/de da /usr/share/harbour-hermes/translations, in base
|
||||
// alla lingua di sistema (l'inglese e' la lingua sorgente: nessun .qm).
|
||||
|
||||
@@ -4,17 +4,22 @@
|
||||
<context>
|
||||
<name>ApiClient</name>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="72" />
|
||||
<location filename="../src/apiclient.cpp" line="92" />
|
||||
<source>Authentication required</source>
|
||||
<translation>Authentifizierung erforderlich</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="74" />
|
||||
<location filename="../src/apiclient.cpp" line="94" />
|
||||
<source>HTTP %1</source>
|
||||
<translation>HTTP %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="159" />
|
||||
<location filename="../src/apiclient.cpp" line="162" />
|
||||
<source>Server not reachable (timeout)</source>
|
||||
<translation>Server nicht erreichbar (Zeitüberschreitung)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="186" />
|
||||
<source>Server address not configured</source>
|
||||
<translation>Serveradresse nicht konfiguriert</translation>
|
||||
</message>
|
||||
|
||||
@@ -4,17 +4,22 @@
|
||||
<context>
|
||||
<name>ApiClient</name>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="72" />
|
||||
<location filename="../src/apiclient.cpp" line="92" />
|
||||
<source>Authentication required</source>
|
||||
<translation>Authentification requise</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="74" />
|
||||
<location filename="../src/apiclient.cpp" line="94" />
|
||||
<source>HTTP %1</source>
|
||||
<translation>HTTP %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="159" />
|
||||
<location filename="../src/apiclient.cpp" line="162" />
|
||||
<source>Server not reachable (timeout)</source>
|
||||
<translation>Serveur injoignable (délai dépassé)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="186" />
|
||||
<source>Server address not configured</source>
|
||||
<translation>Adresse du serveur non configurée</translation>
|
||||
</message>
|
||||
|
||||
@@ -4,17 +4,22 @@
|
||||
<context>
|
||||
<name>ApiClient</name>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="72" />
|
||||
<location filename="../src/apiclient.cpp" line="92" />
|
||||
<source>Authentication required</source>
|
||||
<translation>Autenticazione richiesta</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="74" />
|
||||
<location filename="../src/apiclient.cpp" line="94" />
|
||||
<source>HTTP %1</source>
|
||||
<translation>HTTP %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="159" />
|
||||
<location filename="../src/apiclient.cpp" line="162" />
|
||||
<source>Server not reachable (timeout)</source>
|
||||
<translation>Server non raggiungibile (timeout)</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="../src/apiclient.cpp" line="186" />
|
||||
<source>Server address not configured</source>
|
||||
<translation>Indirizzo del server non configurato</translation>
|
||||
</message>
|
||||
|
||||
Reference in New Issue
Block a user