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
|
||||
|
||||
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.
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+24
-13
@@ -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": "...",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <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
|
||||
- Fix pagine non caricabili sul device: TextField.onAccepted inesistente
|
||||
-> EnterKey; Button.highlighted rimosso; ComboBox senza Repeater;
|
||||
|
||||
+16
-3
@@ -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());
|
||||
|
||||
+6
-3
@@ -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;
|
||||
|
||||
@@ -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/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
|
||||
|
||||
Reference in New Issue
Block a user