fix(sessions): lista vuota sul device — modello C++ con ruoli (v0.1.2)
Causa: su Qt 5.6 (kit Sailfish 5.1) una QVariantList di QVariantMap
usata come model non fa risolvere i campi nei delegate QML
(model.title/message_count/updated_at = undefined -> righe vuote), pur
con payload corretto dal server (verificato con cattura reale del 12/09).
- Nuovo src/sessionsmodel.{h,cpp}: QAbstractListModel con ruoli
session_id/title/message_count/updated_at; api.sessions ora e' il
modello, api.sessionsCount per lo stato "nessuna sessione"
- /api/sessions con sidebar_source=webui&exclude_hidden=1 (come la
sidebar del browser: solo sessioni WebUI, niente CLI/cron/nascoste)
- SessionsPage: describe(message_count, updated_at) con parametri espliciti
- PROTOCOL.md: payload sessions verificato + nota del pitfall Qt 5.6
This commit is contained in:
@@ -29,6 +29,6 @@ speaks (validated live against a real instance).
|
|||||||
|
|
||||||
## Status
|
## 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,
|
(login, sessions, streaming chat, TTS, STT); on-device testing (audio paths,
|
||||||
Sailjail prompts, keyboard handling) still pending.
|
Sailjail prompts, keyboard handling) still pending.
|
||||||
|
|||||||
+3
-3
@@ -76,12 +76,12 @@ richiede il probe WebUI su :8899). Esito 12/09/2026: unit 9/9, live completo OK.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /home/kaneda/workspace
|
cd /home/kaneda/workspace
|
||||||
tar czf harbour-hermes-0.1.1.tar.gz \
|
tar czf harbour-hermes-0.1.2.tar.gz \
|
||||||
--transform 's,^harbour-hermes,harbour-hermes-0.1.1,' \
|
--transform 's,^harbour-hermes,harbour-hermes-0.1.2,' \
|
||||||
--exclude='.git' harbour-hermes
|
--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).
|
progetti: cercato in modo robusto anche per i sorgenti live di sfdk).
|
||||||
|
|
||||||
## 5. Dipendenze lato server (WebUI)
|
## 5. Dipendenze lato server (WebUI)
|
||||||
|
|||||||
+24
-13
@@ -30,32 +30,43 @@ server ha una password impostata.
|
|||||||
|
|
||||||
## 2. Sessioni
|
## 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
|
```json
|
||||||
{
|
{
|
||||||
"sessions": [
|
"sessions": [
|
||||||
{
|
{
|
||||||
"session_id": "171ff18f9313",
|
"session_id": "20260907_123845_c7e52d",
|
||||||
"title": "Webui Session",
|
"title": "Diagnosi guasto YunoHost",
|
||||||
"workspace": "/home/kaneda/.hermes/profiles/carlo/home/workspace",
|
"workspace": "/home/kaneda/.hermes/profiles/carlo/home/workspace",
|
||||||
"model": "deepseek-v4-flash",
|
"model": "deepseek-v4-flash",
|
||||||
"model_provider": "deepseek",
|
"message_count": 163,
|
||||||
"message_count": 2,
|
"created_at": 1788707925.6,
|
||||||
"created_at": 1785701975.8797414,
|
"updated_at": 1788778530.726691,
|
||||||
"updated_at": 1785701985.0872252,
|
"pinned": false, "archived": false, "project_id": null,
|
||||||
"pinned": false, "archived": false,
|
|
||||||
"profile": "default",
|
"profile": "default",
|
||||||
"source_tag": "webui", "session_source": "webui",
|
"source_tag": "webui", "raw_source": "webui",
|
||||||
"is_cli_session": false, "is_streaming": false,
|
"session_source": "webui", "source_label": "WebUI",
|
||||||
"active_stream_id": null
|
"parent_session_id": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"active_profile": "default",
|
"sidebar_reference_sessions": [ "..." ],
|
||||||
"server_time": 1789188804.1041577
|
"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ù:
|
`GET /api/session?session_id=ID` → `{"session": { ... }}` con in più:
|
||||||
|
|
||||||
- `messages`: array di `{"role": "user"|"assistant", "content": "...",
|
- `messages`: array di `{"role": "user"|"assistant", "content": "...",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ SOURCES += \
|
|||||||
src/sseparser.cpp \
|
src/sseparser.cpp \
|
||||||
src/apiclient.cpp \
|
src/apiclient.cpp \
|
||||||
src/chatmodel.cpp \
|
src/chatmodel.cpp \
|
||||||
|
src/sessionsmodel.cpp \
|
||||||
src/recorder.cpp \
|
src/recorder.cpp \
|
||||||
src/player.cpp
|
src/player.cpp
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ HEADERS += \
|
|||||||
src/sseparser.h \
|
src/sseparser.h \
|
||||||
src/apiclient.h \
|
src/apiclient.h \
|
||||||
src/chatmodel.h \
|
src/chatmodel.h \
|
||||||
|
src/sessionsmodel.h \
|
||||||
src/recorder.h \
|
src/recorder.h \
|
||||||
src/player.h
|
src/player.h
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ Page {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
font.pixelSize: Theme.fontSizeExtraSmall
|
font.pixelSize: Theme.fontSizeExtraSmall
|
||||||
color: Theme.secondaryColor
|
color: Theme.secondaryColor
|
||||||
text: page.describe(model)
|
text: page.describe(model.message_count, model.updated_at)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ Page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ViewPlaceholder {
|
ViewPlaceholder {
|
||||||
enabled: api.sessions.length === 0 && !api.busy
|
enabled: api.sessionsCount === 0 && !api.busy
|
||||||
text: qsTr("No sessions")
|
text: qsTr("No sessions")
|
||||||
hintText: qsTr("Pull down to create a new session")
|
hintText: qsTr("Pull down to create a new session")
|
||||||
}
|
}
|
||||||
@@ -119,9 +119,9 @@ Page {
|
|||||||
chat.newSession()
|
chat.newSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
function describe(m) {
|
function describe(n, u) {
|
||||||
var n = m.message_count ? m.message_count : 0
|
var c = n ? n : 0
|
||||||
var d = new Date((m.updated_at ? m.updated_at : 0) * 1000)
|
var d = new Date((u ? u : 0) * 1000)
|
||||||
return n + qsTr(" messages") + " · " + Qt.formatDateTime(d, "dd/MM HH:mm")
|
return c + qsTr(" messages") + " · " + Qt.formatDateTime(d, "dd/MM HH:mm")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Name: harbour-hermes
|
Name: harbour-hermes
|
||||||
Summary: Client for the Hermes Web UI with voice
|
Summary: Client for the Hermes Web UI with voice
|
||||||
Version: 0.1.1
|
Version: 0.1.2
|
||||||
Release: 1
|
Release: 1
|
||||||
Group: Qt/Qt
|
Group: Qt/Qt
|
||||||
License: MIT
|
License: MIT
|
||||||
@@ -24,6 +24,12 @@ Sessions list, chat with live streaming replies, voice dictation
|
|||||||
(server-side TTS), plus a hands-free conversation mode.
|
(server-side TTS), plus a hands-free conversation mode.
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Sat Sep 12 2026 Carlo Baratto <carlo@carlobaratto.it> - 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 <carlo@carlobaratto.it> - 0.1.1-1
|
* Sat Sep 12 2026 Carlo Baratto <carlo@carlobaratto.it> - 0.1.1-1
|
||||||
- Fix pagine non caricabili sul device: TextField.onAccepted inesistente
|
- Fix pagine non caricabili sul device: TextField.onAccepted inesistente
|
||||||
-> EnterKey; Button.highlighted rimosso; ComboBox senza Repeater;
|
-> EnterKey; Button.highlighted rimosso; ComboBox senza Repeater;
|
||||||
|
|||||||
+16
-3
@@ -15,6 +15,7 @@ ApiClient::ApiClient(Settings *settings, QObject *parent)
|
|||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_settings(settings)
|
, m_settings(settings)
|
||||||
, m_jar(new CookieJar(this))
|
, m_jar(new CookieJar(this))
|
||||||
|
, m_sessionsModel(new SessionsModel(this))
|
||||||
, m_loggedIn(false)
|
, m_loggedIn(false)
|
||||||
, m_busy(false)
|
, m_busy(false)
|
||||||
, m_streaming(false)
|
, m_streaming(false)
|
||||||
@@ -170,7 +171,7 @@ void ApiClient::logout()
|
|||||||
});
|
});
|
||||||
m_jar->clear();
|
m_jar->clear();
|
||||||
setLoggedIn(false);
|
setLoggedIn(false);
|
||||||
m_sessions.clear();
|
m_sessionsModel->setSessions(QVariantList());
|
||||||
emit sessionsChanged();
|
emit sessionsChanged();
|
||||||
m_session.clear();
|
m_session.clear();
|
||||||
emit sessionChanged();
|
emit sessionChanged();
|
||||||
@@ -181,7 +182,14 @@ void ApiClient::logout()
|
|||||||
void ApiClient::refreshSessions()
|
void ApiClient::refreshSessions()
|
||||||
{
|
{
|
||||||
setBusy(true);
|
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]() {
|
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||||
reply->deleteLater();
|
reply->deleteLater();
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
@@ -192,11 +200,16 @@ void ApiClient::refreshSessions()
|
|||||||
}
|
}
|
||||||
const QJsonObject obj = QJsonDocument::fromJson(body).object();
|
const QJsonObject obj = QJsonDocument::fromJson(body).object();
|
||||||
const QJsonArray arr = obj.value(QStringLiteral("sessions")).toArray();
|
const QJsonArray arr = obj.value(QStringLiteral("sessions")).toArray();
|
||||||
m_sessions = arr.toVariantList();
|
m_sessionsModel->setSessions(arr.toVariantList());
|
||||||
emit sessionsChanged();
|
emit sessionsChanged();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ApiClient::sessionsCount() const
|
||||||
|
{
|
||||||
|
return m_sessionsModel->rowCount();
|
||||||
|
}
|
||||||
|
|
||||||
void ApiClient::openSession(const QString &id)
|
void ApiClient::openSession(const QString &id)
|
||||||
{
|
{
|
||||||
setLastError(QString());
|
setLastError(QString());
|
||||||
|
|||||||
+6
-3
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
#include "cookiejar.h"
|
#include "cookiejar.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
|
#include "sessionsmodel.h"
|
||||||
#include "sseparser.h"
|
#include "sseparser.h"
|
||||||
|
|
||||||
class QHttpMultiPart;
|
class QHttpMultiPart;
|
||||||
@@ -36,7 +37,8 @@ class ApiClient : public QObject
|
|||||||
Q_PROPERTY(bool streaming READ streaming NOTIFY streamingChanged)
|
Q_PROPERTY(bool streaming READ streaming NOTIFY streamingChanged)
|
||||||
Q_PROPERTY(bool transcribing READ transcribing NOTIFY transcribingChanged)
|
Q_PROPERTY(bool transcribing READ transcribing NOTIFY transcribingChanged)
|
||||||
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
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)
|
Q_PROPERTY(QVariantMap session READ session NOTIFY sessionChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -47,7 +49,8 @@ public:
|
|||||||
bool streaming() const { return m_streaming; }
|
bool streaming() const { return m_streaming; }
|
||||||
bool transcribing() const { return m_transcribing; }
|
bool transcribing() const { return m_transcribing; }
|
||||||
QString lastError() const { return m_lastError; }
|
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; }
|
QVariantMap session() const { return m_session; }
|
||||||
|
|
||||||
// Avvio: verifica lo stato auth (cookie persistito o auth disabilitata).
|
// Avvio: verifica lo stato auth (cookie persistito o auth disabilitata).
|
||||||
@@ -119,7 +122,7 @@ private:
|
|||||||
bool m_transcribing;
|
bool m_transcribing;
|
||||||
QString m_lastError;
|
QString m_lastError;
|
||||||
|
|
||||||
QVariantList m_sessions;
|
SessionsModel *m_sessionsModel;
|
||||||
QVariantMap m_session;
|
QVariantMap m_session;
|
||||||
|
|
||||||
QString m_streamId;
|
QString m_streamId;
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#include "sessionsmodel.h"
|
||||||
|
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
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<int, QByteArray> SessionsModel::roleNames() const
|
||||||
|
{
|
||||||
|
QHash<int, QByteArray> 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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#ifndef SESSIONSMODEL_H
|
||||||
|
#define SESSIONSMODEL_H
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
|
// 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<int, QByteArray> 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
|
||||||
@@ -19,6 +19,7 @@ SOURCES += \
|
|||||||
$$SRC/settings.cpp \
|
$$SRC/settings.cpp \
|
||||||
$$SRC/apiclient.cpp \
|
$$SRC/apiclient.cpp \
|
||||||
$$SRC/chatmodel.cpp \
|
$$SRC/chatmodel.cpp \
|
||||||
|
$$SRC/sessionsmodel.cpp \
|
||||||
$$SRC/recorder.cpp \
|
$$SRC/recorder.cpp \
|
||||||
$$SRC/player.cpp \
|
$$SRC/player.cpp \
|
||||||
core_test.cpp
|
core_test.cpp
|
||||||
@@ -29,5 +30,6 @@ HEADERS += \
|
|||||||
$$SRC/settings.h \
|
$$SRC/settings.h \
|
||||||
$$SRC/apiclient.h \
|
$$SRC/apiclient.h \
|
||||||
$$SRC/chatmodel.h \
|
$$SRC/chatmodel.h \
|
||||||
|
$$SRC/sessionsmodel.h \
|
||||||
$$SRC/recorder.h \
|
$$SRC/recorder.h \
|
||||||
$$SRC/player.h
|
$$SRC/player.h
|
||||||
|
|||||||
Reference in New Issue
Block a user