Files
php-qml/spike/qt/Main.qml
magdev 9655b6fef9 Add Phase 0 spike: end-to-end transport verified
Bare PHP behind FrankenPHP plus a Qt/QML host that spawns it. GET /api/ping
publishes a Mercure event; the QML window receives it back over the SSE
stream. Findings (Caddy directive ordering, Mercure transport scalar,
PR_SET_PDEATHSIG for child cleanup, PHP 8.5 curl_close deprecation, port
collision with system FrankenPHP, pure-QML SSE viability) are recorded in
spike/README.md so Phase 1 starts from a known-good baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:15:50 +02:00

129 lines
3.8 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Window
ApplicationWindow {
id: window
width: 720
height: 520
visible: true
title: "php-qml — Phase 0 spike"
readonly property string baseUrl: "http://127.0.0.1:8765"
readonly property string topic: "app://ping"
property string status: "starting…"
property string lastResponse: ""
property string mercureState: "disconnected"
Mercure {
id: mercure
url: window.baseUrl + "/.well-known/mercure?topic=" + encodeURIComponent(window.topic)
onEventReceived: function(data, id) {
log.append("← event " + id.split(":").pop() + " " + data)
}
onStateChanged: function(s) {
window.mercureState = s
log.append("· mercure → " + s)
}
}
Timer {
id: bootProbe
interval: 250
running: window.status !== "online"
repeat: true
onTriggered: {
const xhr = new XMLHttpRequest()
xhr.open("GET", window.baseUrl + "/api/ping")
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
if (window.status !== "online") {
window.status = "online"
log.append("· backend ready")
mercure.connect()
}
}
}
xhr.send()
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
RowLayout {
spacing: 10
Layout.fillWidth: true
Rectangle {
width: 12; height: 12; radius: 6
color: window.status === "online" ? "#3ab36c" : "#d89614"
}
Label { text: "Backend: " + window.status }
Item { width: 12 }
Rectangle {
width: 12; height: 12; radius: 6
color: window.mercureState === "connected" ? "#3ab36c" : "#666"
}
Label { text: "Mercure: " + window.mercureState }
Item { Layout.fillWidth: true }
Button {
text: "Ping"
enabled: window.status === "online"
onClicked: {
const t0 = Date.now()
const xhr = new XMLHttpRequest()
xhr.open("GET", window.baseUrl + "/api/ping")
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
const dt = Date.now() - t0
if (xhr.status === 200) {
window.lastResponse = xhr.responseText
log.append("→ ping " + dt + "ms " + xhr.responseText)
} else {
log.append("× ping failed status=" + xhr.status)
}
}
}
xhr.send()
}
}
}
Label {
text: "Last response: " + (window.lastResponse || "—")
color: "#888"
font.pixelSize: 12
Layout.fillWidth: true
elide: Text.ElideRight
}
Frame {
Layout.fillWidth: true
Layout.fillHeight: true
padding: 0
ScrollView {
anchors.fill: parent
TextArea {
id: log
readOnly: true
wrapMode: TextArea.Wrap
font.family: "monospace"
font.pixelSize: 12
placeholderText: "events will appear here…"
}
}
}
}
}