fix: show the active diagnostics URL

This commit is contained in:
Franz Rolfsvaag 2026-07-18 16:56:40 +02:00
parent ccb8cb70be
commit 5c17408221
14 changed files with 87 additions and 33 deletions

View File

@ -1,5 +1,9 @@
# Lumi changelog
## 0.2.4
- Changed the production diagnostics endpoint, cURL example, and local-client configuration example to use the actual URL from which the administrator opened Lumi, including trusted reverse-proxy HTTPS handling.
## 0.2.3
- Fixed shared form action resolution so submit buttons without a `formaction` use their parent form's endpoint, restoring timed confirmations such as diagnostics-key creation.

View File

@ -677,6 +677,7 @@ This section is for Lumi chat answer feedback and AI Improvement Center work, no
## Done
- 2026-07-18: Updated production diagnostics examples in core 0.2.4 to derive the full endpoint and client base URL from the administrator's current Lumi request instead of showing a placeholder hostname.
- 2026-07-18: Fixed the shared submit-action resolver in core 0.2.3: ordinary buttons now inherit their parent form endpoint unless they explicitly declare `formaction`, restoring timed diagnostics-key creation and preventing async update actions from posting back to the Updates page.
- 2026-07-18: Added production-stage plugin update diagnostics in core 0.2.2: selected plugin source is verified before snapshotting, failures record their exact stage and target in update state, and the affected plugin row displays the server error directly.
- 2026-07-18: Added opt-in production diagnostics in core 0.2.2: administrators can issue or revoke a one-time access key for an HTTPS-only, rate-limited endpoint with five fixed read-only checks, recursive secret/path redaction, and audited access; no arbitrary command, SQL, file, URL, or write capability is exposed.

View File

@ -14,7 +14,7 @@ editable: false
Lumi is the core web UI and bot runtime.
## Runtime
Package: lumi-bot
Version: 0.2.3
Version: 0.2.4
## Routes
- GET /api/events
- POST /api/destructive-confirmations

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "lumi-bot",
"version": "0.2.3",
"version": "0.2.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "lumi-bot",
"version": "0.2.3",
"version": "0.2.4",
"dependencies": {
"adm-zip": "^0.5.12",
"better-sqlite3": "^11.5.0",

View File

@ -1,6 +1,6 @@
{
"name": "lumi-bot",
"version": "0.2.3",
"version": "0.2.4",
"private": true,
"type": "commonjs",
"scripts": {

View File

@ -2,6 +2,36 @@
"schema_version": 1,
"channel": "stable",
"releases": [
{
"version": "0.2.4",
"ref": "refs/tags/v0.2.4",
"released_at": "2026-07-18",
"installable": true,
"rollback_safe": true,
"replaces_versions": [
"1.2.0"
],
"data_policy": "preserve",
"dependency_policy": "sync_on_restart",
"migration_notes": "Uses the administrator's current request origin for production diagnostics endpoint and local-client examples.",
"plugins": {
"auto-vc": "0.1.6",
"birthday": "0.1.3",
"economy-framework": "0.2.10",
"economy-games": "0.1.7",
"expression-interaction": "0.2.1",
"lumi_ai": "0.8.2",
"moderation": "0.1.5",
"okf": "0.1.1",
"quotes": "0.1.2",
"sample-plugin": "0.1.0",
"throne_wishlist": "0.1.2",
"welcome_messages": "0.1.1"
},
"tools": {
"lumi_ai_web_search": "0.1.1"
}
},
{
"version": "0.2.3",
"ref": "refs/tags/v0.2.3",

View File

@ -4,7 +4,7 @@ const path = require("path");
const AdmZip = require("adm-zip");
const root = path.join(__dirname, "..");
const destination = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.3-repair.zip");
const destination = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.4-repair.zip");
const files = [
"CHANGELOG.md",
"README.md",
@ -53,10 +53,10 @@ for (const relativePath of files) {
const manifest = {
schema_version: 1,
name: "Lumi core 0.2.3 repair",
name: "Lumi core 0.2.4 repair",
target: "core",
from_versions: ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2", "0.2.3"],
to_version: "0.2.3",
from_versions: ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2", "0.2.3", "0.2.4"],
to_version: "0.2.4",
data_policy: "preserve",
dependency_policy: "sync_on_restart",
created_at: new Date().toISOString(),

View File

@ -7,7 +7,7 @@ const AdmZip = require("adm-zip");
const { verifyPatchPackage } = require("../src/services/update-manager");
const root = path.join(__dirname, "..");
const archivePath = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.3-repair.zip");
const archivePath = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.4-repair.zip");
assert.equal(fs.existsSync(archivePath), true, "build the repair patch first");
const zip = new AdmZip(archivePath);
const entries = zip.getEntries().filter((entry) => !entry.isDirectory);
@ -15,9 +15,9 @@ const names = new Set(entries.map((entry) => entry.entryName.replace(/\\/g, "/")
assert.equal(names.has("patch-manifest.json"), true);
const manifest = JSON.parse(zip.readAsText("patch-manifest.json"));
assert.equal(manifest.target, "core");
assert.equal(manifest.to_version, "0.2.3");
assert.equal(manifest.to_version, "0.2.4");
assert.equal(manifest.data_policy, "preserve");
assert.deepEqual(manifest.from_versions, ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2", "0.2.3"]);
assert.deepEqual(manifest.from_versions, ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2", "0.2.3", "0.2.4"]);
const forbidden = /^(?:data|plugins|node_modules|config|storage|uploads|logs|database|databases|knowledge\/(?:community|corrections))(?:\/|$)|^\.env(?:\.|$)|^\.secrets$/;
for (const entry of entries) {
@ -29,7 +29,7 @@ for (const [relativePath, expected] of Object.entries(manifest.files)) {
assert.equal(actual, expected, `${relativePath} checksum`);
}
assert.equal(Object.keys(manifest.files).length + 1, entries.length, "every repair file must be checksummed");
assert.equal(JSON.parse(zip.readAsText("package.json")).version, "0.2.3");
assert.equal(JSON.parse(zip.readAsText("package.json")).version, "0.2.4");
const simulation = fs.mkdtempSync(path.join(os.tmpdir(), "lumi-repair-simulation-"));
try {
@ -47,11 +47,11 @@ try {
fs.writeFileSync(target, `preserve:${sentinel}`);
}
zip.extractAllTo(simulation, true);
assert.equal(verifyPatchPackage(simulation).to_version, "0.2.3");
assert.equal(verifyPatchPackage(simulation).to_version, "0.2.4");
for (const sentinel of sentinels) {
assert.equal(fs.readFileSync(path.join(simulation, sentinel), "utf8"), `preserve:${sentinel}`);
}
assert.equal(JSON.parse(fs.readFileSync(path.join(simulation, "package.json"), "utf8")).version, "0.2.3");
assert.equal(JSON.parse(fs.readFileSync(path.join(simulation, "package.json"), "utf8")).version, "0.2.4");
assert.equal(fs.existsSync(path.join(simulation, "src", "services", "dependency-manager.js")), true);
assert.equal(fs.existsSync(path.join(simulation, "src", "services", "production-diagnostics.js")), true);
assert.equal(fs.existsSync(path.join(simulation, "src", "web", "views", "admin-diagnostics.ejs")), true);

View File

@ -79,7 +79,10 @@ const viewSource = fs.readFileSync(path.join(root, "src", "web", "views", "admin
assert.match(viewSource, /no remote control/i);
assert.match(viewSource, /data-confirm-mode="modal"/);
assert.match(viewSource, /\.secrets/);
assert.match(viewSource, /\/api\/diagnostics\/v1\/run/);
assert.match(viewSource, /diagnosticsEndpointUrl/);
assert.match(viewSource, /diagnosticsBaseUrl/);
assert.doesNotMatch(viewSource, /https:\/\/your-lumi-host/);
assert.match(serverSource, /const diagnosticsBaseUrl = `\$\{req\.protocol\}:\/\/\$\{req\.get\("host"\)\}`/);
const clientSource = fs.readFileSync(path.join(root, "scripts", "production-diagnostics-client.js"), "utf8");
assert.match(clientSource, /\.secrets["'], "production-diagnostics\.json"/);

View File

@ -4,8 +4,8 @@ const path = require("path");
const { findSafeTarget } = require("../src/services/versioning");
const root = path.join(__dirname, "..");
const releaseVersion = "0.2.3";
const previousCoreVersion = "0.2.2";
const releaseVersion = "0.2.4";
const previousCoreVersion = "0.2.3";
const earliestCompatibleCoreVersion = "0.1.9";
const changedPlugins = {
"auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" },
@ -87,4 +87,4 @@ assert.equal(webSearch.minimum_lumi_version, "0.2.0");
assert.equal(webSearch.minimum_lumi_ai_version, changedPlugins.lumi_ai.to);
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
console.log("Release metadata verification passed: core 0.2.3 and 11 changed plugin/tool packages.");
console.log("Release metadata verification passed: core 0.2.4 and 11 changed plugin/tool packages.");

View File

@ -16,7 +16,7 @@ function readJson(relativePath) {
const releaseIndex = readJson("release-index.json");
const releaseVersions = releaseIndex.releases.map((release) => release.version);
assert.deepEqual(releaseVersions, ["0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
assert.deepEqual(releaseVersions, ["0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
for (const release of releaseIndex.releases) {
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
@ -35,8 +35,9 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) {
}
const baseTarget = {
current_version: "0.2.2",
current_version: "0.2.3",
available_versions: [
{ version: "0.2.4", ref: "refs/tags/v0.2.4", rollback_safe: true },
{ version: "0.2.3", ref: "refs/tags/v0.2.3", rollback_safe: true },
{ version: "0.2.2", ref: "refs/tags/v0.2.2", rollback_safe: true },
{ version: "0.2.1", ref: "refs/tags/v0.2.1", rollback_safe: true },
@ -63,7 +64,7 @@ const corrected = buildStatus({
channel: "stable"
});
assert.equal(corrected.version_correction, true);
assert.equal(corrected.safe_target_version, "0.2.3");
assert.equal(corrected.safe_target_version, "0.2.4");
assert.equal(corrected.update_available, true);
assert.equal(corrected.blocked, false);

View File

@ -5769,7 +5769,8 @@ function createWebServer({ loadPlugins, discordClient }) {
res.redirect("/admin/theming");
});
const renderDiagnosticsAdmin = (res, values = {}) => {
const renderDiagnosticsAdmin = (req, res, values = {}) => {
const diagnosticsBaseUrl = `${req.protocol}://${req.get("host")}`;
res.set("Cache-Control", "no-store");
res.render("admin-diagnostics", {
title: "Production diagnostics",
@ -5779,12 +5780,14 @@ function createWebServer({ loadPlugins, discordClient }) {
diagnosticResult: null,
diagnosticError: null,
selectedCheck: "system_health",
diagnosticsBaseUrl,
diagnosticsEndpointUrl: `${diagnosticsBaseUrl}/api/diagnostics/v1/run`,
...values
});
};
app.get("/admin/diagnostics", requireRole("admin"), (_req, res) => {
renderDiagnosticsAdmin(res);
app.get("/admin/diagnostics", requireRole("admin"), (req, res) => {
renderDiagnosticsAdmin(req, res);
});
app.post("/admin/diagnostics/run", requireRole("admin"), (req, res) => {
@ -5800,7 +5803,7 @@ function createWebServer({ loadPlugins, discordClient }) {
fingerprint: `admin:${req.session.user.id}`,
duration_ms: Date.now() - startedAt
});
renderDiagnosticsAdmin(res, { diagnosticResult, selectedCheck: check });
renderDiagnosticsAdmin(req, res, { diagnosticResult, selectedCheck: check });
} catch (error) {
auditDiagnosticRequest({
request_id: requestId,
@ -5810,13 +5813,13 @@ function createWebServer({ loadPlugins, discordClient }) {
reason: error.message,
duration_ms: Date.now() - startedAt
});
renderDiagnosticsAdmin(res, { diagnosticError: error.message, selectedCheck: check });
renderDiagnosticsAdmin(req, res, { diagnosticError: error.message, selectedCheck: check });
}
});
app.post("/admin/diagnostics/access/renew", requireRole("admin"), (_req, res) => {
app.post("/admin/diagnostics/access/renew", requireRole("admin"), (req, res) => {
const issuedKey = issueDiagnosticsAccessKey();
renderDiagnosticsAdmin(res, { issuedKey });
renderDiagnosticsAdmin(req, res, { issuedKey });
});
app.post("/admin/diagnostics/access/revoke", requireRole("admin"), (req, res) => {

View File

@ -16,7 +16,7 @@
<div><span>Remote access</span><strong><%= diagnosticsAccess.enabled ? "Enabled" : "Disabled" %></strong></div>
<div><span>Access key</span><strong><%= diagnosticsAccess.configured ? diagnosticsAccess.key_prefix : "Not created" %></strong></div>
<div><span>Created</span><strong><%= diagnosticsAccess.created_at ? new Date(diagnosticsAccess.created_at).toLocaleString() : "Never" %></strong></div>
<div><span>Endpoint</span><strong><code>/api/diagnostics/v1/run</code></strong></div>
<div><span>Endpoint</span><strong><code><%= diagnosticsEndpointUrl %></code></strong></div>
</div>
<% if (issuedKey) { %>
@ -76,12 +76,12 @@
<fieldset>
<legend>Connect from a trusted computer</legend>
<p>Use the public HTTPS address for this Lumi installation. Plain HTTP is accepted only from the same machine.</p>
<pre class="log-details">curl -X POST "https://your-lumi-host/api/diagnostics/v1/run" \
<pre class="log-details">curl -X POST "<%= diagnosticsEndpointUrl %>" \
-H "Authorization: Bearer $LUMI_DIAGNOSTICS_KEY" \
-H "Content-Type: application/json" \
--data '{"check":"update_state"}'</pre>
<p class="hint">Keep the key out of shell history, tickets, chat, and logs. A local environment variable or an ignored <code>.secrets</code> file is safer than placing it directly in a command.</p>
<p class="hint">Repository maintainers can save <code>{ "base_url": "https://your-lumi-host", "key": "…" }</code> in <code>.secrets/production-diagnostics.json</code>, then run <code>npm run diagnostics:production -- update_state</code>.</p>
<p class="hint">Repository maintainers can save <code>{ "base_url": "<%= diagnosticsBaseUrl %>", "key": "…" }</code> in <code>.secrets/production-diagnostics.json</code>, then run <code>npm run diagnostics:production -- update_state</code>.</p>
</fieldset>
</section>

View File

@ -1,6 +1,6 @@
{
"name": "Lumi Core",
"version": "0.2.3",
"version": "0.2.4",
"channel": "stable",
"released_at": "2026-07-18",
"compatible_from": "0.1.9",
@ -8,7 +8,7 @@
"replaces_versions": [
"1.2.0"
],
"migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics, and the shared form-action fix required by timed confirmations and repository updates. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, and secrets.",
"migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics with request-aware examples, and the shared form-action fix required by timed confirmations and repository updates. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, and secrets.",
"rollback_safe": true,
"requirements": [
"Node.js 18 or newer"
@ -61,6 +61,18 @@
],
"rollback_safe": true,
"migration_notes": "Fixes shared browser form endpoint resolution for timed confirmations and repository updates; preserved local data is not replaced."
},
{
"version": "0.2.4",
"channel": "stable",
"released_at": "2026-07-18",
"compatible_from": "0.1.9",
"migration_kind": "patch",
"replaces_versions": [
"1.2.0"
],
"rollback_safe": true,
"migration_notes": "Uses the administrator's current Lumi origin in production diagnostics examples; preserved local data is not replaced."
}
]
}