feat(qt6): DBAL live data integration + fix deployment Python 3.9 compat

Wire up DBALProvider across all Qt6 views for live CRUD operations,
add PackageLoader, MediaServicePanel, NotificationsPanel, SettingsView,
and JSON-driven CMake config. Fix deployment helpers.py str|None syntax
for Python 3.9 compatibility via __future__ annotations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 01:43:26 +00:00
parent 2895bc053e
commit e0893c2fe3
40 changed files with 6021 additions and 482 deletions
+2
View File
@@ -1,5 +1,7 @@
"""Shared helpers for all CLI command modules."""
from __future__ import annotations
import os
import subprocess
import sys
+76 -3
View File
@@ -2,11 +2,45 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: "transparent"
// ── DBAL connection ──────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
function loadEntityData() {
if (!useLiveData) return;
dbal.list(selectedEntity, { take: pageSize, skip: currentPage * pageSize }, function(result, error) {
if (error || !result) return;
var items = result.items || [];
var fields = entityFields[selectedEntity] || [];
var liveRecords = [];
for (var i = 0; i < items.length; i++) {
var rec = {};
for (var f = 0; f < fields.length; f++) {
rec[fields[f]] = items[i][fields[f]] || "";
}
liveRecords.push(rec);
}
var updated = Object.assign({}, records);
updated[selectedEntity] = liveRecords;
records = updated;
});
}
Component.onCompleted: {
if (useLiveData) loadEntityData();
}
onUseLiveDataChanged: {
if (useLiveData) loadEntityData();
}
// ── State ──────────────────────────────────────────────────────
property string selectedEntity: "User"
property string searchText: ""
@@ -281,6 +315,7 @@ Rectangle {
selectAll = false;
searchText = "";
activeFilter = "All";
if (useLiveData) loadEntityData();
}
// ── Layout ─────────────────────────────────────────────────────
@@ -395,6 +430,10 @@ Rectangle {
Layout.fillWidth: true
spacing: 12
CText { variant: "h3"; text: (entityIcons[selectedEntity] || "") + " " + selectedEntity + " Management" }
CStatusBadge {
status: useLiveData ? "success" : "warning"
text: useLiveData ? "Live" : "Mock"
}
Item { Layout.fillWidth: true }
CButton {
text: "Create Record"
@@ -749,7 +788,17 @@ Rectangle {
newRec[fields[f]] = createFormData[fields[f]] || "";
}
if (!newRec.status) newRec.status = "Active";
addRecord(newRec);
if (useLiveData) {
dbal.create(selectedEntity, newRec, function(result, error) {
if (!error) {
loadEntityData();
} else {
addRecord(newRec);
}
});
} else {
addRecord(newRec);
}
createDialogOpen = false;
}
}
@@ -810,7 +859,17 @@ Rectangle {
for (var f = 1; f < fields.length; f++) {
updatedRec[fields[f]] = editFormData[fields[f]] || editingRecord[fields[f]] || "";
}
updateRecord(updatedRec);
if (useLiveData) {
dbal.update(selectedEntity, editingRecord.id, updatedRec, function(result, error) {
if (!error) {
loadEntityData();
} else {
updateRecord(updatedRec);
}
});
} else {
updateRecord(updatedRec);
}
editDialogOpen = false;
}
}
@@ -861,7 +920,21 @@ Rectangle {
variant: "danger"
size: "sm"
onClicked: {
deleteRecord(editingIndex);
if (useLiveData) {
var paged = getPagedRecords();
var rec = paged[editingIndex];
if (rec) {
dbal.remove(selectedEntity, rec.id, function(result, error) {
if (!error) {
loadEntityData();
} else {
deleteRecord(editingIndex);
}
});
}
} else {
deleteRecord(editingIndex);
}
deleteDialogOpen = false;
}
}
+157 -1
View File
@@ -1,7 +1,9 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt.labs.settings 1.0
import QmlComponents 1.0
import "qmllib/dbal"
ApplicationWindow {
id: appWindow
@@ -11,11 +13,23 @@ ApplicationWindow {
title: "MetaBuilder Observatory"
color: Theme.background
// ── DBAL connection ──
DBALProvider {
id: dbalProvider
}
// ── Theme ──
property string currentTheme: "dark"
// ── DBAL offline detection ──
property bool dbalConnected: dbalProvider.connected
// ── Auth state ──
property int currentLevel: 1
property string currentUser: ""
property string currentRole: "public"
property bool loggedIn: false
property string authToken: ""
property string currentView: "frontpage"
// Seed users (mirrors old/ seed data)
@@ -45,6 +59,8 @@ ApplicationWindow {
currentRole = "public"
currentLevel = 1
loggedIn = false
authToken = ""
dbalProvider.authToken = ""
currentView = "frontpage"
}
@@ -67,6 +83,39 @@ ApplicationWindow {
text: "Level " + currentLevel
}
// DBAL connection status
Row {
spacing: 4
Layout.leftMargin: 4
Rectangle {
width: 8
height: 8
radius: 4
color: dbalProvider.connected ? "#4caf50" : "#f44336"
anchors.verticalCenter: parent.verticalCenter
}
CText {
variant: "caption"
text: "DBAL"
anchors.verticalCenter: parent.verticalCenter
}
}
// Theme toggle
CButton {
variant: "ghost"
size: "sm"
text: currentTheme === "dark" ? "Light" : "Dark"
onClicked: {
currentTheme = currentTheme === "dark" ? "light" : "dark"
if (typeof Theme.setTheme === "function") {
Theme.setTheme(currentTheme)
}
}
}
Item { Layout.fillWidth: true }
// Level navigation
@@ -111,9 +160,29 @@ ApplicationWindow {
}
}
// ── DBAL offline banner ──
Rectangle {
id: dbalBanner
visible: !dbalConnected
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 28
color: "#e65100"
z: 10
CText {
anchors.centerIn: parent
text: "DBAL Offline — showing cached data"
variant: "caption"
color: "#ffffff"
}
}
// ── Sidebar + Content ──
RowLayout {
anchors.fill: parent
anchors.topMargin: dbalBanner.visible ? 28 : 0
spacing: 0
// Sidebar (Level 2+)
@@ -205,7 +274,7 @@ ApplicationWindow {
PackageManager {} // 12: Package Manager
Storybook {} // 13: Storybook
SuperGodPanel {} // 14: Super God Panel
PackageViewLoader { packageId: "user-settings" } // 15: Settings
SettingsView {} // 15: Settings
CommentsView {} // 16: Comments
}
}
@@ -221,4 +290,91 @@ ApplicationWindow {
var idx = views.indexOf(view)
return idx >= 0 ? idx : 0
}
// ── Window state persistence ──
Settings {
id: windowSettings
category: "MetaBuilder"
property alias windowWidth: appWindow.width
property alias windowHeight: appWindow.height
property alias windowX: appWindow.x
property alias windowY: appWindow.y
property alias theme: appWindow.currentTheme
property alias authToken: appWindow.authToken
}
// ── Auto-login with persisted token ──
Component.onCompleted: {
if (authToken !== "") {
dbalProvider.authToken = authToken
dbalProvider.execute("core/auth/validate", { token: authToken }, function(result, error) {
if (!error && result && result.valid) {
currentUser = result.username || ""
currentRole = result.role || "user"
currentLevel = result.level || 2
loggedIn = true
currentView = "dashboard"
} else {
// Token invalid or expired — clear it
authToken = ""
dbalProvider.authToken = ""
}
})
}
}
// ── Keyboard shortcuts ──
Shortcut {
sequence: "Ctrl+K"
onActivated: console.log("[MetaBuilder] Command palette (Ctrl+K) — not yet implemented")
}
Shortcut {
sequence: "Ctrl+L"
onActivated: {
if (loggedIn) {
logout()
} else {
currentView = "login"
}
}
}
Shortcut {
sequence: "Ctrl+1"
onActivated: currentView = "frontpage"
}
Shortcut {
sequence: "Ctrl+2"
onActivated: if (currentLevel >= 2) currentView = "dashboard"
}
Shortcut {
sequence: "Ctrl+3"
onActivated: if (currentLevel >= 3) currentView = "admin"
}
Shortcut {
sequence: "Ctrl+4"
onActivated: if (currentLevel >= 4) currentView = "god-panel"
}
Shortcut {
sequence: "Ctrl+5"
onActivated: if (currentLevel >= 5) currentView = "supergod"
}
Shortcut {
sequence: "Escape"
onActivated: {
if (currentView === "login") {
currentView = "frontpage"
} else if (loggedIn && currentView !== "dashboard") {
currentView = "dashboard"
} else if (!loggedIn && currentView !== "frontpage") {
currentView = "frontpage"
}
}
}
}
+4
View File
@@ -21,6 +21,7 @@ qt_add_executable(dbal-qml
src/PackageRegistry.cpp
src/ModPlayer.cpp
src/DBALClient.cpp
src/PackageLoader.cpp
)
# Pass source dir so the binary can find shared QML components at runtime
@@ -55,6 +56,9 @@ qt_add_qml_module(dbal-qml
ModPlayerPanel.qml
PackageManager.qml
Storybook.qml
MediaServicePanel.qml
NotificationsPanel.qml
SettingsView.qml
qmllib/dbal/DBALProvider.qml
RESOURCES
assets/audio/retro-gaming.mod
+1 -1
View File
@@ -4,6 +4,6 @@
"conan": {}
},
"include": [
"build/generators/CMakePresets.json"
"build/Release/generators/CMakePresets.json"
]
}
+71 -4
View File
@@ -2,13 +2,71 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
color: "transparent"
// ── DBAL connection ──
DBALProvider { id: dbal }
property string newCommentText: ""
property int sortMode: 0 // 0=Newest, 1=Oldest, 2=Most Liked
// ── DBAL data loading ──
function loadComments() {
dbal.list("comment", { take: 50 }, function(result, error) {
if (result && result.items && result.items.length > 0) {
commentsModel.clear();
for (var i = 0; i < result.items.length; i++) {
var c = result.items[i];
commentsModel.append({
commentId: c.id || (i + 1),
username: c.username || c.author || "unknown",
initials: (c.username || c.author || "??").substring(0, 2).toUpperCase(),
timestamp: c.timestamp || c.createdAt || "Unknown",
body: c.body || c.text || "",
likes: c.likes || 0,
liked: false
});
}
}
// On error or empty result, keep existing mock data
});
}
function postCommentToDBAL(text) {
var commentData = {
text: text,
author: appWindow.currentUser,
username: appWindow.currentUser
};
dbal.create("comment", commentData, function(result, error) {
if (error) {
console.warn("Failed to post comment to DBAL:", error);
}
// Comment is already added locally via addComment()
});
}
function likeCommentOnDBAL(commentId, newLikes) {
dbal.update("comment", commentId, { likes: newLikes }, function(result, error) {
if (error) console.warn("Failed to update like on DBAL:", error);
});
}
function deleteCommentOnDBAL(commentId) {
dbal.remove("comment", commentId, function(result, error) {
if (error) console.warn("Failed to delete comment on DBAL:", error);
});
}
Component.onCompleted: {
dbal.ping(function(success) {
if (success) loadComments();
});
}
ListModel {
id: commentsModel
@@ -71,15 +129,17 @@ Rectangle {
function addComment() {
if (newCommentText.trim().length === 0) return
var initials = appWindow.currentUser.substring(0, 2).toUpperCase()
var text = newCommentText.trim()
commentsModel.insert(0, {
commentId: commentsModel.count + 1,
username: appWindow.currentUser,
initials: initials,
timestamp: "Just now",
body: newCommentText.trim(),
body: text,
likes: 0,
liked: false
})
postCommentToDBAL(text)
newCommentText = ""
}
@@ -220,13 +280,17 @@ Rectangle {
variant: model.liked ? "primary" : "ghost"
size: "sm"
onClicked: {
var newLikes;
if (model.liked) {
commentsModel.setProperty(index, "likes", model.likes - 1)
newLikes = model.likes - 1;
commentsModel.setProperty(index, "likes", newLikes)
commentsModel.setProperty(index, "liked", false)
} else {
commentsModel.setProperty(index, "likes", model.likes + 1)
newLikes = model.likes + 1;
commentsModel.setProperty(index, "likes", newLikes)
commentsModel.setProperty(index, "liked", true)
}
likeCommentOnDBAL(model.commentId, newLikes)
}
}
@@ -237,7 +301,10 @@ Rectangle {
variant: "danger"
size: "sm"
visible: canDelete(model.username)
onClicked: commentsModel.remove(index)
onClicked: {
deleteCommentOnDBAL(model.commentId)
commentsModel.remove(index)
}
}
}
}
@@ -2,12 +2,33 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: Theme.background
// ── DBAL ──────────────────────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
// Flat array representing tree structure; depth encodes hierarchy
property var mockTreeNodes: [
{ nodeId: 0, name: "App", type: "container", depth: 0, visible: true, props: [{ key: "maxWidth", value: "1280" }] },
{ nodeId: 1, name: "NavBar", type: "layout", depth: 1, visible: true, props: [{ key: "sticky", value: "true" }, { key: "height", value: "64" }] },
{ nodeId: 2, name: "MainContent", type: "container", depth: 1, visible: true, props: [{ key: "padding", value: "24" }] },
{ nodeId: 3, name: "HeroSection", type: "widget", depth: 2, visible: true, props: [{ key: "title", value: "Welcome" }, { key: "backgroundImage", value: "hero.png" }] },
{ nodeId: 4, name: "FeatureGrid", type: "layout", depth: 2, visible: true, props: [{ key: "columns", value: "3" }, { key: "gap", value: "16" }] },
{ nodeId: 5, name: "FeatureCard", type: "atom", depth: 3, visible: true, props: [{ key: "icon", value: "speed" }, { key: "label", value: "Fast" }] },
{ nodeId: 6, name: "FeatureCard", type: "atom", depth: 3, visible: true, props: [{ key: "icon", value: "shield" }, { key: "label", value: "Secure" }] },
{ nodeId: 7, name: "FeatureCard", type: "atom", depth: 3, visible: true, props: [{ key: "icon", value: "plug" }, { key: "label", value: "Extensible" }] },
{ nodeId: 8, name: "ContactForm", type: "widget", depth: 2, visible: true, props: [{ key: "action", value: "/api/contact" }] },
{ nodeId: 9, name: "Footer", type: "layout", depth: 1, visible: true, props: [{ key: "copyright", value: "2026" }] },
{ nodeId: 10, name: "Sidebar", type: "container", depth: 1, visible: true, props: [{ key: "width", value: "280" }, { key: "collapsible", value: "true" }] },
{ nodeId: 11, name: "NavigationList", type: "widget", depth: 2, visible: true, props: [{ key: "items", value: "5" }] },
{ nodeId: 12, name: "UserPanel", type: "widget", depth: 2, visible: true, props: [{ key: "showAvatar", value: "true" }] }
]
property var treeNodes: [
{ nodeId: 0, name: "App", type: "container", depth: 0, visible: true, props: [{ key: "maxWidth", value: "1280" }] },
{ nodeId: 1, name: "NavBar", type: "layout", depth: 1, visible: true, props: [{ key: "sticky", value: "true" }, { key: "height", value: "64" }] },
@@ -59,6 +80,9 @@ Rectangle {
props: []
}
nextNodeId++
if (useLiveData) {
dbal.create("component_node", newNode, function(r, e) { if (!e) loadComponents() })
}
var updated = treeNodes.slice()
updated.splice(insertAt, 0, newNode)
treeNodes = updated
@@ -69,6 +93,9 @@ Rectangle {
if (idx < 0 || idx >= treeNodes.length) return
// Prevent removing root
if (treeNodes[idx].depth === 0) return
if (useLiveData && treeNodes[idx].id) {
dbal.remove("component_node", treeNodes[idx].id, function(r, e) { if (!e) loadComponents() })
}
var endIdx = subtreeEnd(idx)
var updated = treeNodes.slice()
updated.splice(idx, endIdx - idx)
@@ -95,6 +122,30 @@ Rectangle {
return s
}
// ── DBAL Integration ─────────────────────────────────────────────
function loadComponents() {
dbal.list("component_node", { take: 200 }, function(result, error) {
if (!error && result && result.items && result.items.length > 0) {
var parsed = []; var maxId = 0
for (var i = 0; i < result.items.length; i++) {
var n = result.items[i]; var nid = n.nodeId || n.id || i
if (nid > maxId) maxId = nid
parsed.push({ id: n.id, nodeId: nid, name: n.name || "Component", type: n.type || "atom", depth: n.depth !== undefined ? n.depth : 0, visible: n.visible !== undefined ? n.visible : true, props: n.props || [] })
}
treeNodes = parsed; nextNodeId = maxId + 1
}
})
}
onUseLiveDataChanged: { if (useLiveData) loadComponents() }
Component.onCompleted: { loadComponents() }
function saveNode(idx) {
if (!useLiveData) return
var node = treeNodes[idx]
var data = { nodeId: node.nodeId, name: node.name, type: node.type, depth: node.depth, visible: node.visible, props: node.props }
if (node.id) dbal.update("component_node", node.id, data, function(r, e) { if (!e) loadComponents() })
else dbal.create("component_node", data, function(r, e) { if (!e) loadComponents() })
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
@@ -300,6 +351,7 @@ Rectangle {
var updated = treeNodes.slice()
updated[selectedIndex] = Object.assign({}, updated[selectedIndex], { name: text })
treeNodes = updated
saveNode(selectedIndex)
}
}
}
@@ -325,6 +377,7 @@ Rectangle {
var updated = treeNodes.slice()
updated[selectedIndex] = Object.assign({}, updated[selectedIndex], { type: modelData })
treeNodes = updated
saveNode(selectedIndex)
}
}
}
@@ -346,6 +399,7 @@ Rectangle {
var updated = treeNodes.slice()
updated[selectedIndex] = Object.assign({}, updated[selectedIndex], { visible: checked })
treeNodes = updated
saveNode(selectedIndex)
}
}
}
+42 -7
View File
@@ -2,10 +2,16 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: Theme.background
// ── DBAL ──────────────────────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
// ── Mock data ──────────────────────────────────────────────────────
property var cssClasses: [
{
@@ -104,6 +110,7 @@ Rectangle {
function updateClasses(arr) {
cssClasses = arr
if (useLiveData) saveCssClass(selectedClassIndex)
}
function addPropertyToSelected() {
@@ -155,21 +162,24 @@ Rectangle {
}
function addClass(name) {
var newClass = { name: name, usageCount: 0, properties: [{ prop: "color", value: "#ffffff" }] }
if (useLiveData) {
dbal.execute("core/css-classes/create", { data: newClass }, function(r, e) { if (!e) loadCssClasses() })
}
var cls = cssClasses.slice()
cls.push({
name: name,
usageCount: 0,
properties: [{ prop: "color", value: "#ffffff" }]
})
updateClasses(cls)
cls.push(newClass)
cssClasses = cls
selectedClassIndex = cls.length - 1
}
function deleteSelectedClass() {
if (cssClasses.length <= 1) return
if (useLiveData && cssClasses[selectedClassIndex].id) {
dbal.execute("core/css-classes/delete", { id: cssClasses[selectedClassIndex].id }, function(r, e) { if (!e) loadCssClasses() })
}
var cls = cssClasses.slice()
cls.splice(selectedClassIndex, 1)
updateClasses(cls)
cssClasses = cls
if (selectedClassIndex >= cls.length)
selectedClassIndex = cls.length - 1
}
@@ -204,6 +214,31 @@ Rectangle {
return resolvePreviewColor(properties, "color", Theme.surface)
}
// ── DBAL Integration ─────────────────────────────────────────────
function loadCssClasses() {
dbal.execute("core/css-classes", {}, function(result, error) {
if (!error && result && result.items && result.items.length > 0) {
var parsed = []
for (var i = 0; i < result.items.length; i++) {
var c = result.items[i]
parsed.push({ id: c.id, name: c.name || "", usageCount: c.usageCount || 0, properties: c.properties || [] })
}
cssClasses = parsed
if (selectedClassIndex >= cssClasses.length) selectedClassIndex = cssClasses.length - 1
}
})
}
onUseLiveDataChanged: { if (useLiveData) loadCssClasses() }
Component.onCompleted: { loadCssClasses() }
function saveCssClass(index) {
if (!useLiveData || index < 0 || index >= cssClasses.length) return
var cls = cssClasses[index]
var data = { name: cls.name, usageCount: cls.usageCount, properties: cls.properties }
if (cls.id) dbal.execute("core/css-classes/update", { id: cls.id, data: data }, function(r, e) {})
else dbal.execute("core/css-classes/create", { data: data }, function(r, e) { if (!e) loadCssClasses() })
}
// ── Layout ──────────────────────────────────────────────────────────
ColumnLayout {
+29 -1
View File
@@ -2,10 +2,30 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: dashRoot
color: "transparent"
// ── DBAL connection ──────────────────────────────────────────
DBALProvider { id: dbal }
property var healthData: ({})
property bool dbalOnline: dbal.connected
function refreshDBAL() {
dbal.ping(function(success, error) {
if (success) {
dbal.execute("health", {}, function(result, err) {
if (result) healthData = result;
});
}
});
}
Component.onCompleted: refreshDBAL()
ScrollView {
anchors.fill: parent
anchors.margins: 24
@@ -31,6 +51,14 @@ Rectangle {
variant: "body1"
text: "Level " + appWindow.currentLevel + " \u00b7 " + appWindow.currentRole + " access"
}
CButton {
text: dbal.loading ? "Refreshing..." : "Refresh"
variant: "ghost"
size: "sm"
enabled: !dbal.loading
onClicked: refreshDBAL()
}
}
}
@@ -41,7 +69,7 @@ Rectangle {
Repeater {
model: [
{ title: "DBAL Status", value: "Healthy", status: "success" },
{ title: "DBAL Status", value: dbalOnline ? "Healthy" : "Offline", status: dbalOnline ? "success" : "error" },
{ title: "Packages", value: "20", status: "info" },
{ title: "Active Users", value: "4", status: "info" },
{ title: "Uptime", value: "99.9%", status: "success" }
+76 -1
View File
@@ -2,11 +2,81 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: Theme.background
// ── DBAL connection ──────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
property var mockBackends: JSON.parse(JSON.stringify(backends))
function loadAdapterStatus() {
dbal.execute("core/adapters", {}, function(result, error) {
if (!error && result && result.adapters && result.adapters.length > 0) {
var liveBackends = []
for (var i = 0; i < result.adapters.length; i++) {
var a = result.adapters[i]
liveBackends.push({
name: a.name || "",
key: a.key || "",
status: a.status || "disconnected",
description: a.description || "",
connectionString: a.connectionString || "",
records: a.records || 0,
sizeKb: a.sizeKb || 0,
lastBackup: a.lastBackup || "Never"
})
}
backends = liveBackends
if (selectedBackendIndex >= liveBackends.length)
selectedBackendIndex = 0
if (activeBackendIndex >= liveBackends.length)
activeBackendIndex = 0
}
// On error or empty result, keep existing mock backends as fallback
})
}
function testConnectionLive(index) {
var backend = backends[index]
testingIndex = index
if (useLiveData) {
dbal.execute("core/test-connection", { adapter: backend.key }, function(result, error) {
var newResults = Object.assign({}, testResults)
if (!error && result && result.success) {
newResults[index] = "success"
} else {
newResults[index] = "error"
}
testResults = newResults
testingIndex = -1
})
} else {
// Fall back to mock timer
testTimer.targetIndex = index
testTimer.start()
}
}
function checkHealth() {
if (useLiveData) {
dbal.ping()
}
}
onUseLiveDataChanged: {
if (useLiveData) loadAdapterStatus()
}
Component.onCompleted: {
loadAdapterStatus()
}
// ── State ──────────────────────────────────────────────────────────
property int selectedBackendIndex: 2
property int activeBackendIndex: 2
@@ -144,6 +214,11 @@ Rectangle {
CText { variant: "h3"; text: "Database Manager" }
CStatusBadge { status: "success"; text: connectedCount() + " / " + backends.length + " connected" }
CBadge {
text: useLiveData ? "Connected to DBAL" : "Mock Data"
color: useLiveData ? Theme.success : Theme.warning
}
Item { Layout.fillWidth: true }
CButton { text: "Export"; variant: "ghost"; onClicked: showExportDialog = true }
@@ -316,7 +391,7 @@ Rectangle {
text: testingIndex === selectedBackendIndex ? "Testing..." : "Test Connection"
variant: "primary"
enabled: testingIndex === -1
onClicked: testConnection(selectedBackendIndex)
onClicked: testConnectionLive(selectedBackendIndex)
}
CButton {
+42 -6
View File
@@ -2,11 +2,16 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: Theme.background
// ── DBAL ──────────────────────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
property int selectedIndex: -1
property bool addDialogOpen: false
property bool deleteDialogOpen: false
@@ -122,6 +127,7 @@ Rectangle {
var copy = dropdowns.slice()
copy[index] = updated
dropdowns = copy
if (useLiveData) saveDropdown(index)
}
function updateSelectedField(field, value) {
@@ -165,16 +171,18 @@ Rectangle {
function addDropdown() {
if (newDropdownName.trim() === "") return
var copy = dropdowns.slice()
copy.push({
var newDd = {
name: newDropdownName.trim().toLowerCase().replace(/ /g, "_"),
description: newDropdownDescription.trim() || "No description",
allowCustom: false,
required: false,
options: [
{ label: "Option 1", value: "option_1" }
]
})
options: [{ label: "Option 1", value: "option_1" }]
}
if (useLiveData) {
dbal.execute("core/dropdown-configs/create", { data: newDd }, function(r, e) { if (!e) loadDropdowns() })
}
var copy = dropdowns.slice()
copy.push(newDd)
dropdowns = copy
selectedIndex = dropdowns.length - 1
newDropdownName = ""
@@ -184,6 +192,9 @@ Rectangle {
function deleteSelectedDropdown() {
if (selectedIndex < 0) return
if (useLiveData && dropdowns[selectedIndex].id) {
dbal.execute("core/dropdown-configs/delete", { id: dropdowns[selectedIndex].id }, function(r, e) { if (!e) loadDropdowns() })
}
var copy = dropdowns.slice()
copy.splice(selectedIndex, 1)
dropdowns = copy
@@ -191,6 +202,31 @@ Rectangle {
deleteDialogOpen = false
}
// ── DBAL Integration ─────────────────────────────────────────────
function loadDropdowns() {
dbal.execute("core/dropdown-configs", {}, function(result, error) {
if (!error && result && result.items && result.items.length > 0) {
var parsed = []
for (var i = 0; i < result.items.length; i++) {
var d = result.items[i]
parsed.push({ id: d.id, name: d.name || "", description: d.description || "", allowCustom: d.allowCustom || false, required: d.required || false, options: d.options || [] })
}
dropdowns = parsed
if (selectedIndex >= dropdowns.length) selectedIndex = dropdowns.length > 0 ? dropdowns.length - 1 : -1
}
})
}
onUseLiveDataChanged: { if (useLiveData) loadDropdowns() }
Component.onCompleted: { loadDropdowns() }
function saveDropdown(index) {
if (!useLiveData || index < 0 || index >= dropdowns.length) return
var dd = dropdowns[index]
var data = { name: dd.name, description: dd.description, allowCustom: dd.allowCustom, required: dd.required, options: dd.options }
if (dd.id) dbal.execute("core/dropdown-configs/update", { id: dd.id, data: data }, function(r, e) {})
else dbal.execute("core/dropdown-configs/create", { data: data }, function(r, e) { if (!e) loadDropdowns() })
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
+56 -5
View File
@@ -2,11 +2,66 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: Theme.background
// ── DBAL connection ──
DBALProvider { id: dbal }
property bool dbalOnline: dbal.connected
property string platformVersion: ""
property var publicStats: ({ users: "---", packages: "---", workflows: "---" })
// ── Mock fallback data ──
property var mockStatusItems: [
{ label: "DBAL stack", value: "healthy" },
{ label: "Prisma migrations", value: "pending" },
{ label: "Daemon progress", value: "building" }
]
function loadPlatformStatus() {
dbal.ping(function(success) {
if (success) {
// Load version info
dbal.execute("core/version", {}, function(result, error) {
if (result && result.version) {
platformVersion = result.version;
}
});
// Load public stats
dbal.execute("core/stats", {}, function(result, error) {
if (result) {
publicStats = {
users: result.totalUsers || publicStats.users,
packages: result.totalPackages || publicStats.packages,
workflows: result.totalWorkflows || publicStats.workflows
};
}
});
// Load live status items
dbal.execute("health", {}, function(result, error) {
if (result && result.services) {
var liveItems = [];
for (var i = 0; i < result.services.length; i++) {
var svc = result.services[i];
liveItems.push({ label: svc.name, value: svc.status });
}
if (liveItems.length > 0) {
statusItems = liveItems;
}
}
});
}
});
}
Component.onCompleted: loadPlatformStatus()
property int currentTab: 0
property var featureHighlights: [
@@ -21,11 +76,7 @@ Rectangle {
{ name: "prisma-migrations", status: "running" }
]
property var statusItems: [
{ label: "DBAL stack", value: "healthy" },
{ label: "Prisma migrations", value: "pending" },
{ label: "Daemon progress", value: "building" }
]
property var statusItems: mockStatusItems
ScrollView {
anchors.fill: parent
+11 -1
View File
@@ -37,6 +37,7 @@ Rectangle {
{ label: "CSS Classes" },
{ label: "Dropdowns" },
{ label: "Database" },
{ label: "Media" },
{ label: "Settings" }
]
@@ -312,7 +313,16 @@ Rectangle {
}
}
// 12 - Settings (inline: Theme + SMTP side by side)
// 12 - Media Service
Rectangle {
color: "transparent"
Loader {
anchors.fill: parent
source: "MediaServicePanel.qml"
}
}
// 13 - Settings (inline: Theme + SMTP side by side)
Rectangle {
color: "transparent"
+41 -9
View File
@@ -2,12 +2,18 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: loginView
color: "transparent"
property string errorMessage: ""
property bool loggingIn: false
DBALProvider {
id: dbal
}
ColumnLayout {
anchors.centerIn: parent
@@ -39,6 +45,7 @@ Rectangle {
Layout.fillWidth: true
label: "Username"
placeholderText: "demo, admin, god, or super"
enabled: !loggingIn
}
CTextField {
@@ -47,20 +54,22 @@ Rectangle {
label: "Password"
placeholderText: "Enter password"
echoMode: TextInput.Password
enabled: !loggingIn
onAccepted: doLogin()
}
CText {
CAlert {
Layout.fillWidth: true
visible: errorMessage.length > 0
severity: "error"
text: errorMessage
colorVariant: "error"
variant: "body2"
}
CButton {
Layout.fillWidth: true
text: "Sign In"
text: loggingIn ? "Signing in..." : "Sign In"
variant: "primary"
enabled: !loggingIn
onClicked: doLogin()
}
@@ -77,11 +86,34 @@ Rectangle {
}
}
function loginWithDBAL(username, password) {
loggingIn = true
errorMessage = ""
dbal.execute("core/auth/login", { username: username, password: password }, function(result, error) {
if (!error && result && result.token) {
appWindow.currentUser = result.username || username
appWindow.currentRole = result.role || "user"
appWindow.currentLevel = result.level || 2
appWindow.loggedIn = true
appWindow.authToken = result.token
dbal.authToken = result.token
appWindow.currentView = "dashboard"
loggingIn = false
} else {
// DBAL failed — fall back to local seed user auth
loggingIn = false
if (appWindow.login(username, password)) {
errorMessage = ""
} else {
errorMessage = error || "Invalid username or password"
}
}
})
}
function doLogin() {
if (appWindow.login(usernameField.text, passwordField.text)) {
errorMessage = ""
} else {
errorMessage = "Invalid username or password"
}
errorMessage = ""
loginWithDBAL(usernameField.text, passwordField.text)
}
}
File diff suppressed because it is too large Load Diff
+445
View File
@@ -0,0 +1,445 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: "transparent"
// ── DBAL connection ──────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
// ── State ────────────────────────────────────────────────────
property string activeFilter: "All"
property int unreadCount: countUnread()
property var filters: ["All", "System", "Alerts", "Info"]
// ── Mock data (mirrors AdminView notification records) ───────
property var notifications: [
{ id: "NTF-001", type: "system", title: "Scheduled Maintenance", message: "Maintenance window 03/20 from 02:00-04:00 UTC", timestamp: "2026-03-18 06:00", read: false },
{ id: "NTF-002", type: "alert", title: "High CPU Alert", message: "High CPU on dbal-prod — 94% sustained for 10m", timestamp: "2026-03-18 07:30", read: false },
{ id: "NTF-003", type: "info", title: "Export Complete", message: "Your export is ready for download", timestamp: "2026-03-18 09:00", read: true },
{ id: "NTF-004", type: "warning", title: "Failed Login Attempts", message: "3 failed login attempts detected for eve_sec", timestamp: "2026-03-18 08:15", read: false },
{ id: "NTF-005", type: "system", title: "Deployment Successful", message: "v2.1.0 deployed to production successfully", timestamp: "2026-03-15 12:00", read: true },
{ id: "NTF-006", type: "info", title: "New Package Available", message: "analytics v1.2.0 is available for installation", timestamp: "2026-03-14 10:30", read: true },
{ id: "NTF-007", type: "alert", title: "Disk Space Warning", message: "Primary storage at 87% capacity", timestamp: "2026-03-13 15:45", read: false },
{ id: "NTF-008", type: "system", title: "Database Backup Complete", message: "Nightly backup completed — 2.4 GB archived", timestamp: "2026-03-13 03:00", read: true },
{ id: "NTF-009", type: "info", title: "Welcome to MetaBuilder", message: "Your account has been set up successfully", timestamp: "2026-03-10 08:00", read: true },
{ id: "NTF-010", type: "warning", title: "Certificate Expiry", message: "TLS certificate expires in 14 days", timestamp: "2026-03-12 09:00", read: false }
]
// ── Helpers ──────────────────────────────────────────────────
function countUnread() {
var count = 0
for (var i = 0; i < notifications.length; i++) {
if (!notifications[i].read) count++
}
return count
}
function filteredNotifications() {
var result = []
for (var i = 0; i < notifications.length; i++) {
var n = notifications[i]
if (activeFilter === "All") {
result.push(n)
} else if (activeFilter === "System" && n.type === "system") {
result.push(n)
} else if (activeFilter === "Alerts" && (n.type === "alert" || n.type === "warning")) {
result.push(n)
} else if (activeFilter === "Info" && n.type === "info") {
result.push(n)
}
}
return result
}
function typeIcon(type) {
switch (type) {
case "system": return "\u2699" // gear
case "alert": return "\u26A0" // warning sign
case "warning": return "\u26A0" // warning sign
case "info": return "\u2139" // info
default: return "\u2709" // envelope
}
}
function typeColor(type) {
switch (type) {
case "system": return "#2196f3"
case "alert": return "#f44336"
case "warning": return "#ff9800"
case "info": return "#4caf50"
default: return "#9e9e9e"
}
}
function markAllRead() {
var updated = []
for (var i = 0; i < notifications.length; i++) {
var n = Object.assign({}, notifications[i])
n.read = true
updated.push(n)
}
notifications = updated
unreadCount = 0
}
function markRead(index) {
var updated = []
for (var i = 0; i < notifications.length; i++) {
var n = Object.assign({}, notifications[i])
if (i === index) n.read = true
updated.push(n)
}
notifications = updated
unreadCount = countUnread()
}
function dismissNotification(notifId) {
var updated = []
for (var i = 0; i < notifications.length; i++) {
if (notifications[i].id !== notifId) {
updated.push(notifications[i])
}
}
notifications = updated
unreadCount = countUnread()
}
function formatTimestamp(ts) {
// Show relative-style label for recent items
if (ts.indexOf("2026-03-18") === 0) return "Today " + ts.substring(11)
if (ts.indexOf("2026-03-17") === 0) return "Yesterday " + ts.substring(11)
return ts
}
// ── DBAL integration ─────────────────────────────────────────
function loadFromDBAL() {
if (!useLiveData) return
dbal.list("notification", { take: 50 }, function(result, error) {
if (error || !result) return
var items = result.items || []
var liveNotifs = []
for (var i = 0; i < items.length; i++) {
var item = items[i]
liveNotifs.push({
id: item.id || ("NTF-LIVE-" + i),
type: item.type || "info",
title: item.title || item.message || "Notification",
message: item.message || "",
timestamp: item.sent || item.created || "",
read: item.status === "Inactive"
})
}
if (liveNotifs.length > 0) {
notifications = liveNotifs
unreadCount = countUnread()
}
})
}
Component.onCompleted: {
if (useLiveData) loadFromDBAL()
}
onUseLiveDataChanged: {
if (useLiveData) loadFromDBAL()
}
// ── UI ───────────────────────────────────────────────────────
ScrollView {
anchors.fill: parent
anchors.margins: 24
clip: true
ColumnLayout {
width: parent.width
spacing: 16
// ── Header ──────────────────────────────────────────
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 12
CText {
variant: "h3"
text: "Notifications"
}
CBadge {
visible: unreadCount > 0
text: unreadCount + " unread"
}
Item { Layout.fillWidth: true }
CButton {
text: "Mark All Read"
variant: "ghost"
size: "sm"
enabled: unreadCount > 0
onClicked: markAllRead()
}
CButton {
text: dbal.loading ? "Loading..." : "Refresh"
variant: "ghost"
size: "sm"
enabled: !dbal.loading
onClicked: loadFromDBAL()
}
}
CDivider { Layout.fillWidth: true }
// ── Filter tabs ─────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: filters
delegate: CButton {
text: modelData
variant: activeFilter === modelData ? "primary" : "ghost"
size: "sm"
onClicked: activeFilter = modelData
}
}
}
}
}
// ── Notification list ───────────────────────────────
CCard {
Layout.fillWidth: true
visible: filteredNotifications().length > 0
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 0
Repeater {
model: filteredNotifications()
delegate: Rectangle {
Layout.fillWidth: true
height: notifContent.implicitHeight + 24
color: modelData.read ? "transparent" : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.04)
radius: 6
Rectangle {
id: typeStripe
width: 4
height: parent.height - 8
anchors.left: parent.left
anchors.leftMargin: 4
anchors.verticalCenter: parent.verticalCenter
radius: 2
color: typeColor(modelData.type)
}
RowLayout {
id: notifContent
anchors.fill: parent
anchors.leftMargin: 16
anchors.rightMargin: 12
anchors.topMargin: 12
anchors.bottomMargin: 12
spacing: 12
// Type icon circle
Rectangle {
width: 36
height: 36
radius: 18
color: Qt.rgba(typeColor(modelData.type).r, typeColor(modelData.type).g, typeColor(modelData.type).b, 0.15)
Layout.alignment: Qt.AlignTop
CText {
anchors.centerIn: parent
text: typeIcon(modelData.type)
variant: "body1"
}
}
// Content
ColumnLayout {
Layout.fillWidth: true
spacing: 4
FlexRow {
Layout.fillWidth: true
spacing: 8
CText {
variant: modelData.read ? "body1" : "subtitle1"
text: modelData.title
font.bold: !modelData.read
}
CBadge {
text: modelData.type
visible: true
}
Item { Layout.fillWidth: true }
CText {
variant: "caption"
text: formatTimestamp(modelData.timestamp)
opacity: 0.6
}
}
CText {
Layout.fillWidth: true
variant: "body2"
text: modelData.message
opacity: modelData.read ? 0.6 : 0.85
wrapMode: Text.WordWrap
}
}
// Actions
ColumnLayout {
Layout.alignment: Qt.AlignTop
spacing: 4
CButton {
visible: !modelData.read
text: "Read"
variant: "ghost"
size: "sm"
onClicked: {
// Find original index
for (var i = 0; i < notifications.length; i++) {
if (notifications[i].id === modelData.id) {
markRead(i)
break
}
}
}
}
CButton {
text: "Dismiss"
variant: "ghost"
size: "sm"
onClicked: dismissNotification(modelData.id)
}
}
}
// Bottom separator
CDivider {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
for (var i = 0; i < notifications.length; i++) {
if (notifications[i].id === modelData.id) {
markRead(i)
break
}
}
}
cursorShape: Qt.PointingHandCursor
}
}
}
}
}
// ── Empty state ─────────────────────────────────────
CCard {
Layout.fillWidth: true
visible: filteredNotifications().length === 0
ColumnLayout {
anchors.fill: parent
anchors.margins: 40
spacing: 16
Layout.alignment: Qt.AlignHCenter
CText {
Layout.alignment: Qt.AlignHCenter
variant: "h2"
text: "\u{1F514}"
}
CText {
Layout.alignment: Qt.AlignHCenter
variant: "h4"
text: activeFilter === "All" ? "No notifications" : "No " + activeFilter.toLowerCase() + " notifications"
}
CText {
Layout.alignment: Qt.AlignHCenter
variant: "body2"
text: "When there are new notifications, they will appear here."
opacity: 0.6
}
}
}
// ── Summary footer ──────────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 8
visible: notifications.length > 0
CText {
variant: "caption"
text: notifications.length + " total notifications"
opacity: 0.5
}
CText {
variant: "caption"
text: " \u00b7 "
opacity: 0.3
}
CText {
variant: "caption"
text: unreadCount + " unread"
opacity: 0.5
}
Item { Layout.fillWidth: true }
CText {
variant: "caption"
text: useLiveData ? "Live data" : "Mock data"
opacity: 0.4
}
}
// Bottom spacer
Item { Layout.preferredHeight: 20 }
}
}
}
+87 -7
View File
@@ -2,11 +2,16 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: Theme.background
// ── DBAL ──────────────────────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
property int selectedIndex: -1
property bool addDialogVisible: false
property bool deleteDialogVisible: false
@@ -19,6 +24,18 @@ Rectangle {
property var layoutOptions: ["default", "sidebar", "dashboard", "blank"]
property var levelOptions: [1, 2, 3, 4, 5]
property var mockRoutes: [
{ path: "/", title: "Home", level: 1, layout: "default", enabled: true, permissions: "public" },
{ path: "/dashboard", title: "Dashboard", level: 1, layout: "dashboard", enabled: true, permissions: "authenticated" },
{ path: "/admin", title: "Admin Panel", level: 3, layout: "sidebar", enabled: true, permissions: "role:admin" },
{ path: "/forum", title: "Forum", level: 1, layout: "sidebar", enabled: true, permissions: "authenticated" },
{ path: "/gallery", title: "Gallery", level: 1, layout: "default", enabled: true, permissions: "public" },
{ path: "/profile", title: "Profile", level: 1, layout: "sidebar", enabled: true, permissions: "authenticated" },
{ path: "/settings", title: "Settings", level: 2, layout: "sidebar", enabled: true, permissions: "authenticated" },
{ path: "/god-panel", title: "God Panel", level: 4, layout: "dashboard", enabled: true, permissions: "role:god" },
{ path: "/supergod", title: "Super God", level: 5, layout: "blank", enabled: false, permissions: "role:supergod" }
]
property var routes: [
{ path: "/", title: "Home", level: 1, layout: "default", enabled: true, permissions: "public" },
{ path: "/dashboard", title: "Dashboard", level: 1, layout: "dashboard", enabled: true, permissions: "authenticated" },
@@ -41,20 +58,28 @@ Rectangle {
selectedIndex = -1
selectedIndex = index
}
if (useLiveData) saveRoute(index)
}
function addRoute() {
if (newPath.length === 0 || newTitle.length === 0) return
var updated = routes.slice()
updated.push({
var newRoute = {
path: newPath,
title: newTitle,
level: newLevel,
layout: newLayout,
enabled: true,
permissions: "authenticated"
})
routes = updated
}
if (useLiveData) {
dbal.create("ui_page", newRoute, function(result, error) {
if (!error) loadRoutes()
})
} else {
var updated = routes.slice()
updated.push(newRoute)
routes = updated
}
newPath = ""
newTitle = ""
newLevel = 1
@@ -64,9 +89,15 @@ Rectangle {
function deleteRoute() {
if (selectedIndex < 0 || selectedIndex >= routes.length) return
var updated = routes.slice()
updated.splice(selectedIndex, 1)
routes = updated
if (useLiveData && routes[selectedIndex].id) {
dbal.remove("ui_page", routes[selectedIndex].id, function(result, error) {
if (!error) loadRoutes()
})
} else {
var updated = routes.slice()
updated.splice(selectedIndex, 1)
routes = updated
}
selectedIndex = -1
deleteDialogVisible = false
}
@@ -90,6 +121,55 @@ Rectangle {
return "#9c27b0"
}
// ── DBAL Integration ─────────────────────────────────────────────
function loadRoutes() {
dbal.list("ui_page", { take: 100 }, function(result, error) {
if (!error && result && result.items && result.items.length > 0) {
var parsed = []
for (var i = 0; i < result.items.length; i++) {
var r = result.items[i]
parsed.push({
id: r.id || undefined,
path: r.path || r.route || "",
title: r.title || r.name || "",
level: r.level || 1,
layout: r.layout || "default",
enabled: r.enabled !== undefined ? r.enabled : true,
permissions: r.permissions || "public"
})
}
routes = parsed
}
// On error or empty result, keep existing mock routes as fallback
})
}
onUseLiveDataChanged: {
if (useLiveData) loadRoutes()
}
Component.onCompleted: {
loadRoutes()
}
// ── CRUD wiring ──────────────────────────────────────────────────
function saveRoute(index) {
if (!useLiveData) return
var route = routes[index]
var data = { path: route.path, title: route.title, level: route.level, layout: route.layout, enabled: route.enabled, permissions: route.permissions }
if (route.id) {
dbal.update("ui_page", route.id, data, function(result, error) {
if (!error) loadRoutes()
})
} else {
dbal.create("ui_page", data, function(result, error) {
if (!error) loadRoutes()
})
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
+72 -6
View File
@@ -2,16 +2,83 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
color: "transparent"
property string userBio: "MetaBuilder enthusiast and open-source contributor."
property string userEmail: "demo@metabuilder.io"
// ── DBAL connection ──
DBALProvider { id: dbal }
// ── Mock fallback data ──
property string mockBio: "MetaBuilder enthusiast and open-source contributor."
property string mockEmail: "demo@metabuilder.io"
property string userBio: mockBio
property string userEmail: mockEmail
property string userDisplayName: appWindow.currentUser
property string currentPassword: ""
property string newPassword: ""
property string confirmPassword: ""
property bool saving: false
property string saveStatus: ""
// ── DBAL data loading ──
function loadProfile() {
if (!appWindow.currentUser) return;
dbal.read("user", appWindow.currentUser, function(result, error) {
if (result) {
if (result.bio) userBio = result.bio;
if (result.email) userEmail = result.email;
if (result.displayName) userDisplayName = result.displayName;
}
// On error, keep existing mock data
});
}
function saveProfile() {
saving = true;
saveStatus = "";
var profileData = {
displayName: userDisplayName,
email: userEmail,
bio: userBio
};
dbal.update("user", appWindow.currentUser, profileData, function(result, error) {
saving = false;
if (result) {
saveStatus = "saved";
console.log("Profile saved for", appWindow.currentUser);
} else {
saveStatus = "error";
console.warn("Profile save failed:", error);
}
});
}
function changePassword() {
if (newPassword !== confirmPassword) return;
dbal.execute("core/change-password", {
userId: appWindow.currentUser,
oldPassword: currentPassword,
newPassword: newPassword
}, function(result, error) {
if (result) {
currentPassword = "";
newPassword = "";
confirmPassword = "";
console.log("Password changed successfully");
} else {
console.warn("Password change failed:", error);
}
});
}
Component.onCompleted: {
dbal.ping(function(success) {
if (success) loadProfile();
});
}
function userInitials() {
var name = appWindow.currentUser
@@ -262,11 +329,10 @@ Rectangle {
spacing: 12
Item { Layout.fillWidth: true }
CButton {
text: "Save Changes"
text: saving ? "Saving..." : "Save Changes"
variant: "primary"
onClicked: {
console.log("Profile saved for", appWindow.currentUser)
}
enabled: !saving
onClicked: saveProfile()
}
}
+58 -40
View File
@@ -1,6 +1,6 @@
# Qt6 Frontend Roadmap
**Status**: Compiles and links (25 QML views, ~12,800 LOC)
**Status**: Compiles and links (26 QML views, ~14,500 LOC)
**Last Build**: 2026-03-19 | Qt 6.7.3 via Conan | MSVC 19.5 | C++20
---
@@ -34,7 +34,7 @@
- [x] Level 2: `ProfileView.qml` — Avatar, bio, password change, connected accounts
- [x] Level 2: `CommentsView.qml` — Post/like/delete, sort, role-based visibility
- [x] Level 3: `AdminView.qml` — 10 entities, CRUD dialogs, search, filter, pagination, bulk delete (871 LOC)
- [x] Level 4: `GodPanel.qml` — 13-tab builder container with config summary
- [x] Level 4: `GodPanel.qml` — 14-tab builder container with config summary
- [x] Level 5: `SuperGodPanel.qml` — Tenants, god users, power transfer, system health
### Phase 4: God Panel Builder Tools (15 Agents)
@@ -50,42 +50,49 @@
- [x] `ThemeEditor.qml` — 9 theme selector, color swatches, typography (876 LOC)
- [x] `SMTPConfigEditor.qml` — Server config, test send, email templates (632 LOC)
---
## In Progress
### Phase 5: DBAL Integration
- [ ] Register `DBALClient` as QML singleton type (currently context property)
- [ ] Wire `AdminView` entity table to real DBAL endpoints (`/{tenant}/{package}/{entity}`)
- [ ] Wire `SchemaEditor` to load from `dbal/shared/api/schema/entities/`
- [ ] Wire `UserManagement` to real User entity CRUD
- [ ] Wire `DashboardView` health cards to `/health`, `/version`, `/status`
- [ ] Add DBAL connection status indicator in app bar (ping on startup)
- [ ] Replace mock data in all editors with `DBALProvider.list/create/update/remove` calls
- [x] Register `DBALClient` as QML context property in `main.cpp`
- [x] Migrate to DBAL REST API: `/api/v1/{tenant}/{package}/{entity}[/{id}]`
- [x] Add `packageId` property to DBALClient (C++ + QML), default `"core"`
- [x] Wire `AdminView` entity table to DBAL REST endpoints with mock fallback
- [x] Wire `SchemaEditor` to load schemas from DBAL with mock fallback
- [x] Wire `UserManagement` to real User entity CRUD with mock fallback
- [x] Wire `DashboardView` health cards to `/health` endpoint
- [x] Add DBAL connection status indicator in app bar (green/red dot + "DBAL")
- [x] Add DBAL offline banner below app bar ("DBAL Offline — showing cached data")
- [x] Add `health()`, `version()`, `status()`, `listSchemas()`, `getSchema()` to C++ DBALClient
- [x] `DBALProvider.qml` — REST-based QML HTTP client with `entityPath()` helpers
### Phase 6: Build System (Python + stdlib)
- [x] Create `generate_cmake.py` — zero-dependency script (Python stdlib only)
- Globs all `*.qml` files automatically (root, qmllib/, packages/)
- Reads `metadata.json` from each package for auto-registration
- Discovers `src/*.cpp` and `src/*.h` for C++ sources
- Handles SVG/audio/resource globbing
- Supports conditional features (libopenmpt, Qt Multimedia)
- [x] Create `cmake_config.json` defining modules, dependencies, feature flags
- [x] `--dry-run` mode to preview generated CMakeLists.txt
- [x] `--output` and `--config` CLI options
### Phase 7: Runtime Polish
- [x] Dark/light theme switching (toggle button in app bar)
- [x] Keyboard shortcuts (Ctrl+K search, Ctrl+L login/logout, Ctrl+1-5 level switch, Escape back)
- [x] Window state persistence via `Qt.labs.settings` (size, position, theme)
- [x] Error boundary — DBAL offline banner with warning styling
### Phase 4.5: Media Service Integration
- [x] `MediaServicePanel.qml` — 4-tab media service management (~730 LOC)
- Jobs tab: submission form, active jobs table, progress bars, cancel
- Radio tab: channel management, playlists, start/stop streaming
- TV tab: channel scheduling, multi-resolution, broadcast controls
- Plugins tab: FFmpeg/ImageMagick/Pandoc/Radio/LibRetro grid with reload
- [x] Integrated into GodPanel as tab 12 (14 total tabs)
- [x] Separate HTTP client for media service at `http://localhost:8090`
---
## Planned
### Phase 6: Build System (Python + Jinja2 + JSON + GLOB)
- [ ] Create `generate_cmake.py` script that:
- Globs all `*.qml` files automatically (no manual CMakeLists.txt maintenance)
- Reads `metadata.json` from each package for auto-registration
- Templates `CMakeLists.txt` via Jinja2 from `cmake_config.json`
- Handles SVG/audio/resource globbing
- Supports conditional features (libopenmpt, Qt Multimedia)
- [ ] Create `cmake_config.json` defining modules, dependencies, feature flags
- [ ] Add `--dry-run` mode to preview generated CMakeLists.txt
- [ ] Integrate into pre-commit or CI
### Phase 7: Runtime Polish
- [ ] Dark/light theme switching (Theme singleton already supports 9 themes)
- [ ] i18n integration (LanguageContext from shared `/qml/` — 19 languages ready)
- [ ] Responsive layout (Responsive singleton from shared `/qml/`)
- [ ] Keyboard shortcuts (Ctrl+K search, Ctrl+L login/logout)
- [ ] Window state persistence (size, position, last view)
- [ ] Error boundary / graceful degradation when DBAL is offline
### Phase 8: Package System
- [ ] Dynamic package view loading from disk (PackageViewLoader → real file resolution)
- [ ] Package install/uninstall with metadata validation
@@ -111,18 +118,21 @@
```
App.qml (ApplicationWindow)
├── CAppBar (Level navigation + auth)
├── CAppBar (Level nav + auth + DBAL status + theme toggle)
├── DBAL Offline Banner (conditional warning strip)
├── Sidebar (CListItem navigation, level-gated)
├── Settings (Qt.labs.settings — window size/position/theme persistence)
├── Shortcuts (Ctrl+K/L/1-5, Escape)
└── StackLayout (17 views)
├── FrontPage (Level 1 - Public)
├── LoginView (Auth)
├── DashboardView (Level 2 - User)
├── DashboardView (Level 2 - User, DBAL health)
├── ProfileView (Level 2)
├── CommentsView (Level 2)
├── PackageViewLoader×6 (Level 2 - Forum, Gallery, etc.)
├── AdminView (Level 3 - Django CRUD)
├── GodPanel (Level 4 - 13-tab builder)
│ ├── SchemaEditor
├── AdminView (Level 3 - Django CRUD, DBAL REST)
├── GodPanel (Level 4 - 14-tab builder)
│ ├── SchemaEditor (DBAL REST)
│ ├── WorkflowEditor
│ ├── LuaEditor
│ ├── DatabaseManager
@@ -130,9 +140,10 @@ App.qml (ApplicationWindow)
│ ├── ComponentHierarchyEditor
│ ├── CssClassManager
│ ├── DropdownConfigManager
│ ├── UserManagement
│ ├── UserManagement (DBAL REST)
│ ├── ThemeEditor
── SMTPConfigEditor
── SMTPConfigEditor
│ └── MediaServicePanel (Media Daemon REST)
├── PackageManager (Level 4)
├── Storybook (Level 4)
└── SuperGodPanel (Level 5 - Tenants + Power Transfer)
@@ -140,7 +151,14 @@ App.qml (ApplicationWindow)
C++ Backend
├── PackageRegistry (JSON metadata loader)
├── ModPlayer (stub — libopenmpt pending)
└── DBALClient (HTTP client → DBAL daemon)
└── DBALClient (REST client → DBAL daemon :8080)
├── CRUD: /api/v1/{tenant}/{package}/{entity}[/{id}]
├── System: /health, /version, /status
└── Schema: /api/v1/{tenant}/schema[/{entity}]
Build System
├── generate_cmake.py (auto-generates CMakeLists.txt from file globs)
└── cmake_config.json (project config, Qt components, feature flags)
Shared: /qml/ QmlComponents 1.0 (119 components, 9 themes, 19 languages)
```
+75 -6
View File
@@ -2,10 +2,15 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
color: Theme.background
// ── DBAL ──────────────────────────────────────────────────────────
DBALProvider { id: dbal }
// ── State ──────────────────────────────────────────────────────────
property int selectedSchemaIndex: 0
@@ -106,6 +111,50 @@ Rectangle {
property var fieldTypes: ["string", "integer", "number", "boolean", "text", "json", "enum", "datetime", "date", "uuid", "array"]
property var mockSchemas: JSON.parse(JSON.stringify(schemas))
// ── DBAL Integration ─────────────────────────────────────────────
function loadSchemas() {
dbal.execute("core/schema", {}, function(result, error) {
if (!error && result && result.items) {
var parsed = []
for (var i = 0; i < result.items.length; i++) {
var item = result.items[i]
var fields = []
if (item.fields) {
for (var j = 0; j < item.fields.length; j++) {
var f = item.fields[j]
fields.push({
name: f.name || "",
type: f.type || "string",
required: f.required || false,
defaultValue: f.defaultValue || f["default"] || "",
description: f.description || ""
})
}
}
parsed.push({
name: item.name || "",
description: item.description || "",
fields: fields
})
}
if (parsed.length > 0) {
schemas = parsed
selectedSchemaIndex = 0
selectedFieldIndex = -1
}
// If parsed is empty, keep existing mock schemas as fallback
}
// On error, keep existing mock schemas as fallback
})
}
Component.onCompleted: {
loadSchemas()
}
// ── Helpers ────────────────────────────────────────────────────────
function currentSchema() {
@@ -132,22 +181,42 @@ Rectangle {
function addSchema() {
if (newSchemaName.trim() === "") return
var copy = JSON.parse(JSON.stringify(schemas))
copy.push({
var schemaData = {
name: newSchemaName.trim(),
description: newSchemaDescription.trim(),
fields: [
{ name: "id", type: "string", required: true, defaultValue: "uuid()", description: "Primary key" }
]
})
schemas = copy
selectedSchemaIndex = copy.length - 1
selectedFieldIndex = -1
}
// POST to DBAL when connected, then update local state
if (dbal.connected) {
dbal.create("schema", schemaData, function(result, error) {
if (!error) {
// Reload from server to stay in sync
loadSchemas()
} else {
// Fallback: add locally
addSchemaLocally(schemaData)
}
})
} else {
addSchemaLocally(schemaData)
}
newSchemaName = ""
newSchemaDescription = ""
createSchemaDialogOpen = false
}
function addSchemaLocally(schemaData) {
var copy = JSON.parse(JSON.stringify(schemas))
copy.push(schemaData)
schemas = copy
selectedSchemaIndex = copy.length - 1
selectedFieldIndex = -1
}
function deleteSchema() {
if (schemas.length <= 1) return
var copy = JSON.parse(JSON.stringify(schemas))
+580
View File
@@ -0,0 +1,580 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: "transparent"
// ── DBAL connection ──────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
// ── Profile state ────────────────────────────────────────────
property string displayName: appWindow.currentUser
property string userEmail: appWindow.currentUser + "@metabuilder.io"
property bool profileSaved: false
// ── Appearance state ─────────────────────────────────────────
property var availableThemes: [
{ id: "dark", label: "Dark" },
{ id: "light", label: "Light" },
{ id: "midnight", label: "Midnight" },
{ id: "solarized", label: "Solarized" },
{ id: "nord", label: "Nord" },
{ id: "dracula", label: "Dracula" },
{ id: "monokai", label: "Monokai" },
{ id: "github", label: "GitHub" },
{ id: "high-contrast", label: "High Contrast" }
]
property string selectedTheme: appWindow.currentTheme
property string fontSize: "medium"
// ── Notification preferences ─────────────────────────────────
property bool emailNotifications: true
property bool desktopNotifications: true
property bool soundAlerts: false
// ── Connection state ─────────────────────────────────────────
property string dbalUrl: dbal.baseUrl
property string mediaServiceUrl: "http://localhost:9090"
property string dbalConnectionStatus: dbal.connected ? "connected" : "disconnected"
property string mediaConnectionStatus: "unknown"
// ── Helpers ──────────────────────────────────────────────────
function userInitials() {
var name = appWindow.currentUser
if (!name || name.length === 0) return "??"
var parts = name.split(" ")
if (parts.length >= 2)
return (parts[0][0] + parts[1][0]).toUpperCase()
return name.substring(0, 2).toUpperCase()
}
function saveProfile() {
if (useLiveData) {
dbal.update("user", appWindow.currentUser, {
displayName: displayName,
email: userEmail
}, function(result, error) {
if (!error) {
profileSaved = true
profileSavedTimer.restart()
}
})
} else {
profileSaved = true
profileSavedTimer.restart()
console.log("[Settings] Profile saved (offline):", displayName, userEmail)
}
}
function savePreferences() {
if (useLiveData) {
dbal.update("user", appWindow.currentUser, {
preferences: {
theme: selectedTheme,
fontSize: fontSize,
emailNotifications: emailNotifications,
desktopNotifications: desktopNotifications,
soundAlerts: soundAlerts
}
}, function(result, error) {
if (!error) console.log("[Settings] Preferences saved to DBAL")
})
}
}
function testDBALConnection() {
dbalConnectionStatus = "testing"
dbal.baseUrl = dbalUrl
dbal.ping(function(success, error) {
dbalConnectionStatus = success ? "connected" : "disconnected"
})
}
function testMediaConnection() {
mediaConnectionStatus = "testing"
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
mediaConnectionStatus = (xhr.status >= 200 && xhr.status < 300) ? "connected" : "disconnected"
}
}
xhr.open("GET", mediaServiceUrl + "/health")
xhr.send()
}
function connectionStatusColor(status) {
switch (status) {
case "connected": return "success"
case "disconnected": return "error"
case "testing": return "warning"
default: return "info"
}
}
function connectionStatusLabel(status) {
switch (status) {
case "connected": return "Connected"
case "disconnected": return "Disconnected"
case "testing": return "Testing..."
default: return "Unknown"
}
}
// ── Load preferences from DBAL ───────────────────────────────
function loadPreferences() {
if (!useLiveData) return
dbal.findFirst("user", { username: appWindow.currentUser }, function(result, error) {
if (error || !result) return
var items = result.items || []
if (items.length === 0) return
var user = items[0]
if (user.displayName) displayName = user.displayName
if (user.email) userEmail = user.email
if (user.preferences) {
if (user.preferences.theme) selectedTheme = user.preferences.theme
if (user.preferences.fontSize) fontSize = user.preferences.fontSize
if (user.preferences.emailNotifications !== undefined) emailNotifications = user.preferences.emailNotifications
if (user.preferences.desktopNotifications !== undefined) desktopNotifications = user.preferences.desktopNotifications
if (user.preferences.soundAlerts !== undefined) soundAlerts = user.preferences.soundAlerts
}
})
}
Component.onCompleted: {
if (useLiveData) loadPreferences()
}
onUseLiveDataChanged: {
if (useLiveData) loadPreferences()
}
Timer {
id: profileSavedTimer
interval: 3000
onTriggered: profileSaved = false
}
// ── UI ───────────────────────────────────────────────────────
ScrollView {
anchors.fill: parent
anchors.margins: 24
clip: true
ColumnLayout {
width: parent.width
spacing: 20
// Page title
CText {
variant: "h3"
text: "Settings"
}
// ── Profile Section ─────────────────────────────────
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
CText { variant: "h4"; text: "Profile" }
CDivider { Layout.fillWidth: true }
FlexRow {
Layout.fillWidth: true
spacing: 16
// Avatar
Rectangle {
width: 64
height: 64
radius: 32
color: Theme.primary
Layout.alignment: Qt.AlignTop
CText {
anchors.centerIn: parent
text: userInitials()
variant: "h4"
color: "#ffffff"
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
CText {
variant: "subtitle1"
text: appWindow.currentUser
font.bold: true
}
CText {
variant: "body2"
text: appWindow.currentRole + " \u00b7 Level " + appWindow.currentLevel
opacity: 0.7
}
}
}
CTextField {
Layout.fillWidth: true
label: "Display Name"
placeholderText: "Enter display name"
text: displayName
onTextChanged: displayName = text
}
CTextField {
Layout.fillWidth: true
label: "Email"
placeholderText: "Enter email address"
text: userEmail
onTextChanged: userEmail = text
}
FlexRow {
Layout.fillWidth: true
spacing: 12
Item { Layout.fillWidth: true }
CAlert {
visible: profileSaved
severity: "success"
text: "Profile saved successfully"
}
CButton {
text: "Save Profile"
variant: "primary"
onClicked: saveProfile()
}
}
}
}
// ── Appearance Section ──────────────────────────────
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
CText { variant: "h4"; text: "Appearance" }
CDivider { Layout.fillWidth: true }
// Theme selector
CText {
variant: "subtitle2"
text: "Theme"
}
Flow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: availableThemes
delegate: CButton {
text: modelData.label
variant: selectedTheme === modelData.id ? "primary" : "default"
size: "sm"
onClicked: {
selectedTheme = modelData.id
appWindow.currentTheme = modelData.id
if (typeof Theme.setTheme === "function") {
Theme.setTheme(modelData.id)
}
savePreferences()
}
}
}
}
// Font size selector
CText {
variant: "subtitle2"
text: "Font Size"
Layout.topMargin: 8
}
FlexRow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: [
{ id: "small", label: "Small" },
{ id: "medium", label: "Medium" },
{ id: "large", label: "Large" }
]
delegate: CButton {
text: modelData.label
variant: fontSize === modelData.id ? "primary" : "default"
size: "sm"
onClicked: {
fontSize = modelData.id
savePreferences()
}
}
}
}
}
}
// ── Notifications Section ───────────────────────────
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
CText { variant: "h4"; text: "Notifications" }
CDivider { Layout.fillWidth: true }
// Email notifications toggle
FlexRow {
Layout.fillWidth: true
spacing: 12
ColumnLayout {
Layout.fillWidth: true
spacing: 2
CText { variant: "subtitle2"; text: "Email Notifications" }
CText { variant: "caption"; text: "Receive notification summaries via email"; opacity: 0.6 }
}
Switch {
checked: emailNotifications
onCheckedChanged: {
emailNotifications = checked
savePreferences()
}
}
}
CDivider { Layout.fillWidth: true }
// Desktop notifications toggle
FlexRow {
Layout.fillWidth: true
spacing: 12
ColumnLayout {
Layout.fillWidth: true
spacing: 2
CText { variant: "subtitle2"; text: "Desktop Notifications" }
CText { variant: "caption"; text: "Show desktop push notifications for alerts"; opacity: 0.6 }
}
Switch {
checked: desktopNotifications
onCheckedChanged: {
desktopNotifications = checked
savePreferences()
}
}
}
CDivider { Layout.fillWidth: true }
// Sound alerts toggle
FlexRow {
Layout.fillWidth: true
spacing: 12
ColumnLayout {
Layout.fillWidth: true
spacing: 2
CText { variant: "subtitle2"; text: "Sound Alerts" }
CText { variant: "caption"; text: "Play a sound when new notifications arrive"; opacity: 0.6 }
}
Switch {
checked: soundAlerts
onCheckedChanged: {
soundAlerts = checked
savePreferences()
}
}
}
}
}
// ── Connection Section ──────────────────────────────
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
CText { variant: "h4"; text: "Connection" }
CDivider { Layout.fillWidth: true }
// DBAL URL
CText { variant: "subtitle2"; text: "DBAL Server" }
FlexRow {
Layout.fillWidth: true
spacing: 12
CTextField {
Layout.fillWidth: true
label: "DBAL URL"
placeholderText: "http://localhost:8080"
text: dbalUrl
onTextChanged: dbalUrl = text
}
ColumnLayout {
spacing: 4
Layout.alignment: Qt.AlignBottom
CButton {
text: dbalConnectionStatus === "testing" ? "Testing..." : "Test Connection"
variant: "default"
size: "sm"
enabled: dbalConnectionStatus !== "testing"
onClicked: testDBALConnection()
}
CStatusBadge {
status: connectionStatusColor(dbalConnectionStatus)
text: connectionStatusLabel(dbalConnectionStatus)
}
}
}
CDivider { Layout.fillWidth: true }
// Media Service URL
CText { variant: "subtitle2"; text: "Media Service" }
FlexRow {
Layout.fillWidth: true
spacing: 12
CTextField {
Layout.fillWidth: true
label: "Media Service URL"
placeholderText: "http://localhost:9090"
text: mediaServiceUrl
onTextChanged: mediaServiceUrl = text
}
ColumnLayout {
spacing: 4
Layout.alignment: Qt.AlignBottom
CButton {
text: mediaConnectionStatus === "testing" ? "Testing..." : "Test Connection"
variant: "default"
size: "sm"
enabled: mediaConnectionStatus !== "testing"
onClicked: testMediaConnection()
}
CStatusBadge {
status: connectionStatusColor(mediaConnectionStatus)
text: connectionStatusLabel(mediaConnectionStatus)
}
}
}
}
}
// ── About Section ───────────────────────────────────
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
CText { variant: "h4"; text: "About" }
CDivider { Layout.fillWidth: true }
Repeater {
model: [
{ label: "Version", value: "2.1.0" },
{ label: "Build Date", value: "2026-03-19" },
{ label: "Qt Version", value: "6.8.x" },
{ label: "Platform", value: Qt.platform.os },
{ label: "DBAL Schema", value: "v1 REST API" }
]
delegate: FlexRow {
Layout.fillWidth: true
spacing: 12
CText {
variant: "body2"
text: modelData.label
opacity: 0.6
Layout.preferredWidth: 120
}
CText {
variant: "body1"
text: modelData.value
}
}
}
CDivider { Layout.fillWidth: true }
FlexRow {
Layout.fillWidth: true
spacing: 12
CButton {
text: "View Documentation"
variant: "default"
size: "sm"
onClicked: Qt.openUrlExternally("https://github.com/nicholasgriffintn/metabuilder")
}
CButton {
text: "Report Issue"
variant: "ghost"
size: "sm"
onClicked: Qt.openUrlExternally("https://github.com/nicholasgriffintn/metabuilder/issues")
}
}
}
}
// ── Data status footer ──────────────────────────────
FlexRow {
Layout.fillWidth: true
spacing: 8
CText {
variant: "caption"
text: useLiveData ? "Connected to DBAL — preferences synced" : "Offline — preferences stored locally"
opacity: 0.4
}
}
// Bottom spacer
Item { Layout.preferredHeight: 20 }
}
}
}
+92 -15
View File
@@ -2,14 +2,41 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: superGodPanel
color: "transparent"
// ── DBAL connection ──
DBALProvider { id: dbal }
property bool dbalOnline: dbal.connected
property int currentTab: 0
// ── Tenant data ──
// ── Mock fallback data ──
property var mockTenants: [
{ name: "default", owner: "admin", status: "active", homepage: "/", created: "2025-01-15" },
{ name: "staging", owner: "devops", status: "active", homepage: "/staging", created: "2025-06-01" },
{ name: "production", owner: "platform-admin", status: "active", homepage: "/prod", created: "2025-08-20" }
]
property var mockGodUsers: [
{ username: "admin", initials: "AD", role: "supergod", level: 5, tenant: "default", status: "online" },
{ username: "platform-admin", initials: "PA", role: "supergod", level: 5, tenant: "production", status: "online" },
{ username: "devops", initials: "DO", role: "god", level: 4, tenant: "staging", status: "online" },
{ username: "builder01", initials: "B1", role: "god", level: 4, tenant: "default", status: "offline" },
{ username: "builder02", initials: "B2", role: "god", level: 4, tenant: "production", status: "away" }
]
property var mockDaemons: [
{ name: "DBAL", status: "healthy", uptime: "14d 7h 32m", port: 8080 },
{ name: "Nginx", status: "healthy", uptime: "14d 7h 30m", port: 443 },
{ name: "PostgreSQL", status: "healthy", uptime: "14d 6h 55m", port: 5432 },
{ name: "Redis", status: "degraded", uptime: "2d 1h 12m", port: 6379 }
]
property var mockSystemMetrics: ({ cpu: 34, memory: 62, disk: 47, network: 18 })
// ── Live data (falls back to mock) ──
property var tenants: [
{ name: "default", owner: "admin", status: "active", homepage: "/", created: "2025-01-15" },
{ name: "staging", owner: "devops", status: "active", homepage: "/staging", created: "2025-06-01" },
@@ -21,13 +48,7 @@ Rectangle {
property string newTenantHomepage: ""
// ── God users data ──
property var godUsers: [
{ username: "admin", initials: "AD", role: "supergod", level: 5, tenant: "default", status: "online" },
{ username: "platform-admin", initials: "PA", role: "supergod", level: 5, tenant: "production", status: "online" },
{ username: "devops", initials: "DO", role: "god", level: 4, tenant: "staging", status: "online" },
{ username: "builder01", initials: "B1", role: "god", level: 4, tenant: "default", status: "offline" },
{ username: "builder02", initials: "B2", role: "god", level: 4, tenant: "production", status: "away" }
]
property var godUsers: mockGodUsers
// ── Power transfer data ──
property bool showTransferForm: false
@@ -45,13 +66,8 @@ Rectangle {
]
// ── System data ──
property var daemons: [
{ name: "DBAL", status: "healthy", uptime: "14d 7h 32m", port: 8080 },
{ name: "Nginx", status: "healthy", uptime: "14d 7h 30m", port: 443 },
{ name: "PostgreSQL", status: "healthy", uptime: "14d 6h 55m", port: 5432 },
{ name: "Redis", status: "degraded", uptime: "2d 1h 12m", port: 6379 }
]
property var systemMetrics: ({ cpu: 34, memory: 62, disk: 47, network: 18 })
property var daemons: mockDaemons
property var systemMetrics: mockSystemMetrics
property bool showReseedDialog: false
property bool showClearCacheDialog: false
property bool showRestartDialog: false
@@ -64,6 +80,67 @@ Rectangle {
{ label: "System" }
]
// ── DBAL data loading ──
function loadTenants() {
dbal.list("tenant", { take: 20 }, function(result, error) {
if (result && result.items) {
tenants = result.items;
} else {
tenants = mockTenants;
}
});
}
function loadGodUsers() {
dbal.list("user", { take: 50 }, function(result, error) {
if (result && result.items) {
var gods = [];
for (var i = 0; i < result.items.length; i++) {
var u = result.items[i];
if (u.role === "god" || u.role === "supergod") {
gods.push(u);
}
}
godUsers = gods.length > 0 ? gods : mockGodUsers;
} else {
godUsers = mockGodUsers;
}
});
}
function loadSystemHealth() {
dbal.execute("core/status", {}, function(result, error) {
if (result) {
if (result.daemons) daemons = result.daemons;
if (result.metrics) systemMetrics = result.metrics;
} else {
daemons = mockDaemons;
systemMetrics = mockSystemMetrics;
}
});
}
function executePowerTransfer(tenantId, newOwnerId) {
dbal.update("tenant", tenantId, { owner: newOwnerId }, function(result, error) {
if (result) {
console.log("Power transfer completed for tenant:", tenantId);
loadTenants();
} else {
console.warn("Power transfer failed:", error);
}
});
}
Component.onCompleted: {
dbal.ping(function(success) {
if (success) {
loadTenants();
loadGodUsers();
loadSystemHealth();
}
});
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
+120 -13
View File
@@ -2,11 +2,18 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: Theme.background
// ── DBAL ──────────────────────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
// ── Local state ──────────────────────────────────────────────────
property var users: [
{ uid: 1, username: "demo", email: "demo@metabuilder.dev", role: "user", level: 2, status: "active", created: "2025-11-02" },
@@ -35,6 +42,41 @@ Rectangle {
readonly property var roles: ["user", "admin", "god", "supergod"]
property var mockUsers: JSON.parse(JSON.stringify(users))
// ── DBAL Integration ─────────────────────────────────────────────
function loadUsers() {
dbal.list("user", { take: 50 }, function(result, error) {
if (!error && result && result.items && result.items.length > 0) {
var parsed = []
for (var i = 0; i < result.items.length; i++) {
var u = result.items[i]
parsed.push({
uid: u.id || u.uid || (i + 1),
username: u.username || "",
email: u.email || "",
role: u.role || "user",
level: levelForRole(u.role || "user"),
status: u.status || "active",
created: u.createdAt ? u.createdAt.slice(0, 10) : (u.created || "")
})
}
users = parsed
nextUid = parsed.length + 1
}
// On error or empty result, keep existing mock users as fallback
})
}
onUseLiveDataChanged: {
if (useLiveData) loadUsers()
}
Component.onCompleted: {
loadUsers()
}
// ── Helpers ──────────────────────────────────────────────────────
function initials(name) {
var parts = name.split("")
@@ -105,45 +147,110 @@ Rectangle {
function createUser() {
if (formUsername === "" || formEmail === "") return
var newUser = {
uid: nextUid,
var userData = {
username: formUsername,
email: formEmail,
role: formRole,
level: levelForRole(formRole),
status: formActive ? "active" : "inactive",
status: formActive ? "active" : "inactive"
}
if (useLiveData) {
dbal.create("user", userData, function(result, error) {
if (!error) {
loadUsers()
} else {
createUserLocally(userData)
}
createDialogOpen = false
clearForm()
})
} else {
createUserLocally(userData)
createDialogOpen = false
clearForm()
}
}
function createUserLocally(userData) {
var newUser = {
uid: nextUid,
username: userData.username,
email: userData.email,
role: userData.role,
level: levelForRole(userData.role),
status: userData.status,
created: new Date().toISOString().slice(0, 10)
}
var copy = users.slice()
copy.push(newUser)
users = copy
nextUid++
createDialogOpen = false
clearForm()
}
function saveEdit() {
if (editIndex < 0) return
var copy = users.slice()
copy[editIndex] = Object.assign({}, copy[editIndex], {
var userData = {
username: formUsername,
email: formEmail,
role: formRole,
level: levelForRole(formRole),
status: formActive ? "active" : "inactive"
}
if (useLiveData) {
var userId = users[editIndex].uid
dbal.update("user", userId, userData, function(result, error) {
if (!error) {
loadUsers()
} else {
saveEditLocally(userData)
}
editDialogOpen = false
clearForm()
})
} else {
saveEditLocally(userData)
editDialogOpen = false
clearForm()
}
}
function saveEditLocally(userData) {
var copy = users.slice()
copy[editIndex] = Object.assign({}, copy[editIndex], {
username: userData.username,
email: userData.email,
role: userData.role,
level: levelForRole(userData.role),
status: userData.status
})
users = copy
editDialogOpen = false
clearForm()
}
function confirmDelete() {
if (deleteIndex < 0) return
if (useLiveData) {
var userId = users[deleteIndex].uid
dbal.remove("user", userId, function(result, error) {
if (!error) {
loadUsers()
} else {
confirmDeleteLocally()
}
deleteDialogOpen = false
deleteIndex = -1
})
} else {
confirmDeleteLocally()
deleteDialogOpen = false
deleteIndex = -1
}
}
function confirmDeleteLocally() {
var copy = users.slice()
copy.splice(deleteIndex, 1)
users = copy
deleteDialogOpen = false
deleteIndex = -1
}
// ── Main layout ──────────────────────────────────────────────────
+110 -5
View File
@@ -2,11 +2,93 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
import "qmllib/dbal"
Rectangle {
id: root
color: "transparent"
// ── DBAL connection ──────────────────────────────────────────
DBALProvider { id: dbal }
property bool useLiveData: dbal.connected
// ── Mock workflow data kept as fallback ───────────────────────
property var mockWorkflows: JSON.parse(JSON.stringify(workflows))
function loadWorkflows() {
dbal.list("workflow", { take: 50 }, function(result, error) {
if (!error && result && result.items && result.items.length > 0) {
var parsed = []
for (var i = 0; i < result.items.length; i++) {
var w = result.items[i]
parsed.push({
id: w.id || "",
name: w.name || "unnamed_workflow",
enabled: w.enabled !== undefined ? w.enabled : true,
nodes: w.nodes || []
})
}
workflows = parsed
if (selectedWorkflowIndex >= parsed.length)
selectedWorkflowIndex = 0
}
// On error or empty result, keep existing mock workflows as fallback
})
}
function saveWorkflow(wf, callback) {
var workflowData = { name: wf.name, enabled: wf.enabled, nodes: wf.nodes }
if (useLiveData) {
if (wf.id) {
dbal.update("workflow", wf.id, workflowData, function(result, error) {
if (!error) loadWorkflows()
if (callback) callback(result, error)
})
} else {
dbal.create("workflow", workflowData, function(result, error) {
if (!error) loadWorkflows()
if (callback) callback(result, error)
})
}
}
}
function deleteWorkflow(index) {
var wf = workflows[index]
if (useLiveData && wf.id) {
dbal.remove("workflow", wf.id, function(result, error) {
if (!error) {
loadWorkflows()
if (selectedWorkflowIndex >= workflows.length - 1)
selectedWorkflowIndex = Math.max(0, workflows.length - 2)
selectedNodeIndex = -1
} else {
deleteWorkflowLocally(index)
}
})
} else {
deleteWorkflowLocally(index)
}
}
function deleteWorkflowLocally(index) {
var copy = workflows.slice()
copy.splice(index, 1)
workflows = copy
if (selectedWorkflowIndex >= copy.length)
selectedWorkflowIndex = Math.max(0, copy.length - 1)
selectedNodeIndex = -1
}
onUseLiveDataChanged: {
if (useLiveData) loadWorkflows()
}
Component.onCompleted: {
loadWorkflows()
}
// ── State ──────────────────────────────────────────────────────────
property int selectedWorkflowIndex: 0
property int selectedNodeIndex: -1
@@ -170,6 +252,7 @@ Rectangle {
config: "// configure " + addNodeName
})
workflows = workflows // trigger re-bind
if (useLiveData) saveWorkflow(wf)
addNodeName = ""
addNodeType = "Action"
addNodeDialog.close()
@@ -236,6 +319,11 @@ Rectangle {
accent: currentWorkflow.enabled
}
CBadge {
text: useLiveData ? "Live" : "Mock"
color: useLiveData ? Theme.success : Theme.warning
}
Item { Layout.fillWidth: true }
CSwitch {
@@ -243,6 +331,7 @@ Rectangle {
onCheckedChanged: {
workflows[selectedWorkflowIndex].enabled = checked
workflows = workflows
if (useLiveData) saveWorkflow(workflows[selectedWorkflowIndex])
}
}
@@ -257,11 +346,26 @@ Rectangle {
{ type: "Trigger", name: "StartEvent", config: "event: custom.event" }
]
}
var wfs = workflows.slice()
wfs.push(newWf)
workflows = wfs
selectedWorkflowIndex = wfs.length - 1
selectedNodeIndex = -1
if (useLiveData) {
dbal.create("workflow", newWf, function(result, error) {
if (!error) {
loadWorkflows()
} else {
// Fallback to local
var wfs = workflows.slice()
wfs.push(newWf)
workflows = wfs
selectedWorkflowIndex = wfs.length - 1
selectedNodeIndex = -1
}
})
} else {
var wfs = workflows.slice()
wfs.push(newWf)
workflows = wfs
selectedWorkflowIndex = wfs.length - 1
selectedNodeIndex = -1
}
}
}
@@ -463,6 +567,7 @@ Rectangle {
wf.nodes.splice(index, 1)
workflows = workflows
selectedNodeIndex = -1
if (useLiveData) saveWorkflow(wf)
}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"project": {
"name": "dbal_qml",
"version": "0.1",
"executable": "dbal-qml"
},
"qt": {
"version": "6.7.3",
"components": ["Core", "Gui", "Quick", "Qml", "Network"]
},
"cpp": {
"standard": 20
},
"qml": {
"uri": "DBALObservatory",
"version": "1.0"
},
"features": {
"libopenmpt": false,
"qt_multimedia": false
},
"compile_definitions": {
"SRCDIR": "${CMAKE_CURRENT_SOURCE_DIR}"
}
}
-1
View File
@@ -11,7 +11,6 @@ qt/*:qtdeclarative=True
qt/*:qtshadertools=True
[tool_requires]
cmake/3.30.0
ninja/1.12.1
[layout]
+102
View File
@@ -0,0 +1,102 @@
# Qt6 Dev Stack — lightweight DBAL + Media for local Qt6 development
#
# Usage:
# docker compose -f docker-compose.dev.yml up -d
# docker compose -f docker-compose.dev.yml --profile media up -d
#
# DBAL runs with SQLite in-memory (no Postgres/Redis/ES needed)
# Media daemon is optional (--profile media)
services:
# Copy schema/seed/template files into volumes
dbal-init:
image: alpine:3.19
container_name: qt6-dbal-init
command: >
sh -c "
cp -r /src/entities/* /schemas/ 2>/dev/null || true;
cp -r /src/templates/* /templates/ 2>/dev/null || true;
cp -r /src/seeds/* /seeds/ 2>/dev/null || true;
echo 'Init complete: schemas, templates, seeds copied';
"
working_dir: /src
volumes:
- ../../dbal/shared/api/schema/entities:/src/entities:ro
- ../../dbal/production/templates/sql:/src/templates:ro
- ../../dbal/shared/seeds/database:/src/seeds:ro
- dbal-schemas:/schemas
- dbal-templates:/templates
- dbal-seeds:/seeds
dbal:
build:
context: ../../dbal
dockerfile: production/build-config/Dockerfile
args:
BUILD_TYPE: Release
container_name: qt6-dbal
restart: unless-stopped
ports:
- "8080:8080"
environment:
DBAL_ADAPTER: sqlite
DATABASE_URL: ":memory:"
DBAL_SCHEMA_DIR: /app/schemas/entities
DBAL_TEMPLATE_DIR: /app/templates/sql
DBAL_SEED_DIR: /app/seeds/database
DBAL_SEED_ON_STARTUP: "true"
DBAL_BIND_ADDRESS: 0.0.0.0
DBAL_PORT: 8080
DBAL_MODE: development
DBAL_DAEMON: "true"
DBAL_LOG_LEVEL: debug
DBAL_AUTO_CREATE_TABLES: "true"
DBAL_ENABLE_HEALTH_CHECK: "true"
DBAL_CORS_ORIGIN: "*"
DBAL_ADMIN_TOKEN: dev-token
JWT_SECRET_KEY: dev-secret
volumes:
- dbal-schemas:/app/schemas/entities:ro
- dbal-templates:/app/templates/sql:ro
- dbal-seeds:/app/seeds/database:ro
depends_on:
dbal-init:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Optional: Media daemon (--profile media)
media-daemon:
build:
context: ../../services/media_daemon
dockerfile: Dockerfile
container_name: qt6-media-daemon
restart: unless-stopped
profiles: ["media"]
ports:
- "8090:8090"
environment:
MEDIA_PORT: 8090
MEDIA_BIND_ADDRESS: 0.0.0.0
MEDIA_WORKERS: 2
DBAL_URL: http://dbal:8080
MEDIA_RADIO_ENABLED: "false"
MEDIA_TV_ENABLED: "false"
depends_on:
dbal:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://127.0.0.1:8090/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 20s
volumes:
dbal-schemas:
dbal-templates:
dbal-seeds:
+370
View File
@@ -0,0 +1,370 @@
#!/usr/bin/env python3
"""Auto-generate CMakeLists.txt from project structure and cmake_config.json.
Scans QML files, C++ sources, SVG/audio assets, and package metadata to produce
a complete CMakeLists.txt for the Qt6 DBAL Observatory frontend.
Usage:
python3 generate_cmake.py # Write CMakeLists.txt
python3 generate_cmake.py --dry-run # Print without writing
python3 generate_cmake.py --output build.cmake # Custom output path
python3 generate_cmake.py --config my.json # Custom config file
"""
import argparse
import glob
import json
import os
import sys
from pathlib import Path
def load_config(config_path: str) -> dict:
"""Load and validate cmake_config.json."""
path = Path(config_path)
if not path.exists():
print(f"Error: config file not found: {config_path}", file=sys.stderr)
sys.exit(1)
with open(path) as f:
return json.load(f)
def find_root_qml_files(root_dir: Path) -> list[str]:
"""Find all *.qml files in the project root directory (not subdirectories)."""
files = sorted(root_dir.glob("*.qml"))
return [f.name for f in files]
def find_qmllib_files(root_dir: Path) -> dict[str, list[str]]:
"""Find all *.qml files and qmldir files in qmllib/ subdirectories.
Returns a dict mapping relative paths (e.g. 'qmllib/dbal/DBALProvider.qml')
grouped by subdirectory.
"""
qmllib_dir = root_dir / "qmllib"
result = {"qml": [], "resources": []}
if not qmllib_dir.exists():
return result
for qml_file in sorted(qmllib_dir.rglob("*.qml")):
result["qml"].append(str(qml_file.relative_to(root_dir)))
for qmldir_file in sorted(qmllib_dir.rglob("qmldir")):
result["resources"].append(str(qmldir_file.relative_to(root_dir)))
return result
def find_package_qml_files(root_dir: Path) -> list[str]:
"""Find all *.qml files in packages/ subdirectories."""
packages_dir = root_dir / "packages"
if not packages_dir.exists():
return []
files = sorted(packages_dir.rglob("*.qml"))
return [str(f.relative_to(root_dir)) for f in files]
def load_package_metadata(root_dir: Path) -> list[dict]:
"""Read metadata.json from each packages/ subdirectory."""
packages_dir = root_dir / "packages"
if not packages_dir.exists():
return []
metadata = []
for meta_file in sorted(packages_dir.rglob("metadata.json")):
with open(meta_file) as f:
data = json.load(f)
data["_dir"] = str(meta_file.parent.relative_to(root_dir))
metadata.append(data)
return metadata
def find_svg_assets(root_dir: Path) -> list[str]:
"""Glob SVG assets from assets/svg/."""
svg_dir = root_dir / "assets" / "svg"
if not svg_dir.exists():
return []
files = sorted(svg_dir.glob("*.svg"))
return [str(f.relative_to(root_dir)) for f in files]
def find_audio_assets(root_dir: Path) -> list[str]:
"""Glob audio assets from assets/audio/."""
audio_dir = root_dir / "assets" / "audio"
if not audio_dir.exists():
return []
files = sorted(audio_dir.rglob("*"))
return [str(f.relative_to(root_dir)) for f in files if f.is_file()]
def find_cpp_sources(root_dir: Path) -> dict[str, list[str]]:
"""Find all *.cpp and *.h files in src/."""
src_dir = root_dir / "src"
result = {"cpp": [], "h": []}
if not src_dir.exists():
return result
result["cpp"] = sorted(
str(f.relative_to(root_dir)) for f in src_dir.rglob("*.cpp")
)
result["h"] = sorted(
str(f.relative_to(root_dir)) for f in src_dir.rglob("*.h")
)
return result
def indent_list(items: list[str], spaces: int = 8) -> str:
"""Format a list of file paths as indented CMake entries."""
prefix = " " * spaces
return "\n".join(f"{prefix}{item}" for item in items)
def generate_cmake(config: dict, root_dir: Path) -> str:
"""Generate the full CMakeLists.txt content from config and discovered files."""
proj = config["project"]
qt = config["qt"]
cpp = config["cpp"]
qml = config["qml"]
features = config.get("features", {})
compile_defs = config.get("compile_definitions", {})
# Discover files
root_qml = find_root_qml_files(root_dir)
qmllib = find_qmllib_files(root_dir)
package_qml = find_package_qml_files(root_dir)
cpp_sources = find_cpp_sources(root_dir)
svg_assets = find_svg_assets(root_dir)
audio_assets = find_audio_assets(root_dir)
packages_meta = load_package_metadata(root_dir)
# Build Qt components string
qt_components = " ".join(qt["components"])
# Conditional components from features
extra_components = []
if features.get("qt_multimedia"):
extra_components.append("Multimedia")
all_components = qt["components"] + extra_components
qt_components_str = " ".join(all_components)
# Build source files list (main.cpp + src/*.cpp)
source_files = ["main.cpp"]
source_files.extend(cpp_sources["cpp"])
# Build QML files list: root QML + qmllib QML + package QML
all_qml_files = root_qml + qmllib["qml"] + package_qml
# Build RESOURCES list: audio + qmllib resources (qmldir files)
resource_files = audio_assets + qmllib["resources"]
# Build compile definitions
defs_lines = []
for key, value in compile_defs.items():
defs_lines.append(f'target_compile_definitions({proj["executable"]} PRIVATE {key}="{value}")')
# Build link libraries
link_libs = " ".join(f"Qt6::{c}" for c in all_components)
# Optional feature blocks
feature_blocks = []
if features.get("libopenmpt"):
feature_blocks.append(f"""
# libopenmpt support
find_package(PkgConfig REQUIRED)
pkg_check_modules(OPENMPT REQUIRED libopenmpt)
target_include_directories({proj["executable"]} PRIVATE ${{OPENMPT_INCLUDE_DIRS}})
target_link_libraries({proj["executable"]} PRIVATE ${{OPENMPT_LIBRARIES}})""")
# Package metadata comment block
pkg_comment_lines = []
if packages_meta:
pkg_comment_lines.append("# Discovered packages:")
for meta in packages_meta:
pkg_comment_lines.append(
f'# {meta.get("packageId", "unknown"):20s} '
f'v{meta.get("version", "?")} - {meta.get("name", "")}'
)
# Assemble the CMakeLists.txt
lines = []
lines.append("# AUTO-GENERATED by generate_cmake.py — do not edit manually")
lines.append(f"# Generated from cmake_config.json | {len(all_qml_files)} QML files, "
f"{len(source_files)} C++ sources, {len(svg_assets)} SVGs, "
f"{len(audio_assets)} audio assets")
if pkg_comment_lines:
lines.append("#")
lines.extend(pkg_comment_lines)
lines.append("")
lines.append("cmake_minimum_required(VERSION 3.27)")
lines.append(f'project({proj["name"]} VERSION {proj["version"]} LANGUAGES CXX)')
lines.append("")
lines.append(f"set(CMAKE_CXX_STANDARD {cpp['standard']})")
lines.append("set(CMAKE_CXX_STANDARD_REQUIRED ON)")
lines.append("set(CMAKE_EXPORT_COMPILE_COMMANDS ON)")
lines.append("set(CMAKE_AUTOMOC ON)")
lines.append("")
lines.append("# MSVC: Qt requires correct __cplusplus macro value")
lines.append("if(MSVC)")
lines.append(" add_compile_options(/Zc:__cplusplus)")
lines.append("endif()")
lines.append("")
lines.append("include(${CMAKE_BINARY_DIR}/conan_toolchain.cmake OPTIONAL)")
lines.append("")
lines.append(f"find_package(Qt6 COMPONENTS {qt_components_str} REQUIRED)")
lines.append("qt_policy(SET QTP0001 NEW)")
lines.append("")
# qt_add_executable
lines.append(f"qt_add_executable({proj['executable']}")
for src in source_files:
lines.append(f" {src}")
lines.append(")")
lines.append("")
# Compile definitions
for d in defs_lines:
lines.append(d)
if defs_lines:
lines.append("")
# qt_add_qml_module
lines.append(f"qt_add_qml_module({proj['executable']}")
lines.append(f" URI {qml['uri']}")
lines.append(f" VERSION {qml['version']}")
lines.append(" QML_FILES")
for qf in all_qml_files:
lines.append(f" {qf}")
if resource_files:
lines.append(" RESOURCES")
for rf in resource_files:
lines.append(f" {rf}")
lines.append(")")
lines.append("")
# SVG assets via file(GLOB) + qt_add_resources
if svg_assets:
lines.append("# SVG assets")
lines.append(f"file(GLOB SVG_ASSETS RELATIVE ${{CMAKE_CURRENT_SOURCE_DIR}} assets/svg/*.svg)")
lines.append(f'qt_add_resources({proj["executable"]} "svg_assets"')
lines.append(' PREFIX "/"')
lines.append(" FILES ${SVG_ASSETS}")
lines.append(")")
lines.append("")
# Link libraries
lines.append(f"target_link_libraries({proj['executable']} PRIVATE")
lines.append(f" {link_libs}")
lines.append(")")
lines.append("")
# Feature blocks
for block in feature_blocks:
lines.append(block)
lines.append("")
# MSVC include path fix
lines.append("# Conan Qt recipe: propagate CMAKE_INCLUDE_PATH entries for MSVC")
lines.append("foreach(_inc ${CMAKE_INCLUDE_PATH})")
lines.append(f' target_include_directories({proj["executable"]} PRIVATE "${{_inc}}")')
lines.append("endforeach()")
lines.append("")
# Finalize
lines.append(f"qt_finalize_executable({proj['executable']})")
lines.append("")
# Ninja warning
lines.append('if(NOT "${CMAKE_GENERATOR}" STREQUAL "Ninja")')
lines.append(" message(")
lines.append(" STATUS")
lines.append(f' "{proj["executable"]} is designed for Ninja; configure with `cmake -G Ninja` so the Conan Ninja toolchain is used."')
lines.append(" )")
lines.append("endif()")
lines.append("")
# Install
lines.append(f"install(TARGETS {proj['executable']}")
lines.append(" RUNTIME DESTINATION bin")
lines.append(")")
lines.append("")
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Auto-generate CMakeLists.txt from project structure and cmake_config.json.",
epilog="Examples:\n"
" python3 generate_cmake.py # Write CMakeLists.txt\n"
" python3 generate_cmake.py --dry-run # Print without writing\n"
" python3 generate_cmake.py --output build.cmake # Custom output\n"
" python3 generate_cmake.py --config my.json # Custom config\n",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--config",
default="cmake_config.json",
help="Path to cmake_config.json (default: cmake_config.json)",
)
parser.add_argument(
"--output",
default="CMakeLists.txt",
help="Output path for generated CMakeLists.txt (default: CMakeLists.txt)",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print generated CMakeLists.txt to stdout without writing to disk",
)
parser.add_argument(
"--root",
default=None,
help="Project root directory (default: directory containing this script)",
)
args = parser.parse_args()
# Determine root directory
if args.root:
root_dir = Path(args.root).resolve()
else:
root_dir = Path(__file__).parent.resolve()
# Resolve config path relative to root if not absolute
config_path = Path(args.config)
if not config_path.is_absolute():
config_path = root_dir / config_path
config = load_config(str(config_path))
content = generate_cmake(config, root_dir)
if args.dry_run:
print(content)
return
# Resolve output path relative to root if not absolute
output_path = Path(args.output)
if not output_path.is_absolute():
output_path = root_dir / output_path
with open(output_path, "w") as f:
f.write(content)
# Summary
root_qml = find_root_qml_files(root_dir)
qmllib = find_qmllib_files(root_dir)
package_qml = find_package_qml_files(root_dir)
cpp_sources = find_cpp_sources(root_dir)
svg_assets = find_svg_assets(root_dir)
audio_assets = find_audio_assets(root_dir)
packages_meta = load_package_metadata(root_dir)
total_qml = len(root_qml) + len(qmllib["qml"]) + len(package_qml)
total_cpp = len(cpp_sources["cpp"]) + 1 # +1 for main.cpp
print(f"Generated {output_path}")
print(f" QML files: {total_qml} ({len(root_qml)} root, {len(qmllib['qml'])} qmllib, {len(package_qml)} packages)")
print(f" C++ sources: {total_cpp} ({len(cpp_sources['cpp'])} in src/ + main.cpp)")
print(f" C++ headers: {len(cpp_sources['h'])}")
print(f" SVG assets: {len(svg_assets)}")
print(f" Audio assets: {len(audio_assets)}")
print(f" Packages: {len(packages_meta)} with metadata.json")
if __name__ == "__main__":
main()
+9
View File
@@ -6,6 +6,8 @@
#include "src/PackageRegistry.h"
#include "src/ModPlayer.h"
#include "src/DBALClient.h"
#include "src/PackageLoader.h"
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
@@ -29,9 +31,16 @@ int main(int argc, char *argv[]) {
PackageRegistry registry;
ModPlayer modPlayer;
DBALClient dbalClient;
PackageLoader packageLoader;
registry.loadPackage("frontpage");
packageLoader.setPackagesDir(QDir(QStringLiteral(SRCDIR) + QStringLiteral("/packages")).absolutePath());
packageLoader.scan();
packageLoader.setWatching(true);
engine.rootContext()->setContextProperty(QStringLiteral("PackageRegistry"), &registry);
engine.rootContext()->setContextProperty(QStringLiteral("ModPlayer"), &modPlayer);
engine.rootContext()->setContextProperty(QStringLiteral("DBALClient"), &dbalClient);
engine.rootContext()->setContextProperty(QStringLiteral("PackageLoader"), &packageLoader);
const QUrl url(QStringLiteral("qrc:/DBALObservatory/App.qml"));
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
@@ -1,50 +1,210 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CPaper {
id: packageCard
width: 460
height: 320
property string title: "Connection Hub"
property string subtitle: "v1.0.0"
property var dependenciesList: ["profile_page", "gallery"]
Rectangle {
id: root
color: "transparent"
ColumnLayout {
property var services: [
{ name: "GitHub", icon: "GH", connected: true, health: "green", lastConnected: "2026-03-19 08:12" },
{ name: "Slack", icon: "SL", connected: true, health: "green", lastConnected: "2026-03-19 07:45" },
{ name: "SMTP", icon: "SM", connected: true, health: "yellow", lastConnected: "2026-03-18 23:00" },
{ name: "S3 Storage", icon: "S3", connected: false, health: "red", lastConnected: "2026-03-15 14:30" },
{ name: "Database", icon: "DB", connected: true, health: "green", lastConnected: "2026-03-19 08:14" }
]
property bool testingAll: false
function healthColor(h) {
if (h === "green") return "#2ecc71"
if (h === "yellow") return "#f39c12"
return "#e74c3c"
}
function healthLabel(h) {
if (h === "green") return "Healthy"
if (h === "yellow") return "Degraded"
return "Offline"
}
function toggleConnection(index) {
var s = services.slice()
s[index] = Object.assign({}, s[index])
s[index].connected = !s[index].connected
if (!s[index].connected) {
s[index].health = "red"
} else {
s[index].health = "green"
s[index].lastConnected = "2026-03-19 08:20"
}
services = s
}
function connectedCount() {
var c = 0
for (var i = 0; i < services.length; i++)
if (services[i].connected) c++
return c
}
function testAll() {
testingAll = true
testTimer.start()
}
Timer {
id: testTimer
interval: 1500
onTriggered: root.testingAll = false
}
ScrollView {
anchors.fill: parent
anchors.margins: 18
spacing: 12
anchors.margins: 20
clip: true
RowLayout {
spacing: 12
CAvatar { initials: title.left(2).toUpper(); backgroundColor: Theme.primary }
ColumnLayout {
spacing: 4
CText { variant: "h3"; text: title }
CText { variant: "body1"; text: subtitle }
ColumnLayout {
width: parent.width
spacing: 16
// ── Header ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h2"; text: "Connection Hub" }
CStatusBadge {
status: connectedCount() === services.length ? "success" : "warning"
text: connectedCount() + "/" + services.length + " Connected"
}
Item { Layout.fillWidth: true }
CButton {
text: root.testingAll ? "Testing..." : "Test All"
variant: "ghost"
enabled: !root.testingAll
onClicked: root.testAll()
}
}
}
}
Item { Layout.fillWidth: true }
CBadge { text: dependenciesList.length ? "Dependency package" : "Standalone"; accent: dependenciesList.length > 0 }
}
Text {
text: "Social graph with events, groups, and shared media."
font.pixelSize: 14
color: Theme.text
wrapMode: Text.WordWrap
}
// ── Service list ──
Repeater {
model: root.services
RowLayout {
spacing: 8
CChip { text: "Adaptive layout" }
CChip { text: "Realtime telemetry" }
CChip { text: "Community moderation" }
}
CCard {
Layout.fillWidth: true
RowLayout {
spacing: 8
CButton { text: "Install" }
CButton { text: "Dependency graph"; variant: "ghost" }
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 16
// Health indicator dot
Rectangle {
width: 14
height: 14
radius: 7
color: healthColor(modelData.health)
SequentialAnimation on opacity {
running: root.testingAll
loops: Animation.Infinite
NumberAnimation { to: 0.3; duration: 400 }
NumberAnimation { to: 1.0; duration: 400 }
}
}
// Service icon
CAvatar {
initials: modelData.icon
backgroundColor: modelData.connected ? Theme.primary : Theme.border
}
// Service info
ColumnLayout {
Layout.fillWidth: true
spacing: 4
FlexRow {
spacing: 8
CText { variant: "subtitle1"; text: modelData.name }
CStatusBadge {
status: modelData.connected ? "success" : "error"
text: modelData.connected ? "Connected" : "Disconnected"
}
}
FlexRow {
spacing: 12
CText {
variant: "caption"
text: "Health: " + healthLabel(modelData.health)
color: healthColor(modelData.health)
}
CText {
variant: "caption"
text: "Last: " + modelData.lastConnected
opacity: 0.6
}
}
}
// Connect / Disconnect toggle
CButton {
text: modelData.connected ? "Disconnect" : "Connect"
variant: modelData.connected ? "ghost" : "default"
size: "sm"
onClicked: root.toggleConnection(index)
}
}
}
}
// ── Summary footer ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
CText { variant: "subtitle1"; text: "Connection Summary" }
CDivider { Layout.fillWidth: true }
FlexRow {
Layout.fillWidth: true
spacing: 16
ColumnLayout {
spacing: 4
CText { variant: "caption"; text: "Total Services" }
CText { variant: "h3"; text: services.length.toString() }
}
ColumnLayout {
spacing: 4
CText { variant: "caption"; text: "Connected" }
CText { variant: "h3"; text: connectedCount().toString(); color: "#2ecc71" }
}
ColumnLayout {
spacing: 4
CText { variant: "caption"; text: "Disconnected" }
CText { variant: "h3"; text: (services.length - connectedCount()).toString(); color: "#e74c3c" }
}
}
}
}
}
}
}
@@ -1,50 +1,357 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CPaper {
id: packageCard
width: 460
height: 320
property string title: "Escape Room"
property string subtitle: "v1.0.0"
property var dependenciesList: ["retro_games", "microthread"]
Rectangle {
id: root
color: "transparent"
ColumnLayout {
// ── Game state ──
property int elapsedSeconds: 0
property bool gameActive: true
property bool puzzleSolved: false
property var inventory: []
property string currentRoom: "The Antechamber"
property string roomDescription: "You stand in a dimly lit stone chamber. Ancient symbols line the walls. A heavy iron door blocks the passage north. A dusty bookshelf leans against the east wall, and a strange brass mechanism sits on a pedestal in the center."
property string digit1: ""
property string digit2: ""
property string digit3: ""
property string digit4: ""
property string correctCode: "7314"
property string feedbackText: ""
ListModel {
id: clueLog
ListElement { clue: "The chamber smells of old parchment and oil." }
ListElement { clue: "You notice scratch marks near the pedestal base." }
}
// ── Timer ──
Timer {
id: gameTimer
interval: 1000
running: root.gameActive && !root.puzzleSolved
repeat: true
onTriggered: root.elapsedSeconds++
}
function formatTime(sec) {
var m = Math.floor(sec / 60)
var s = sec % 60
return (m < 10 ? "0" : "") + m + ":" + (s < 10 ? "0" : "") + s
}
function examine() {
var clues = [
"A faded inscription reads: 'The first digit mirrors the walls' — you count 7 symbols.",
"Behind the bookshelf you find a torn page: '...third, the one that comes after zero...'",
"The brass mechanism has 4 dials. One is stuck on the number 4.",
"A scratched tally on the pedestal base shows III marks.",
"You notice the symbols repeat in groups of seven.",
"An old map fragment shows a path marked with the number 1."
]
var c = clues[Math.floor(Math.random() * clues.length)]
clueLog.append({ clue: c })
}
function lookAround() {
var observations = [
"The iron door has a combination lock with 4 rotating dials.",
"Cobwebs stretch between the ceiling beams. Something glints behind one.",
"The floor tiles are numbered, but most are worn away.",
"A cold draft seeps from beneath the north door.",
"The bookshelf contains old journals — one is bookmarked."
]
var o = observations[Math.floor(Math.random() * observations.length)]
clueLog.append({ clue: o })
}
function useItem() {
if (inventory.length === 0) {
feedbackText = "Your inventory is empty."
return
}
clueLog.append({ clue: "You use the " + inventory[inventory.length - 1] + " but nothing happens... yet." })
}
function collectItem() {
var items = ["Rusty Key", "Torn Page", "Brass Gear", "Glass Lens", "Iron Nail"]
var available = []
for (var i = 0; i < items.length; i++) {
if (inventory.indexOf(items[i]) < 0) available.push(items[i])
}
if (available.length === 0) {
feedbackText = "Nothing more to collect here."
return
}
var item = available[Math.floor(Math.random() * available.length)]
var inv = inventory.slice()
inv.push(item)
inventory = inv
clueLog.append({ clue: "You found: " + item })
}
function tryCode() {
var entered = digit1 + digit2 + digit3 + digit4
if (entered.length < 4) {
feedbackText = "Enter all 4 digits."
return
}
if (entered === correctCode) {
puzzleSolved = true
feedbackText = "The lock clicks open! You escaped!"
clueLog.append({ clue: "VICTORY! The door swings open. Time: " + formatTime(elapsedSeconds) })
} else {
feedbackText = "Wrong combination. Try again."
clueLog.append({ clue: "Code " + entered + " failed. The dials reset." })
}
}
ScrollView {
anchors.fill: parent
anchors.margins: 18
spacing: 12
anchors.margins: 20
clip: true
RowLayout {
spacing: 12
CAvatar { initials: title.left(2).toUpper(); backgroundColor: Theme.primary }
ColumnLayout {
spacing: 4
CText { variant: "h3"; text: title }
CText { variant: "body1"; text: subtitle }
ColumnLayout {
width: parent.width
spacing: 16
// ── Header ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h2"; text: "Escape Room" }
CStatusBadge {
status: root.puzzleSolved ? "success" : "warning"
text: root.puzzleSolved ? "Escaped!" : "In Progress"
}
Item { Layout.fillWidth: true }
// Timer
Rectangle {
width: 90
height: 36
radius: 8
color: Theme.card
border.color: Theme.border
border.width: 1
CText {
anchors.centerIn: parent
variant: "h3"
text: formatTime(root.elapsedSeconds)
color: root.elapsedSeconds > 300 ? "#e74c3c" : Theme.text
}
}
}
}
}
Item { Layout.fillWidth: true }
CBadge { text: dependenciesList.length ? "Dependency package" : "Standalone"; accent: dependenciesList.length > 0 }
}
Text {
text: "Puzzle-driven escape room within the Qt UI for team-building."
font.pixelSize: 14
color: Theme.text
wrapMode: Text.WordWrap
}
RowLayout {
Layout.fillWidth: true
spacing: 16
RowLayout {
spacing: 8
CChip { text: "Adaptive layout" }
CChip { text: "Realtime telemetry" }
CChip { text: "Community moderation" }
}
// ── Left column: Room + Puzzle ──
ColumnLayout {
Layout.fillWidth: true
Layout.preferredWidth: 400
spacing: 16
RowLayout {
spacing: 8
CButton { text: "Install" }
CButton { text: "Dependency graph"; variant: "ghost" }
// Room description
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 10
FlexRow {
spacing: 8
CText { variant: "subtitle1"; text: root.currentRoom }
CBadge { text: "Room 1 of 3" }
}
CText {
variant: "body1"
text: root.roomDescription
Layout.fillWidth: true
wrapMode: Text.WordWrap
font.italic: true
opacity: 0.85
}
}
}
// Interaction buttons
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 10
CText { variant: "subtitle1"; text: "Actions" }
GridLayout {
Layout.fillWidth: true
columns: 2
columnSpacing: 8
rowSpacing: 8
CButton { text: "Examine"; Layout.fillWidth: true; onClicked: root.examine(); enabled: !root.puzzleSolved }
CButton { text: "Look Around"; Layout.fillWidth: true; onClicked: root.lookAround(); enabled: !root.puzzleSolved }
CButton { text: "Use Item"; Layout.fillWidth: true; variant: "ghost"; onClicked: root.useItem(); enabled: !root.puzzleSolved }
CButton { text: "Search Area"; Layout.fillWidth: true; variant: "ghost"; onClicked: root.collectItem(); enabled: !root.puzzleSolved }
}
}
}
// Combination lock puzzle
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
CText { variant: "subtitle1"; text: "Combination Lock" }
CText { variant: "caption"; text: "Enter the 4-digit code to unlock the door" }
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: 8
Repeater {
model: 4
TextField {
Layout.preferredWidth: 50
Layout.preferredHeight: 50
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 22
font.bold: true
maximumLength: 1
inputMethodHints: Qt.ImhDigitsOnly
color: Theme.text
background: Rectangle {
radius: 8
color: Theme.card
border.color: root.puzzleSolved ? "#2ecc71" : Theme.border
border.width: 2
}
onTextChanged: {
if (index === 0) root.digit1 = text
else if (index === 1) root.digit2 = text
else if (index === 2) root.digit3 = text
else root.digit4 = text
}
}
}
}
CButton {
text: root.puzzleSolved ? "Unlocked!" : "Try Combination"
Layout.fillWidth: true
enabled: !root.puzzleSolved
onClicked: root.tryCode()
}
CText {
variant: "caption"
text: root.feedbackText
color: root.puzzleSolved ? "#2ecc71" : "#e74c3c"
visible: root.feedbackText.length > 0
}
}
}
}
// ── Right column: Inventory + Clues ──
ColumnLayout {
Layout.fillWidth: true
Layout.preferredWidth: 280
spacing: 16
// Inventory
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
FlexRow {
spacing: 8
CText { variant: "subtitle1"; text: "Inventory" }
CBadge { text: root.inventory.length + " items" }
}
CDivider { Layout.fillWidth: true }
CText {
variant: "body2"
text: "No items collected yet."
visible: root.inventory.length === 0
opacity: 0.5
}
Repeater {
model: root.inventory
CListItem {
Layout.fillWidth: true
text: modelData
}
}
}
}
// Clue log
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
FlexRow {
spacing: 8
CText { variant: "subtitle1"; text: "Clue Log" }
CBadge { text: clueLog.count + " clues" }
}
CDivider { Layout.fillWidth: true }
Repeater {
model: clueLog
CText {
variant: "caption"
text: "\u2022 " + model.clue
Layout.fillWidth: true
wrapMode: Text.WordWrap
opacity: 0.8
}
}
}
}
}
}
}
}
}
@@ -1,50 +1,231 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CPaper {
id: packageCard
width: 460
height: 320
property string title: "Marketplace"
property string subtitle: "v1.0.0"
property var dependenciesList: ["frontpage", "storybook"]
Rectangle {
id: root
color: "transparent"
ColumnLayout {
property string activeCategory: "All"
property string searchQuery: ""
property int cartCount: 0
property var cartItems: []
property var categories: ["All", "Electronics", "Software", "Services"]
property var products: [
{ name: "MetaBuilder Pro License", price: 49.99, rating: 5, category: "Software", color: Theme.primary },
{ name: "USB-C Hub Adapter", price: 29.95, rating: 4, category: "Electronics", color: "#e67e22" },
{ name: "CI/CD Pipeline Setup", price: 199.00, rating: 5, category: "Services", color: "#2ecc71" },
{ name: "Mechanical Keyboard", price: 89.99, rating: 4, category: "Electronics", color: "#9b59b6" },
{ name: "Code Review Package", price: 75.00, rating: 3, category: "Services", color: "#e74c3c" },
{ name: "Dark Theme Extension", price: 4.99, rating: 5, category: "Software", color: "#34495e" }
]
function filteredProducts() {
var result = []
for (var i = 0; i < products.length; i++) {
var p = products[i]
var matchCategory = activeCategory === "All" || p.category === activeCategory
var matchSearch = searchQuery.length === 0 ||
p.name.toLowerCase().indexOf(searchQuery.toLowerCase()) >= 0
if (matchCategory && matchSearch) result.push(p)
}
return result
}
function addToCart(productName) {
var items = cartItems.slice()
items.push(productName)
cartItems = items
cartCount = items.length
}
function ratingStars(count) {
var s = ""
for (var i = 0; i < 5; i++) s += (i < count ? "\u2605" : "\u2606")
return s
}
ScrollView {
anchors.fill: parent
anchors.margins: 18
spacing: 12
anchors.margins: 20
clip: true
RowLayout {
spacing: 12
CAvatar { initials: title.left(2).toUpper(); backgroundColor: Theme.primary }
ColumnLayout {
spacing: 4
CText { variant: "h3"; text: title }
CText { variant: "body1"; text: subtitle }
ColumnLayout {
width: parent.width
spacing: 16
// ── Header ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h2"; text: "Marketplace" }
CBadge { text: products.length + " Products" }
Item { Layout.fillWidth: true }
CBadge { text: cartCount + " in Cart"; accent: cartCount > 0 }
}
}
}
Item { Layout.fillWidth: true }
CBadge { text: dependenciesList.length ? "Dependency package" : "Standalone"; accent: dependenciesList.length > 0 }
}
Text {
text: "Curated extensions and paid components with purchase-to-install workflow."
font.pixelSize: 14
color: Theme.text
wrapMode: Text.WordWrap
}
// ── Search bar ──
CCard {
Layout.fillWidth: true
RowLayout {
spacing: 8
CChip { text: "Adaptive layout" }
CChip { text: "Realtime telemetry" }
CChip { text: "Community moderation" }
}
RowLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
RowLayout {
spacing: 8
CButton { text: "Install" }
CButton { text: "Dependency graph"; variant: "ghost" }
CTextField {
Layout.fillWidth: true
placeholderText: "Search products..."
text: root.searchQuery
onTextChanged: root.searchQuery = text
}
}
}
// ── Category filter chips ──
FlexRow {
Layout.fillWidth: true
spacing: 8
Repeater {
model: root.categories
CChip {
text: modelData
selected: root.activeCategory === modelData
onClicked: root.activeCategory = modelData
}
}
}
// ── Product Grid ──
GridLayout {
Layout.fillWidth: true
columns: 3
columnSpacing: 12
rowSpacing: 12
Repeater {
model: filteredProducts()
CCard {
Layout.fillWidth: true
Layout.preferredWidth: 200
Layout.preferredHeight: 260
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 10
// Image placeholder
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 80
radius: 8
color: modelData.color
opacity: 0.85
CText {
anchors.centerIn: parent
variant: "h3"
text: modelData.name.substring(0, 2).toUpperCase()
color: "#ffffff"
}
}
CText {
variant: "subtitle1"
text: modelData.name
Layout.fillWidth: true
elide: Text.ElideRight
}
CText {
variant: "caption"
text: ratingStars(modelData.rating)
font.pixelSize: 16
color: "#f39c12"
}
FlexRow {
Layout.fillWidth: true
spacing: 8
CText {
variant: "h3"
text: "$" + modelData.price.toFixed(2)
color: Theme.primary
}
Item { Layout.fillWidth: true }
CChip { text: modelData.category }
}
CButton {
text: "Add to Cart"
Layout.fillWidth: true
onClicked: root.addToCart(modelData.name)
}
}
}
}
}
// ── Cart summary ──
CCard {
Layout.fillWidth: true
visible: cartCount > 0
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "h3"; text: "Cart Summary" }
CBadge { text: cartCount + " items"; accent: true }
Item { Layout.fillWidth: true }
CButton {
text: "Clear Cart"
variant: "ghost"
size: "sm"
onClicked: { root.cartItems = []; root.cartCount = 0 }
}
}
CDivider { Layout.fillWidth: true }
Repeater {
model: root.cartItems
CText { variant: "body2"; text: "\u2022 " + modelData }
}
CDivider { Layout.fillWidth: true }
CButton {
text: "Checkout"
Layout.fillWidth: true
}
}
}
}
}
}
@@ -1,50 +1,258 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CPaper {
id: packageCard
width: 460
height: 320
property string title: "MicroThread"
property string subtitle: "v1.0.0"
property var dependenciesList: ["login", "profile_page"]
Rectangle {
id: root
color: "transparent"
ColumnLayout {
property string composeText: ""
property int maxChars: 280
property string sortMode: "Recent"
property var sortOptions: ["Recent", "Popular", "Following"]
ListModel {
id: postsModel
ListElement {
postId: 1; username: "alice_dev"; initials: "AL"; avatarColor: "#3498db"
timestamp: "2026-03-19 08:30"
body: "Just shipped the new workflow engine with event-driven architecture. JSON config is the way forward!"
likes: 24; replies: 5; liked: false; following: true
}
ListElement {
postId: 2; username: "bob_ops"; initials: "BO"; avatarColor: "#e67e22"
timestamp: "2026-03-19 07:15"
body: "Redis caching layer benchmarks are in: 3ms avg response time with read-through pattern. Production ready."
likes: 18; replies: 3; liked: true; following: true
}
ListElement {
postId: 3; username: "charlie_ui"; initials: "CH"; avatarColor: "#2ecc71"
timestamp: "2026-03-18 22:45"
body: "FakeMUI hit 167 components today. Next milestone: full parity with Material UI core set."
likes: 31; replies: 8; liked: false; following: false
}
ListElement {
postId: 4; username: "diana_sec"; initials: "DI"; avatarColor: "#9b59b6"
timestamp: "2026-03-18 19:00"
body: "Security audit complete. All endpoints now enforce multi-tenant filtering. No exceptions."
likes: 42; replies: 2; liked: false; following: true
}
ListElement {
postId: 5; username: "eve_data"; initials: "EV"; avatarColor: "#e74c3c"
timestamp: "2026-03-18 16:20"
body: "14 database backends and counting. SurrealDB adapter just passed all CRUD tests."
likes: 15; replies: 6; liked: false; following: false
}
ListElement {
postId: 6; username: "frank_ml"; initials: "FR"; avatarColor: "#1abc9c"
timestamp: "2026-03-18 14:00"
body: "Working on the Mojo compiler integration. Hot reload is surprisingly smooth with the new bridge."
likes: 9; replies: 1; liked: false; following: false
}
}
function toggleLike(index) {
var item = postsModel.get(index)
postsModel.setProperty(index, "liked", !item.liked)
postsModel.setProperty(index, "likes", item.liked ? item.likes - 1 : item.likes + 1)
}
function submitPost() {
if (composeText.length === 0 || composeText.length > maxChars) return
postsModel.insert(0, {
postId: postsModel.count + 1,
username: "demo_user",
initials: "DE",
avatarColor: Theme.primary,
timestamp: "2026-03-19 08:35",
body: composeText,
likes: 0,
replies: 0,
liked: false,
following: false
})
composeText = ""
}
ScrollView {
anchors.fill: parent
anchors.margins: 18
spacing: 12
anchors.margins: 20
clip: true
RowLayout {
spacing: 12
CAvatar { initials: title.left(2).toUpper(); backgroundColor: Theme.primary }
ColumnLayout {
spacing: 4
CText { variant: "h3"; text: title }
CText { variant: "body1"; text: subtitle }
ColumnLayout {
width: parent.width
spacing: 16
// ── Header ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h2"; text: "MicroThread" }
CBadge { text: postsModel.count + " Posts" }
Item { Layout.fillWidth: true }
CStatusBadge { status: "success"; text: "Live Feed" }
}
}
}
Item { Layout.fillWidth: true }
CBadge { text: dependenciesList.length ? "Dependency package" : "Standalone"; accent: dependenciesList.length > 0 }
}
Text {
text: "Rapid-fire posting feed, like a micro-blogging stream."
font.pixelSize: 14
color: Theme.text
wrapMode: Text.WordWrap
}
// ── Compose area ──
CCard {
Layout.fillWidth: true
RowLayout {
spacing: 8
CChip { text: "Adaptive layout" }
CChip { text: "Realtime telemetry" }
CChip { text: "Community moderation" }
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 10
RowLayout {
spacing: 8
CButton { text: "Install" }
CButton { text: "Dependency graph"; variant: "ghost" }
CText { variant: "subtitle1"; text: "What's happening?" }
CTextField {
id: composeField
Layout.fillWidth: true
placeholderText: "Share your thoughts..."
text: root.composeText
onTextChanged: root.composeText = text
}
FlexRow {
Layout.fillWidth: true
spacing: 8
CText {
variant: "caption"
text: root.composeText.length + " / " + root.maxChars
color: root.composeText.length > root.maxChars ? "#e74c3c" : Theme.text
opacity: root.composeText.length > root.maxChars ? 1.0 : 0.6
}
Item { Layout.fillWidth: true }
CButton {
text: "Post"
enabled: root.composeText.length > 0 && root.composeText.length <= root.maxChars
onClicked: root.submitPost()
}
}
}
}
// ── Sort chips ──
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "caption"; text: "Sort by:" }
Repeater {
model: root.sortOptions
CChip {
text: modelData
selected: root.sortMode === modelData
onClicked: root.sortMode = modelData
}
}
}
// ── Thread feed ──
Repeater {
model: postsModel
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 10
FlexRow {
Layout.fillWidth: true
spacing: 12
// Avatar circle
Rectangle {
width: 40
height: 40
radius: 20
color: model.avatarColor
CText {
anchors.centerIn: parent
variant: "caption"
text: model.initials
color: "#ffffff"
font.bold: true
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
FlexRow {
spacing: 8
CText { variant: "subtitle1"; text: "@" + model.username }
CText { variant: "caption"; text: model.timestamp; opacity: 0.5 }
}
}
Item { Layout.fillWidth: true }
CBadge {
text: "Following"
accent: true
visible: model.following
}
}
CText {
variant: "body1"
text: model.body
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
CDivider { Layout.fillWidth: true }
FlexRow {
Layout.fillWidth: true
spacing: 16
CButton {
text: (model.liked ? "\u2665 " : "\u2661 ") + model.likes
variant: "ghost"
size: "sm"
onClicked: root.toggleLike(index)
}
CButton {
text: "\uD83D\uDCAC " + model.replies
variant: "ghost"
size: "sm"
}
Item { Layout.fillWidth: true }
CButton {
text: "Share"
variant: "ghost"
size: "sm"
}
}
}
}
}
}
}
}
@@ -1,50 +1,224 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CPaper {
id: packageCard
width: 460
height: 320
property string title: "Retro Arcade"
property string subtitle: "v1.0.0"
property var dependenciesList: ["gallery"]
Rectangle {
id: root
color: "transparent"
ColumnLayout {
property var games: [
{ name: "Snake", color: "#2ecc71", highScore: 4250, implemented: true, icon: "\u2588\u2588\u2588" },
{ name: "Breakout", color: "#3498db", highScore: 3800, implemented: true, icon: "\u2580\u2580\u2580" },
{ name: "Tetris", color: "#9b59b6", highScore: 12400, implemented: true, icon: "\u2587\u2587\u2587" },
{ name: "Pong", color: "#e67e22", highScore: 2100, implemented: true, icon: "\u2503 \u25CF \u2503" },
{ name: "Space Invaders", color: "#e74c3c", highScore: 8900, implemented: false, icon: "\u25BD\u25BD\u25BD" },
{ name: "Pac-Man", color: "#f1c40f", highScore: 6750, implemented: false, icon: "\u25D0 \u00B7\u00B7" }
]
property var leaderboard: [
{ rank: 1, player: "alice_dev", game: "Tetris", score: 12400 },
{ rank: 2, player: "bob_ops", game: "Space Invaders", score: 8900 },
{ rank: 3, player: "charlie_ui", game: "Pac-Man", score: 6750 },
{ rank: 4, player: "demo_user", game: "Snake", score: 4250 },
{ rank: 5, player: "diana_sec", game: "Breakout", score: 3800 }
]
function launchGame(gameName) {
console.log("Launching: " + gameName)
}
ScrollView {
anchors.fill: parent
anchors.margins: 18
spacing: 12
anchors.margins: 20
clip: true
RowLayout {
spacing: 12
CAvatar { initials: title.left(2).toUpper(); backgroundColor: Theme.primary }
ColumnLayout {
spacing: 4
CText { variant: "h3"; text: title }
CText { variant: "body1"; text: subtitle }
ColumnLayout {
width: parent.width
spacing: 16
// ── Header ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h2"; text: "Retro Arcade" }
CBadge { text: root.games.length + " Games" }
Item { Layout.fillWidth: true }
CStatusBadge { status: "success"; text: "Arcade Open" }
}
CText {
variant: "body2"
text: "Pixel-perfect retro games to keep you entertained. Select a game to play!"
opacity: 0.7
}
}
}
Item { Layout.fillWidth: true }
CBadge { text: dependenciesList.length ? "Dependency package" : "Standalone"; accent: dependenciesList.length > 0 }
}
Text {
text: "Pixel-perfect retro games to keep visitors entertained inside the app."
font.pixelSize: 14
color: Theme.text
wrapMode: Text.WordWrap
}
// ── Game Grid ──
GridLayout {
Layout.fillWidth: true
columns: 3
columnSpacing: 12
rowSpacing: 12
RowLayout {
spacing: 8
CChip { text: "Adaptive layout" }
CChip { text: "Realtime telemetry" }
CChip { text: "Community moderation" }
}
Repeater {
model: root.games
RowLayout {
spacing: 8
CButton { text: "Install" }
CButton { text: "Dependency graph"; variant: "ghost" }
CCard {
Layout.fillWidth: true
Layout.preferredWidth: 200
Layout.preferredHeight: 250
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 10
// Pixel art placeholder
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 80
radius: 8
color: modelData.color
opacity: modelData.implemented ? 0.9 : 0.4
CText {
anchors.centerIn: parent
text: modelData.icon
font.pixelSize: 24
font.family: "monospace"
color: "#ffffff"
}
// Coming Soon overlay
Rectangle {
anchors.fill: parent
radius: 8
color: "#000000"
opacity: modelData.implemented ? 0 : 0.5
visible: !modelData.implemented
}
CBadge {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 6
text: "Coming Soon"
visible: !modelData.implemented
}
}
CText {
variant: "subtitle1"
text: modelData.name
Layout.fillWidth: true
}
FlexRow {
Layout.fillWidth: true
spacing: 6
CText { variant: "caption"; text: "High Score:" }
CText {
variant: "caption"
text: modelData.highScore.toLocaleString()
color: Theme.primary
font.bold: true
}
}
Item { Layout.fillHeight: true }
CButton {
text: modelData.implemented ? "Play" : "Coming Soon"
Layout.fillWidth: true
enabled: modelData.implemented
onClicked: root.launchGame(modelData.name)
}
}
}
}
}
// ── Leaderboard ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "h3"; text: "Leaderboard" }
CBadge { text: "Top 5" }
}
CDivider { Layout.fillWidth: true }
// Header row
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "caption"; text: "Rank"; Layout.preferredWidth: 50; font.bold: true }
CText { variant: "caption"; text: "Player"; Layout.fillWidth: true; font.bold: true }
CText { variant: "caption"; text: "Game"; Layout.preferredWidth: 140; font.bold: true }
CText { variant: "caption"; text: "Score"; Layout.preferredWidth: 80; font.bold: true; horizontalAlignment: Text.AlignRight }
}
Repeater {
model: root.leaderboard
FlexRow {
Layout.fillWidth: true
spacing: 8
// Rank medal
Rectangle {
width: 28
height: 28
radius: 14
Layout.preferredWidth: 50
color: modelData.rank === 1 ? "#f1c40f" : modelData.rank === 2 ? "#bdc3c7" : modelData.rank === 3 ? "#cd7f32" : "transparent"
border.color: Theme.border
border.width: modelData.rank > 3 ? 1 : 0
CText {
anchors.centerIn: parent
variant: "caption"
text: "#" + modelData.rank
font.bold: true
color: modelData.rank <= 3 ? "#ffffff" : Theme.text
}
}
CText { variant: "body2"; text: modelData.player; Layout.fillWidth: true }
CText { variant: "body2"; text: modelData.game; Layout.preferredWidth: 140 }
CText {
variant: "body2"
text: modelData.score.toLocaleString()
Layout.preferredWidth: 80
horizontalAlignment: Text.AlignRight
font.bold: true
color: Theme.primary
}
}
}
}
}
}
}
}
+371 -36
View File
@@ -1,50 +1,385 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QmlComponents 1.0
CPaper {
id: packageCard
width: 460
height: 320
property string title: "Watchtower"
property string subtitle: "v1.0.0"
property var dependenciesList: ["analytics", "god_panel"]
Rectangle {
id: root
color: "transparent"
ColumnLayout {
property bool autoRefresh: true
property int refreshInterval: 5
property int lastRefreshAgo: 0
property var services: [
{ name: "DBAL", status: "healthy", uptime: "14d 6h 32m", responseMs: 12, lastCheck: "08:14:00", icon: "DB" },
{ name: "Media", status: "healthy", uptime: "14d 6h 30m", responseMs: 45, lastCheck: "08:14:02", icon: "MD" },
{ name: "Redis", status: "healthy", uptime: "14d 6h 28m", responseMs: 3, lastCheck: "08:14:01", icon: "RD" },
{ name: "Postgres", status: "degraded", uptime: "7d 12h 15m", responseMs: 128, lastCheck: "08:13:58", icon: "PG" },
{ name: "Nginx", status: "healthy", uptime: "14d 6h 32m", responseMs: 8, lastCheck: "08:14:03", icon: "NX" }
]
property var metrics: ({
cpu: 34,
memory: 67,
disk: 52
})
ListModel {
id: alertLog
ListElement { severity: "warning"; message: "Postgres response time elevated (128ms)"; timestamp: "08:13:58" }
ListElement { severity: "info"; message: "Redis cache hit rate: 94.2%"; timestamp: "08:12:30" }
ListElement { severity: "error"; message: "Media service timeout on /api/v1/transcode (recovered)"; timestamp: "08:10:15" }
ListElement { severity: "warning"; message: "Disk usage crossed 50% threshold"; timestamp: "08:05:00" }
ListElement { severity: "info"; message: "DBAL auto-seed completed successfully"; timestamp: "07:45:22" }
ListElement { severity: "info"; message: "System startup complete — all services initialized"; timestamp: "07:40:00" }
ListElement { severity: "error"; message: "Failed health check on Postgres (timeout), auto-retried OK"; timestamp: "07:38:45" }
ListElement { severity: "warning"; message: "Memory usage spike to 78% during backup (normalized)"; timestamp: "06:30:10" }
]
Timer {
id: refreshTimer
interval: root.refreshInterval * 1000
running: root.autoRefresh
repeat: true
onTriggered: {
root.lastRefreshAgo = 0
// Simulate small metric fluctuations
var m = Object.assign({}, root.metrics)
m.cpu = Math.max(5, Math.min(95, m.cpu + Math.floor(Math.random() * 11) - 5))
m.memory = Math.max(20, Math.min(95, m.memory + Math.floor(Math.random() * 7) - 3))
root.metrics = m
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: root.lastRefreshAgo++
}
function statusColor(s) {
if (s === "healthy") return "#2ecc71"
if (s === "degraded") return "#f39c12"
return "#e74c3c"
}
function statusLabel(s) {
if (s === "healthy") return "success"
if (s === "degraded") return "warning"
return "error"
}
function severityColor(s) {
if (s === "error") return "#e74c3c"
if (s === "warning") return "#f39c12"
return "#3498db"
}
function metricColor(pct) {
if (pct >= 80) return "#e74c3c"
if (pct >= 60) return "#f39c12"
return "#2ecc71"
}
function healthyCount() {
var c = 0
for (var i = 0; i < services.length; i++)
if (services[i].status === "healthy") c++
return c
}
function manualRefresh() {
root.lastRefreshAgo = 0
var m = Object.assign({}, root.metrics)
m.cpu = Math.max(5, Math.min(95, m.cpu + Math.floor(Math.random() * 11) - 5))
m.memory = Math.max(20, Math.min(95, m.memory + Math.floor(Math.random() * 7) - 3))
root.metrics = m
}
ScrollView {
anchors.fill: parent
anchors.margins: 18
spacing: 12
anchors.margins: 20
clip: true
RowLayout {
spacing: 12
CAvatar { initials: title.left(2).toUpper(); backgroundColor: Theme.primary }
ColumnLayout {
spacing: 4
CText { variant: "h3"; text: title }
CText { variant: "body1"; text: subtitle }
ColumnLayout {
width: parent.width
spacing: 16
// ── Header ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 12
CText { variant: "h2"; text: "Watchtower" }
CStatusBadge {
status: healthyCount() === services.length ? "success" : "warning"
text: healthyCount() + "/" + services.length + " Healthy"
}
Item { Layout.fillWidth: true }
CText {
variant: "caption"
text: "Updated " + root.lastRefreshAgo + "s ago"
opacity: 0.6
}
CButton {
text: "Refresh"
variant: "ghost"
size: "sm"
onClicked: root.manualRefresh()
}
// Auto-refresh toggle
FlexRow {
spacing: 6
CText { variant: "caption"; text: "Auto" }
Switch {
checked: root.autoRefresh
onCheckedChanged: root.autoRefresh = checked
}
}
}
}
}
Item { Layout.fillWidth: true }
CBadge { text: dependenciesList.length ? "Dependency package" : "Standalone"; accent: dependenciesList.length > 0 }
}
Text {
text: "Mission control for logging, alerts, and daemon orchestration."
font.pixelSize: 14
color: Theme.text
wrapMode: Text.WordWrap
}
// ── Service Health Grid ──
CText { variant: "h3"; text: "Service Health" }
RowLayout {
spacing: 8
CChip { text: "Adaptive layout" }
CChip { text: "Realtime telemetry" }
CChip { text: "Community moderation" }
}
GridLayout {
Layout.fillWidth: true
columns: 3
columnSpacing: 12
rowSpacing: 12
RowLayout {
spacing: 8
CButton { text: "Install" }
CButton { text: "Dependency graph"; variant: "ghost" }
Repeater {
model: root.services
CCard {
Layout.fillWidth: true
Layout.preferredWidth: 200
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 8
FlexRow {
Layout.fillWidth: true
spacing: 10
CAvatar {
initials: modelData.icon
backgroundColor: statusColor(modelData.status)
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
CText { variant: "subtitle1"; text: modelData.name }
CStatusBadge { status: statusLabel(modelData.status); text: modelData.status }
}
}
CDivider { Layout.fillWidth: true }
FlexRow {
Layout.fillWidth: true
spacing: 4
CText { variant: "caption"; text: "Uptime:" }
CText { variant: "caption"; text: modelData.uptime; font.bold: true }
}
FlexRow {
Layout.fillWidth: true
spacing: 4
CText { variant: "caption"; text: "Response:" }
CText {
variant: "caption"
text: modelData.responseMs + "ms"
font.bold: true
color: modelData.responseMs > 100 ? "#f39c12" : "#2ecc71"
}
}
FlexRow {
Layout.fillWidth: true
spacing: 4
CText { variant: "caption"; text: "Last check:" }
CText { variant: "caption"; text: modelData.lastCheck; opacity: 0.6 }
}
}
}
}
}
// ── System Metrics ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 16
CText { variant: "h3"; text: "System Metrics" }
// CPU
ColumnLayout {
Layout.fillWidth: true
spacing: 4
FlexRow {
Layout.fillWidth: true
CText { variant: "body2"; text: "CPU Usage" }
Item { Layout.fillWidth: true }
CText { variant: "body2"; text: root.metrics.cpu + "%"; font.bold: true; color: metricColor(root.metrics.cpu) }
}
Rectangle {
Layout.fillWidth: true
height: 10
radius: 5
color: Theme.border
Rectangle {
width: parent.width * root.metrics.cpu / 100
height: parent.height
radius: 5
color: metricColor(root.metrics.cpu)
Behavior on width { NumberAnimation { duration: 300 } }
}
}
}
// Memory
ColumnLayout {
Layout.fillWidth: true
spacing: 4
FlexRow {
Layout.fillWidth: true
CText { variant: "body2"; text: "Memory Usage" }
Item { Layout.fillWidth: true }
CText { variant: "body2"; text: root.metrics.memory + "%"; font.bold: true; color: metricColor(root.metrics.memory) }
}
Rectangle {
Layout.fillWidth: true
height: 10
radius: 5
color: Theme.border
Rectangle {
width: parent.width * root.metrics.memory / 100
height: parent.height
radius: 5
color: metricColor(root.metrics.memory)
Behavior on width { NumberAnimation { duration: 300 } }
}
}
}
// Disk
ColumnLayout {
Layout.fillWidth: true
spacing: 4
FlexRow {
Layout.fillWidth: true
CText { variant: "body2"; text: "Disk Usage" }
Item { Layout.fillWidth: true }
CText { variant: "body2"; text: root.metrics.disk + "%"; font.bold: true; color: metricColor(root.metrics.disk) }
}
Rectangle {
Layout.fillWidth: true
height: 10
radius: 5
color: Theme.border
Rectangle {
width: parent.width * root.metrics.disk / 100
height: parent.height
radius: 5
color: metricColor(root.metrics.disk)
Behavior on width { NumberAnimation { duration: 300 } }
}
}
}
}
}
// ── Alert Log ──
CCard {
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
FlexRow {
Layout.fillWidth: true
spacing: 8
CText { variant: "h3"; text: "Alert Log" }
CBadge { text: alertLog.count + " entries" }
Item { Layout.fillWidth: true }
CButton {
text: "Clear Log"
variant: "ghost"
size: "sm"
onClicked: alertLog.clear()
}
}
CDivider { Layout.fillWidth: true }
Repeater {
model: alertLog
FlexRow {
Layout.fillWidth: true
spacing: 10
// Severity dot
Rectangle {
width: 10
height: 10
radius: 5
color: severityColor(model.severity)
Layout.alignment: Qt.AlignVCenter
}
CText {
variant: "caption"
text: model.timestamp
Layout.preferredWidth: 70
opacity: 0.6
}
CBadge {
text: model.severity
accent: model.severity === "error"
}
CText {
variant: "body2"
text: model.message
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
}
}
}
}
}
}
+41 -92
View File
@@ -35,9 +35,10 @@ import QtQuick
Item {
id: root
// Configuration
property string baseUrl: "http://localhost:3001/api/dbal"
// Configuration — DBAL REST: /api/v1/{tenant}/{package}/{entity}[/{id}]
property string baseUrl: "http://localhost:8080"
property string tenantId: "default"
property string packageId: "core"
property string authToken: ""
// State
@@ -99,111 +100,59 @@ Item {
}
}
// Public API
/**
* Create a new record
* @param {string} entity - Entity name (e.g., "User", "AuditLog")
* @param {object} data - Record data
* @param {function} callback - Callback(result, error)
*/
// REST path helpers
function entityPath(entity) {
return "/api/v1/" + tenantId + "/" + packageId + "/" + entity.toLowerCase()
}
function entityPathWithId(entity, id) {
return entityPath(entity) + "/" + id
}
// Public API — DBAL REST: /api/v1/{tenant}/{package}/{entity}[/{id}]
function create(entity, data, callback) {
internal.request("POST", "/create", {
entity: entity,
data: data,
tenantId: tenantId
}, callback)
internal.request("POST", entityPath(entity), data, callback)
}
/**
* Read a single record by ID
* @param {string} entity - Entity name
* @param {string} id - Record ID
* @param {function} callback - Callback(result, error)
*/
function read(entity, id, callback) {
internal.request("GET", "/read/" + entity + "/" + id, null, callback)
internal.request("GET", entityPathWithId(entity, id), null, callback)
}
/**
* Update an existing record
* @param {string} entity - Entity name
* @param {string} id - Record ID
* @param {object} data - Updated fields
* @param {function} callback - Callback(result, error)
*/
function update(entity, id, data, callback) {
internal.request("PUT", "/update", {
entity: entity,
id: id,
data: data
}, callback)
internal.request("PUT", entityPathWithId(entity, id), data, callback)
}
/**
* Delete a record
* @param {string} entity - Entity name
* @param {string} id - Record ID
* @param {function} callback - Callback(success, error)
*/
function remove(entity, id, callback) {
internal.request("DELETE", "/delete/" + entity + "/" + id, null, callback)
internal.request("DELETE", entityPathWithId(entity, id), null, callback)
}
/**
* List records with pagination and filtering
* @param {string} entity - Entity name
* @param {object} options - { take, skip, where, orderBy }
* @param {function} callback - Callback({ items, total }, error)
*/
function list(entity, options, callback) {
var body = {
entity: entity,
tenantId: tenantId
}
if (options.take !== undefined) body.take = options.take
if (options.skip !== undefined) body.skip = options.skip
if (options.where !== undefined) body.where = options.where
if (options.orderBy !== undefined) body.orderBy = options.orderBy
internal.request("POST", "/list", body, callback)
var path = entityPath(entity)
var queryParts = []
if (options.take !== undefined) queryParts.push("take=" + options.take)
if (options.skip !== undefined) queryParts.push("skip=" + options.skip)
if (options.orderBy !== undefined) queryParts.push("orderBy=" + options.orderBy)
if (queryParts.length > 0) path += "?" + queryParts.join("&")
internal.request("GET", path, null, callback)
}
/**
* Find first record matching filter
* @param {string} entity - Entity name
* @param {object} filter - Filter criteria
* @param {function} callback - Callback(result, error)
*/
function findFirst(entity, filter, callback) {
internal.request("POST", "/findFirst", {
entity: entity,
tenantId: tenantId,
filter: filter
}, callback)
var path = entityPath(entity) + "?take=1"
for (var key in filter) {
path += "&" + encodeURIComponent(key) + "=" + encodeURIComponent(filter[key])
}
internal.request("GET", path, null, callback)
}
/**
* Execute a named operation
* @param {string} operation - Operation name
* @param {object} params - Operation parameters
* @param {function} callback - Callback(result, error)
*/
function execute(operation, params, callback) {
internal.request("POST", "/execute", {
operation: operation,
params: params,
tenantId: tenantId
}, callback)
var path = "/api/v1/" + tenantId + "/" + operation
internal.request("POST", path, params, callback)
}
/**
* Check connection to DBAL backend
* @param {function} callback - Callback(success, error)
*/
function ping(callback) {
internal.request("GET", "/ping", null, function(result, error) {
internal.request("GET", "/health", null, function(result, error) {
root.connected = !error
if (callback) callback(!error, error)
})
+75 -42
View File
@@ -7,8 +7,9 @@
DBALClient::DBALClient(QObject *parent)
: QObject(parent)
, m_networkManager(new QNetworkAccessManager(this))
, m_baseUrl("http://localhost:3001/api/dbal")
, m_baseUrl("http://localhost:8080")
, m_tenantId("default")
, m_packageId("core")
, m_connected(false)
{
connect(m_networkManager, &QNetworkAccessManager::finished,
@@ -45,6 +46,25 @@ void DBALClient::setAuthToken(const QString &token)
}
}
void DBALClient::setPackageId(const QString &pkg)
{
if (m_packageId != pkg) {
m_packageId = pkg;
emit packageIdChanged();
}
}
QString DBALClient::entityPath(const QString &entity) const
{
// REST: /api/v1/{tenant}/{package}/{entity}
return QString("/api/v1/%1/%2/%3").arg(m_tenantId, m_packageId, entity.toLower());
}
QString DBALClient::entityPath(const QString &entity, const QString &id) const
{
return entityPath(entity) + "/" + id;
}
void DBALClient::setError(const QString &error)
{
m_lastError = error;
@@ -125,76 +145,89 @@ void DBALClient::handleNetworkReply(QNetworkReply *reply)
}
}
// CRUD Operations
// CRUD Operations — DBAL REST API: /api/v1/{tenant}/{package}/{entity}[/{id}]
void DBALClient::create(const QString &entity, const QJsonObject &data, const QJSValue &callback)
{
QJsonObject body;
body["entity"] = entity;
body["data"] = data;
body["tenantId"] = m_tenantId;
sendRequest("POST", "/create", body, callback);
sendRequest("POST", entityPath(entity), data, callback);
}
void DBALClient::read(const QString &entity, const QString &id, const QJSValue &callback)
{
QString endpoint = QString("/read/%1/%2").arg(entity, id);
sendRequest("GET", endpoint, QJsonObject(), callback);
sendRequest("GET", entityPath(entity, id), QJsonObject(), callback);
}
void DBALClient::update(const QString &entity, const QString &id,
void DBALClient::update(const QString &entity, const QString &id,
const QJsonObject &data, const QJSValue &callback)
{
QJsonObject body;
body["entity"] = entity;
body["id"] = id;
body["data"] = data;
sendRequest("PUT", "/update", body, callback);
sendRequest("PUT", entityPath(entity, id), data, callback);
}
void DBALClient::remove(const QString &entity, const QString &id, const QJSValue &callback)
{
QString endpoint = QString("/delete/%1/%2").arg(entity, id);
sendRequest("DELETE", endpoint, QJsonObject(), callback);
sendRequest("DELETE", entityPath(entity, id), QJsonObject(), callback);
}
void DBALClient::list(const QString &entity, const QJsonObject &options, const QJSValue &callback)
{
QJsonObject body;
body["entity"] = entity;
body["tenantId"] = m_tenantId;
if (options.contains("take")) body["take"] = options["take"];
if (options.contains("skip")) body["skip"] = options["skip"];
if (options.contains("where")) body["where"] = options["where"];
if (options.contains("orderBy")) body["orderBy"] = options["orderBy"];
sendRequest("POST", "/list", body, callback);
// Build query string from options (take, skip, where, orderBy)
QString path = entityPath(entity);
QStringList queryParts;
if (options.contains("take")) queryParts << "take=" + QString::number(options["take"].toInt());
if (options.contains("skip")) queryParts << "skip=" + QString::number(options["skip"].toInt());
if (options.contains("orderBy")) queryParts << "orderBy=" + options["orderBy"].toString();
if (!queryParts.isEmpty()) path += "?" + queryParts.join("&");
sendRequest("GET", path, QJsonObject(), callback);
}
void DBALClient::findFirst(const QString &entity, const QJsonObject &filter, const QJSValue &callback)
{
QJsonObject body;
body["entity"] = entity;
body["tenantId"] = m_tenantId;
body["filter"] = filter;
sendRequest("POST", "/findFirst", body, callback);
// GET with query params for simple filters
QString path = entityPath(entity);
QStringList queryParts;
queryParts << "take=1";
for (auto it = filter.begin(); it != filter.end(); ++it) {
queryParts << QUrl::toPercentEncoding(it.key()) + "=" + QUrl::toPercentEncoding(it.value().toString());
}
if (!queryParts.isEmpty()) path += "?" + queryParts.join("&");
sendRequest("GET", path, QJsonObject(), callback);
}
void DBALClient::execute(const QString &operation, const QJsonObject &params, const QJSValue &callback)
{
QJsonObject body;
body["operation"] = operation;
body["params"] = params;
body["tenantId"] = m_tenantId;
sendRequest("POST", "/execute", body, callback);
// Execute maps to POST /{tenant}/{package}/{entity}/{action} or system endpoints
QString path = QString("/api/v1/%1/%2").arg(m_tenantId, operation);
sendRequest("POST", path, params, callback);
}
void DBALClient::ping()
{
sendRequest("GET", "/ping", QJsonObject(), QJSValue());
sendRequest("GET", "/health", QJsonObject(), QJSValue());
}
void DBALClient::health(const QJSValue &callback)
{
sendRequest("GET", "/health", QJsonObject(), callback);
}
void DBALClient::version(const QJSValue &callback)
{
sendRequest("GET", "/version", QJsonObject(), callback);
}
void DBALClient::status(const QJSValue &callback)
{
sendRequest("GET", "/status", QJsonObject(), callback);
}
void DBALClient::listSchemas(const QJSValue &callback)
{
sendRequest("GET", "/api/v1/" + m_tenantId + "/schema", QJsonObject(), callback);
}
void DBALClient::getSchema(const QString &entity, const QJSValue &callback)
{
sendRequest("GET", "/api/v1/" + m_tenantId + "/schema/" + entity.toLower(), QJsonObject(), callback);
}
+39
View File
@@ -37,6 +37,7 @@ class DBALClient : public QObject
Q_OBJECT
Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
Q_PROPERTY(QString tenantId READ tenantId WRITE setTenantId NOTIFY tenantIdChanged)
Q_PROPERTY(QString packageId READ packageId WRITE setPackageId NOTIFY packageIdChanged)
Q_PROPERTY(QString authToken READ authToken WRITE setAuthToken NOTIFY authTokenChanged)
Q_PROPERTY(bool connected READ isConnected NOTIFY connectedChanged)
Q_PROPERTY(QString lastError READ lastError NOTIFY errorOccurred)
@@ -48,6 +49,7 @@ public:
// Property getters
QString baseUrl() const { return m_baseUrl; }
QString tenantId() const { return m_tenantId; }
QString packageId() const { return m_packageId; }
QString authToken() const { return m_authToken; }
bool isConnected() const { return m_connected; }
QString lastError() const { return m_lastError; }
@@ -55,6 +57,7 @@ public:
// Property setters
void setBaseUrl(const QString &url);
void setTenantId(const QString &id);
void setPackageId(const QString &pkg);
void setAuthToken(const QString &token);
public slots:
@@ -120,9 +123,41 @@ public slots:
*/
void ping();
/**
* @brief Get DBAL health information
* @param callback QML callback function(result)
*/
void health(const QJSValue &callback);
/**
* @brief Get DBAL version information
* @param callback QML callback function(result)
*/
void version(const QJSValue &callback);
/**
* @brief Get DBAL status/metrics
* @param callback QML callback function(result)
*/
void status(const QJSValue &callback);
/**
* @brief List entity schemas from DBAL
* @param callback QML callback function(schemas)
*/
void listSchemas(const QJSValue &callback);
/**
* @brief Get a specific entity schema
* @param entity Entity name
* @param callback QML callback function(schema)
*/
void getSchema(const QString &entity, const QJSValue &callback);
signals:
void baseUrlChanged();
void tenantIdChanged();
void packageIdChanged();
void authTokenChanged();
void connectedChanged();
void errorOccurred(const QString &error);
@@ -139,7 +174,11 @@ private:
QNetworkAccessManager *m_networkManager;
QString m_baseUrl;
QString m_tenantId;
QString m_packageId;
QString m_authToken;
QString entityPath(const QString &entity) const;
QString entityPath(const QString &entity, const QString &id) const;
bool m_connected;
QString m_lastError;
QMap<QNetworkReply*, QJSValue> m_pendingCallbacks;
+281
View File
@@ -0,0 +1,281 @@
#include "PackageLoader.h"
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonArray>
#include <QDebug>
PackageLoader::PackageLoader(QObject *parent)
: QObject(parent)
, m_packagesDir(QDir(QStringLiteral(SRCDIR) + QStringLiteral("/packages")).absolutePath())
, m_watching(false)
, m_watcher(nullptr)
{
}
PackageLoader::~PackageLoader()
{
delete m_watcher;
}
// ---------------------------------------------------------------------------
// Properties
// ---------------------------------------------------------------------------
QVariantList PackageLoader::packages() const
{
QVariantList list;
for (auto it = m_packages.constBegin(); it != m_packages.constEnd(); ++it) {
QVariantMap entry = it.value().toVariantMap();
entry[QStringLiteral("installed")] = m_installed.value(it.key(), false);
list.append(entry);
}
return list;
}
void PackageLoader::setPackagesDir(const QString &dir)
{
if (m_packagesDir == dir)
return;
m_packagesDir = dir;
emit packagesDirChanged();
}
void PackageLoader::setWatching(bool enabled)
{
if (m_watching == enabled)
return;
m_watching = enabled;
if (m_watching) {
if (!m_watcher) {
m_watcher = new QFileSystemWatcher(this);
connect(m_watcher, &QFileSystemWatcher::directoryChanged,
this, &PackageLoader::onDirectoryChanged);
connect(m_watcher, &QFileSystemWatcher::fileChanged,
this, &PackageLoader::onFileChanged);
}
// Watch the root packages directory
if (QDir(m_packagesDir).exists())
m_watcher->addPath(m_packagesDir);
// Watch each package subdirectory and its metadata.json
QDir root(m_packagesDir);
const auto entries = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &entry : entries) {
const QString pkgDir = root.absoluteFilePath(entry);
m_watcher->addPath(pkgDir);
const QString metaPath = pkgDir + QStringLiteral("/metadata.json");
if (QFile::exists(metaPath))
m_watcher->addPath(metaPath);
}
} else {
delete m_watcher;
m_watcher = nullptr;
}
emit watchingChanged();
}
// ---------------------------------------------------------------------------
// Scanning
// ---------------------------------------------------------------------------
void PackageLoader::scan()
{
m_packages.clear();
QDir root(m_packagesDir);
if (!root.exists()) {
qWarning() << "PackageLoader::scan packages directory does not exist:" << m_packagesDir;
emit packagesChanged();
return;
}
const auto entries = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &entry : entries) {
loadPackage(root.absoluteFilePath(entry));
}
emit packagesChanged();
}
void PackageLoader::loadPackage(const QString &dir)
{
const QString metaPath = dir + QStringLiteral("/metadata.json");
QFile file(metaPath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
// Not every subdirectory is necessarily a package skip silently
return;
}
QJsonParseError parseErr;
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseErr);
file.close();
if (parseErr.error != QJsonParseError::NoError) {
qWarning() << "PackageLoader: JSON parse error in" << metaPath << parseErr.errorString();
return;
}
const QJsonObject meta = doc.object();
if (!validateMetadata(meta)) {
qWarning() << "PackageLoader: invalid metadata in" << metaPath;
return;
}
const QString id = meta.contains(QStringLiteral("id"))
? meta.value(QStringLiteral("id")).toString()
: meta.value(QStringLiteral("packageId")).toString();
// Inject the absolute directory path so QML can locate assets
QJsonObject enriched = meta;
enriched[QStringLiteral("_dir")] = dir;
m_packages.insert(id, enriched);
}
bool PackageLoader::validateMetadata(const QJsonObject &metadata) const
{
return (metadata.contains(QStringLiteral("id")) || metadata.contains(QStringLiteral("packageId")))
&& metadata.contains(QStringLiteral("name"))
&& metadata.contains(QStringLiteral("version"));
}
// ---------------------------------------------------------------------------
// Install / uninstall
// ---------------------------------------------------------------------------
void PackageLoader::install(const QString &packageId)
{
if (!m_packages.contains(packageId)) {
qWarning() << "PackageLoader::install unknown package:" << packageId;
return;
}
m_installed[packageId] = true;
emit packageInstalled(packageId);
emit packagesChanged();
}
void PackageLoader::uninstall(const QString &packageId)
{
if (!m_installed.contains(packageId))
return;
m_installed.remove(packageId);
emit packageUninstalled(packageId);
emit packagesChanged();
}
bool PackageLoader::isInstalled(const QString &packageId) const
{
return m_installed.value(packageId, false);
}
QVariantMap PackageLoader::getPackage(const QString &packageId) const
{
if (!m_packages.contains(packageId))
return {};
return m_packages.value(packageId).toVariantMap();
}
// ---------------------------------------------------------------------------
// Dependencies
// ---------------------------------------------------------------------------
QStringList PackageLoader::resolveDependencies(const QString &packageId) const
{
QStringList visited;
return resolveDepChain(packageId, visited);
}
QStringList PackageLoader::resolveDepChain(const QString &packageId, QStringList &visited) const
{
if (visited.contains(packageId))
return {};
visited.append(packageId);
if (!m_packages.contains(packageId))
return {};
QStringList result;
const QJsonObject &meta = m_packages.value(packageId);
const QJsonArray deps = meta.value(QStringLiteral("dependencies")).toArray();
for (const QJsonValue &dep : deps) {
const QString depId = dep.toString();
if (depId.isEmpty())
continue;
result.append(resolveDepChain(depId, visited));
result.append(depId);
}
return result;
}
// ---------------------------------------------------------------------------
// QML path helper
// ---------------------------------------------------------------------------
QString PackageLoader::qmlPath(const QString &packageId) const
{
if (!m_packages.contains(packageId))
return {};
const QString dir = m_packages.value(packageId).value(QStringLiteral("_dir")).toString();
return dir + QStringLiteral("/PackageView.qml");
}
// ---------------------------------------------------------------------------
// File system watcher slots
// ---------------------------------------------------------------------------
void PackageLoader::onDirectoryChanged(const QString &path)
{
emit fileChanged(path);
if (path == m_packagesDir) {
// A package may have been added or removed full rescan
scan();
return;
}
// A specific package directory changed reload that package
const QDir changedDir(path);
const QString metaPath = changedDir.absoluteFilePath(QStringLiteral("metadata.json"));
if (QFile::exists(metaPath)) {
loadPackage(path);
// Determine packageId from the reloaded data
const QDir d(path);
for (auto it = m_packages.constBegin(); it != m_packages.constEnd(); ++it) {
if (it.value().value(QStringLiteral("_dir")).toString() == path) {
emit packageUpdated(it.key());
break;
}
}
emit packagesChanged();
}
}
void PackageLoader::onFileChanged(const QString &path)
{
emit fileChanged(path);
// Re-read the metadata.json that was modified
QFileInfo fi(path);
const QString pkgDir = fi.absolutePath();
loadPackage(pkgDir);
for (auto it = m_packages.constBegin(); it != m_packages.constEnd(); ++it) {
if (it.value().value(QStringLiteral("_dir")).toString() == pkgDir) {
emit packageUpdated(it.key());
break;
}
}
emit packagesChanged();
// QFileSystemWatcher may drop the watch after a file change re-add
if (m_watcher && QFile::exists(path) && !m_watcher->files().contains(path))
m_watcher->addPath(path);
}
+67
View File
@@ -0,0 +1,67 @@
#ifndef PACKAGELOADER_H
#define PACKAGELOADER_H
#include <QObject>
#include <QJsonObject>
#include <QJsonArray>
#include <QString>
#include <QStringList>
#include <QFileSystemWatcher>
#include <QMap>
#include <QVariantList>
class PackageLoader : public QObject
{
Q_OBJECT
Q_PROPERTY(QVariantList packages READ packages NOTIFY packagesChanged)
Q_PROPERTY(QString packagesDir READ packagesDir WRITE setPackagesDir NOTIFY packagesDirChanged)
Q_PROPERTY(bool watching READ isWatching WRITE setWatching NOTIFY watchingChanged)
Q_PROPERTY(int packageCount READ packageCount NOTIFY packagesChanged)
public:
explicit PackageLoader(QObject *parent = nullptr);
~PackageLoader() override;
QVariantList packages() const;
QString packagesDir() const { return m_packagesDir; }
bool isWatching() const { return m_watching; }
int packageCount() const { return m_packages.count(); }
void setPackagesDir(const QString &dir);
void setWatching(bool enabled);
public slots:
void scan();
void install(const QString &packageId);
void uninstall(const QString &packageId);
bool isInstalled(const QString &packageId) const;
QVariantMap getPackage(const QString &packageId) const;
QStringList resolveDependencies(const QString &packageId) const;
QString qmlPath(const QString &packageId) const;
signals:
void packagesChanged();
void packagesDirChanged();
void watchingChanged();
void packageInstalled(const QString &packageId);
void packageUninstalled(const QString &packageId);
void packageUpdated(const QString &packageId);
void fileChanged(const QString &path);
private slots:
void onDirectoryChanged(const QString &path);
void onFileChanged(const QString &path);
private:
void loadPackage(const QString &dir);
bool validateMetadata(const QJsonObject &metadata) const;
QStringList resolveDepChain(const QString &packageId, QStringList &visited) const;
QString m_packagesDir;
bool m_watching;
QFileSystemWatcher *m_watcher;
QMap<QString, QJsonObject> m_packages;
QMap<QString, bool> m_installed;
};
#endif