Files
logos-basecamp/app/ModuleInstanceModel.cpp
Dario Gabriel Lipicar 7851bf9f46 feat(host): per-plugin identities, an opt-in access policy, and the source split
Each loaded plugin -- including pure-QML ones -- gets its own LogosAPI identity
rather than sharing the host's, so a plugin's calls are attributable and can be
refused independently. The host-services grant is wired through to
capability_module, and its trust root is guarded on an OUTCOME rather than a
log line.

Inter-module access policy stays OFF by default: enforce mode's derived
deny-by-default gates every ui_qml app's calls to its own backend module,
because UI plugins load out-of-process and are not tracked as dependents in the
core ModuleRegistry. Operators opt in per launch with --access-policy enforce
or LOGOS_ACCESS_POLICY.

Takes the Qt host runtime from logos-plugin-qt rather than logos-qt-sdk, which
keeps only the Qt<->lp seam headers, and moves logos-protocol onto the rev that
split host needs. On Windows logos_core must come LAST on the link line: GNU ld
resolves an archive left to right, so the view runtime's references have to be
undefined already when it reaches the import library.

Separates the two source trees -- app/ is the host, src/ is the UI shell -- and
brings the CI onto setup-nix-cache-action. Merges master.
2026-08-22 16:42:13 -03:00

157 lines
6.2 KiB
C++

#include "ModuleInstanceModel.h"
#include <QObject>
#include <QVariantMap>
ModuleInstanceModel::ModuleInstanceModel(QObject* parent)
: QAbstractListModel(parent)
{}
int ModuleInstanceModel::rowCount(const QModelIndex& parent) const
{
return parent.isValid() ? 0 : m_rows.size();
}
QString ModuleInstanceModel::Row::statusText() const
{
if (isMainUi) return QObject::tr("Main UI");
if (hasMissingDeps) return QObject::tr("Missing deps");
if (isLoaded) return QObject::tr("Loaded");
return QObject::tr("Not loaded");
}
QVariant ModuleInstanceModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid() || index.row() < 0 || index.row() >= m_rows.size())
return {};
const Row& r = m_rows[index.row()];
switch (role) {
case NameRole: return r.name;
case LabelRole: return r.label.isEmpty() ? r.name : r.label;
case DescriptionRole: return r.description;
case CategoryRole: return r.category;
case TypeRole: return r.type;
case VersionRole: return r.version;
case IconPathRole: return r.iconPath;
case InstallTypeRole: return r.installType;
case IsLoadedRole: return r.isLoaded;
case IsMainUiRole: return r.isMainUi;
case HasMissingDepsRole: return r.hasMissingDeps;
case StatusTextRole: return r.statusText();
case CpuRole: return r.cpu;
case MemoryRole: return r.memory;
}
return {};
}
QHash<int, QByteArray> ModuleInstanceModel::roleNames() const
{
return {
{NameRole, "name"},
{LabelRole, "label"},
{DescriptionRole, "description"},
{CategoryRole, "category"},
{TypeRole, "type"},
{VersionRole, "version"},
{IconPathRole, "iconPath"},
{InstallTypeRole, "installType"},
{IsLoadedRole, "isLoaded"},
{IsMainUiRole, "isMainUi"},
{HasMissingDepsRole, "hasMissingDeps"},
{StatusTextRole, "statusText"},
{CpuRole, "cpu"},
{MemoryRole, "memory"},
};
}
ModuleInstanceModel::Row ModuleInstanceModel::toRow(const QVariantMap& m)
{
Row r;
r.name = m.value(QStringLiteral("name")).toString();
const QString displayName = m.value(QStringLiteral("displayName")).toString();
r.label = displayName.isEmpty() ? r.name : displayName;
r.description = m.value(QStringLiteral("description")).toString();
r.category = m.value(QStringLiteral("category")).toString();
r.type = m.value(QStringLiteral("type")).toString();
r.version = m.value(QStringLiteral("version")).toString();
r.iconPath = m.value(QStringLiteral("iconPath")).toString();
r.installType = m.value(QStringLiteral("installType")).toString();
r.isLoaded = m.value(QStringLiteral("isLoaded")).toBool();
r.isMainUi = m.value(QStringLiteral("isMainUi")).toBool();
r.hasMissingDeps = m.value(QStringLiteral("hasMissingDeps")).toBool();
// Stats arrive as either doubles or strings depending on what the module
// reports — coerce both to double so the sort proxy always compares
// numerically.
bool ok = false;
const double cpu = m.value(QStringLiteral("cpu")).toDouble(&ok);
r.cpu = ok ? cpu : 0.0;
ok = false;
const double mem = m.value(QStringLiteral("memory")).toDouble(&ok);
r.memory = ok ? mem : 0.0;
return r;
}
QList<int> ModuleInstanceModel::diffRoles(const Row& a, const Row& b)
{
QList<int> roles;
if (a.label != b.label) roles.append(LabelRole);
if (a.description != b.description) roles.append(DescriptionRole);
if (a.category != b.category) roles.append(CategoryRole);
if (a.type != b.type) roles.append(TypeRole);
if (a.version != b.version) roles.append(VersionRole);
if (a.iconPath != b.iconPath) roles.append(IconPathRole);
if (a.installType != b.installType) roles.append(InstallTypeRole);
if (a.isLoaded != b.isLoaded) roles.append(IsLoadedRole);
if (a.isMainUi != b.isMainUi) roles.append(IsMainUiRole);
if (a.hasMissingDeps != b.hasMissingDeps) roles.append(HasMissingDepsRole);
// StatusText is derived from isMainUi/hasMissingDeps/isLoaded — refresh
// it whenever any of those changed.
if (a.isMainUi != b.isMainUi
|| a.hasMissingDeps != b.hasMissingDeps
|| a.isLoaded != b.isLoaded) {
roles.append(StatusTextRole);
}
if (a.cpu != b.cpu) roles.append(CpuRole);
if (a.memory != b.memory) roles.append(MemoryRole);
return roles;
}
void ModuleInstanceModel::replaceRows(const QVariantList& installedModules)
{
QList<Row> incoming;
incoming.reserve(installedModules.size());
for (const QVariant& v : installedModules) {
const QVariantMap m = v.toMap();
if (m.value(QStringLiteral("name")).toString().isEmpty()) continue;
incoming.append(toRow(m));
}
// Fast path: same identity + same order → patch in place and emit
// fine-grained dataChanged so QML delegates re-render only the moved
// roles. This is what keeps the 2-second core-stats poll from resetting
// the table (which would drop the selection and re-instantiate every
// delegate — visible as a flicker).
if (m_rows.size() == incoming.size()) {
bool sameOrder = true;
for (int i = 0; i < incoming.size(); ++i) {
if (m_rows[i].name != incoming[i].name) { sameOrder = false; break; }
}
if (sameOrder) {
for (int i = 0; i < incoming.size(); ++i) {
const QList<int> changed = diffRoles(m_rows[i], incoming[i]);
if (changed.isEmpty()) continue;
m_rows[i] = incoming[i];
const QModelIndex mi = index(i);
emit dataChanged(mi, mi, changed);
}
return;
}
}
// Row identities changed (add / remove / reorder) — a full reset is the
// honest signal to consumers and keeps the code simple.
beginResetModel();
m_rows = std::move(incoming);
endResetModel();
}