diff --git a/README.md b/README.md index 85592e7..ac77096 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,6 @@ speaks (validated live against a real instance). ## Status -Version 0.1.1 — protocol and C++ core validated against a real `hermes-webui` +Version 0.1.2 — protocol and C++ core validated against a real `hermes-webui` (login, sessions, streaming chat, TTS, STT); on-device testing (audio paths, Sailjail prompts, keyboard handling) still pending. diff --git a/docs/BUILD.md b/docs/BUILD.md index 72b6c62..4fd45e9 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -76,12 +76,12 @@ richiede il probe WebUI su :8899). Esito 12/09/2026: unit 9/9, live completo OK. ```bash cd /home/kaneda/workspace -tar czf harbour-hermes-0.1.1.tar.gz \ - --transform 's,^harbour-hermes,harbour-hermes-0.1.1,' \ +tar czf harbour-hermes-0.1.2.tar.gz \ + --transform 's,^harbour-hermes,harbour-hermes-0.1.2,' \ --exclude='.git' harbour-hermes ``` -Lo spec si aspetta la directory `harbour-hermes-0.1.1/` (pattern degli altri +Lo spec si aspetta la directory `harbour-hermes-0.1.2/` (pattern degli altri progetti: cercato in modo robusto anche per i sorgenti live di sfdk). ## 5. Dipendenze lato server (WebUI) diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index b8861e6..4f14332 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -30,32 +30,43 @@ server ha una password impostata. ## 2. Sessioni -`GET /api/sessions` → lista per la sidebar: +`GET /api/sessions?sidebar_source=webui&exclude_hidden=1` → lista per la +sidebar (stessi parametri del frontend web: solo sessioni WebUI visibili; +senza parametri arrivano anche le CLI/cron e le nascoste — lista più lunga). +**Cattura reale del 12/09/2026** (chiavi verificate): ```json { "sessions": [ { - "session_id": "171ff18f9313", - "title": "Webui Session", + "session_id": "20260907_123845_c7e52d", + "title": "Diagnosi guasto YunoHost", "workspace": "/home/kaneda/.hermes/profiles/carlo/home/workspace", "model": "deepseek-v4-flash", - "model_provider": "deepseek", - "message_count": 2, - "created_at": 1785701975.8797414, - "updated_at": 1785701985.0872252, - "pinned": false, "archived": false, + "message_count": 163, + "created_at": 1788707925.6, + "updated_at": 1788778530.726691, + "pinned": false, "archived": false, "project_id": null, "profile": "default", - "source_tag": "webui", "session_source": "webui", - "is_cli_session": false, "is_streaming": false, - "active_stream_id": null + "source_tag": "webui", "raw_source": "webui", + "session_source": "webui", "source_label": "WebUI", + "parent_session_id": null } ], - "active_profile": "default", - "server_time": 1789188804.1041577 + "sidebar_reference_sessions": [ "..." ], + "cli_count": 4, "archived_count": 0, + "archived_webui_count": 0, "archived_cli_count": 0, + "include_archived": false, "all_profiles": false, + "active_profile": "default" } ``` +⚠ Qt 5.6 (kit Sailfish 5.1): i campi di questi oggetti NON si risolvono nei +delegate QML quando il modello è una QVariantList (`model.campo` = undefined +→ righe vuote pur con dati corretti dal server). Il client usa un +QAbstractListModel C++ (`SessionsModel`) con ruoli `session_id`, `title`, +`message_count`, `updated_at`. + `GET /api/session?session_id=ID` → `{"session": { ... }}` con in più: - `messages`: array di `{"role": "user"|"assistant", "content": "...", diff --git a/harbour-hermes.pro b/harbour-hermes.pro index 5176435..c60eaa4 100644 --- a/harbour-hermes.pro +++ b/harbour-hermes.pro @@ -14,6 +14,7 @@ SOURCES += \ src/sseparser.cpp \ src/apiclient.cpp \ src/chatmodel.cpp \ + src/sessionsmodel.cpp \ src/recorder.cpp \ src/player.cpp @@ -23,6 +24,7 @@ HEADERS += \ src/sseparser.h \ src/apiclient.h \ src/chatmodel.h \ + src/sessionsmodel.h \ src/recorder.h \ src/player.h diff --git a/qml/pages/SessionsPage.qml b/qml/pages/SessionsPage.qml index 9ec0d51..30176f8 100644 --- a/qml/pages/SessionsPage.qml +++ b/qml/pages/SessionsPage.qml @@ -58,7 +58,7 @@ Page { width: parent.width font.pixelSize: Theme.fontSizeExtraSmall color: Theme.secondaryColor - text: page.describe(model) + text: page.describe(model.message_count, model.updated_at) } } @@ -66,7 +66,7 @@ Page { } ViewPlaceholder { - enabled: api.sessions.length === 0 && !api.busy + enabled: api.sessionsCount === 0 && !api.busy text: qsTr("No sessions") hintText: qsTr("Pull down to create a new session") } @@ -119,9 +119,9 @@ Page { chat.newSession() } - function describe(m) { - var n = m.message_count ? m.message_count : 0 - var d = new Date((m.updated_at ? m.updated_at : 0) * 1000) - return n + qsTr(" messages") + " · " + Qt.formatDateTime(d, "dd/MM HH:mm") + function describe(n, u) { + var c = n ? n : 0 + var d = new Date((u ? u : 0) * 1000) + return c + qsTr(" messages") + " · " + Qt.formatDateTime(d, "dd/MM HH:mm") } } diff --git a/rpm/harbour-hermes.spec b/rpm/harbour-hermes.spec index bf117cf..6e5ad08 100644 --- a/rpm/harbour-hermes.spec +++ b/rpm/harbour-hermes.spec @@ -1,6 +1,6 @@ Name: harbour-hermes Summary: Client for the Hermes Web UI with voice -Version: 0.1.1 +Version: 0.1.2 Release: 1 Group: Qt/Qt License: MIT @@ -24,6 +24,12 @@ Sessions list, chat with live streaming replies, voice dictation (server-side TTS), plus a hands-free conversation mode. %changelog +* Sat Sep 12 2026 Carlo Baratto - 0.1.2-1 +- Fix lista sessioni vuota sul device: le QVariantList di mappe non + espongono i campi nei delegate su Qt 5.6 -> nuovo SessionsModel + (QAbstractListModel con ruoli: session_id/title/message_count/updated_at) +- /api/sessions con i parametri della sidebar web (sidebar_source=webui, + exclude_hidden=1): solo sessioni WebUI, niente CLI/cron * Sat Sep 12 2026 Carlo Baratto - 0.1.1-1 - Fix pagine non caricabili sul device: TextField.onAccepted inesistente -> EnterKey; Button.highlighted rimosso; ComboBox senza Repeater; diff --git a/src/apiclient.cpp b/src/apiclient.cpp index c470e62..5226dd4 100644 --- a/src/apiclient.cpp +++ b/src/apiclient.cpp @@ -15,6 +15,7 @@ ApiClient::ApiClient(Settings *settings, QObject *parent) : QObject(parent) , m_settings(settings) , m_jar(new CookieJar(this)) + , m_sessionsModel(new SessionsModel(this)) , m_loggedIn(false) , m_busy(false) , m_streaming(false) @@ -170,7 +171,7 @@ void ApiClient::logout() }); m_jar->clear(); setLoggedIn(false); - m_sessions.clear(); + m_sessionsModel->setSessions(QVariantList()); emit sessionsChanged(); m_session.clear(); emit sessionChanged(); @@ -181,7 +182,14 @@ void ApiClient::logout() void ApiClient::refreshSessions() { setBusy(true); - QNetworkReply *reply = m_nam.get(jsonRequest(QStringLiteral("/api/sessions"))); + // Stessi parametri della sidebar del WebUI: solo sessioni WebUI visibili + // (senza parametri arriverebbero anche le CLI/cron e le nascoste). + QUrl url = apiUrl(QStringLiteral("/api/sessions")); + QUrlQuery q; + q.addQueryItem(QStringLiteral("sidebar_source"), QStringLiteral("webui")); + q.addQueryItem(QStringLiteral("exclude_hidden"), QStringLiteral("1")); + url.setQuery(q); + QNetworkReply *reply = m_nam.get(QNetworkRequest(url)); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); setBusy(false); @@ -192,11 +200,16 @@ void ApiClient::refreshSessions() } const QJsonObject obj = QJsonDocument::fromJson(body).object(); const QJsonArray arr = obj.value(QStringLiteral("sessions")).toArray(); - m_sessions = arr.toVariantList(); + m_sessionsModel->setSessions(arr.toVariantList()); emit sessionsChanged(); }); } +int ApiClient::sessionsCount() const +{ + return m_sessionsModel->rowCount(); +} + void ApiClient::openSession(const QString &id) { setLastError(QString()); diff --git a/src/apiclient.h b/src/apiclient.h index 862a55d..07e83a3 100644 --- a/src/apiclient.h +++ b/src/apiclient.h @@ -9,6 +9,7 @@ #include "cookiejar.h" #include "settings.h" +#include "sessionsmodel.h" #include "sseparser.h" class QHttpMultiPart; @@ -36,7 +37,8 @@ class ApiClient : public QObject Q_PROPERTY(bool streaming READ streaming NOTIFY streamingChanged) Q_PROPERTY(bool transcribing READ transcribing NOTIFY transcribingChanged) Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged) - Q_PROPERTY(QVariantList sessions READ sessions NOTIFY sessionsChanged) + Q_PROPERTY(SessionsModel *sessions READ sessions NOTIFY sessionsChanged) + Q_PROPERTY(int sessionsCount READ sessionsCount NOTIFY sessionsChanged) Q_PROPERTY(QVariantMap session READ session NOTIFY sessionChanged) public: @@ -47,7 +49,8 @@ public: bool streaming() const { return m_streaming; } bool transcribing() const { return m_transcribing; } QString lastError() const { return m_lastError; } - QVariantList sessions() const { return m_sessions; } + SessionsModel *sessions() const { return m_sessionsModel; } + int sessionsCount() const; QVariantMap session() const { return m_session; } // Avvio: verifica lo stato auth (cookie persistito o auth disabilitata). @@ -119,7 +122,7 @@ private: bool m_transcribing; QString m_lastError; - QVariantList m_sessions; + SessionsModel *m_sessionsModel; QVariantMap m_session; QString m_streamId; diff --git a/src/sessionsmodel.cpp b/src/sessionsmodel.cpp new file mode 100644 index 0000000..b17e5cb --- /dev/null +++ b/src/sessionsmodel.cpp @@ -0,0 +1,52 @@ +#include "sessionsmodel.h" + +#include + +SessionsModel::SessionsModel(QObject *parent) + : QAbstractListModel(parent) +{ +} + +int SessionsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return m_rows.size(); +} + +QVariant SessionsModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_rows.size()) + return QVariant(); + + const QVariantMap row = m_rows.at(index.row()).toMap(); + switch (role) { + case SessionIdRole: + return row.value(QStringLiteral("session_id")); + case TitleRole: + return row.value(QStringLiteral("title")); + case MessageCountRole: + return row.value(QStringLiteral("message_count")); + case UpdatedAtRole: + return row.value(QStringLiteral("updated_at")); + default: + return QVariant(); + } +} + +QHash SessionsModel::roleNames() const +{ + QHash names; + names[SessionIdRole] = "session_id"; + names[TitleRole] = "title"; + names[MessageCountRole] = "message_count"; + names[UpdatedAtRole] = "updated_at"; + return names; +} + +void SessionsModel::setSessions(const QVariantList &sessions) +{ + beginResetModel(); + m_rows = sessions; + endResetModel(); +} diff --git a/src/sessionsmodel.h b/src/sessionsmodel.h new file mode 100644 index 0000000..ee83a23 --- /dev/null +++ b/src/sessionsmodel.h @@ -0,0 +1,38 @@ +#ifndef SESSIONSMODEL_H +#define SESSIONSMODEL_H + +#include +#include + +// Modello delle sessioni per la lista QML. +// +// Perché non una QVariantList di mappe: sul kit Sailfish 5.1 (Qt 5.6) i campi +// di una QVariantMap dentro una lista non si risolvono nei delegate +// (model.campo = undefined -> righe vuote, pur con dati corretti dal server). +// I ruoli di un QAbstractListModel sono il meccanismo supportato da sempre +// (stesso principio dei ListModel usati con successo negli altri progetti). +class SessionsModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles { + SessionIdRole = Qt::UserRole + 1, + TitleRole, + MessageCountRole, + UpdatedAtRole, + }; + + explicit SessionsModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QHash roleNames() const override; + + // Sostituisce i dati (reset del modello: la lista QML si aggiorna). + void setSessions(const QVariantList &sessions); + +private: + QVariantList m_rows; +}; + +#endif // SESSIONSMODEL_H diff --git a/tests/core_test.pro b/tests/core_test.pro index 9d02692..1d978ce 100644 --- a/tests/core_test.pro +++ b/tests/core_test.pro @@ -19,6 +19,7 @@ SOURCES += \ $$SRC/settings.cpp \ $$SRC/apiclient.cpp \ $$SRC/chatmodel.cpp \ + $$SRC/sessionsmodel.cpp \ $$SRC/recorder.cpp \ $$SRC/player.cpp \ core_test.cpp @@ -29,5 +30,6 @@ HEADERS += \ $$SRC/settings.h \ $$SRC/apiclient.h \ $$SRC/chatmodel.h \ + $$SRC/sessionsmodel.h \ $$SRC/recorder.h \ $$SRC/player.h