diff --git a/CHANGELOG.md b/CHANGELOG.md index 35dcd51..2af1e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Lumi changelog +## 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. +- Fixed the same browser behavior in asynchronous core/plugin update forms, which could submit to the Updates page itself and report a context-free failure without running the update. + ## 0.2.2 - Added stage-specific plugin update failures beside the affected plugin and persisted the production failure stage in update state. diff --git a/TODO.md b/TODO.md index e4040d3..ccded82 100644 --- a/TODO.md +++ b/TODO.md @@ -677,6 +677,7 @@ This section is for Lumi chat answer feedback and AI Improvement Center work, no ## Done +- 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. - 2026-07-18: Released the follow-up core 0.2.1 update-page fix: update checks now have one client-side handler, successful results use clear module-specific wording, and exact core/plugin version controls are collapsed and list only the selected module’s versions. diff --git a/docs/updates.md b/docs/updates.md index c98116e..d1458c2 100644 --- a/docs/updates.md +++ b/docs/updates.md @@ -160,6 +160,10 @@ Admin update actions publish Server-Sent Events through Core update success returns a five-second in-page notice before refresh/restart. Plugin update success updates progress for the affected plugin action without a whole-page refresh, then restarts Lumi so the selected plugin code is loaded. +Submit buttons inherit the endpoint and method from their parent form unless +they explicitly declare `formaction` or `formmethod`; this prevents browsers' +current-page fallback values from redirecting an update away from its apply +route. Plugin failures are shown directly beside the affected plugin instead of only changing the action button label. The error identifies the failed stage: diff --git a/knowledge/core/lumi-core.md b/knowledge/core/lumi-core.md index 8a0d341..42e0d20 100644 --- a/knowledge/core/lumi-core.md +++ b/knowledge/core/lumi-core.md @@ -14,7 +14,7 @@ editable: false Lumi is the core web UI and bot runtime. ## Runtime Package: lumi-bot -Version: 0.2.2 +Version: 0.2.3 ## Routes - GET /api/events - POST /api/destructive-confirmations diff --git a/package-lock.json b/package-lock.json index e9dbccd..8d2115d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lumi-bot", - "version": "0.2.2", + "version": "0.2.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lumi-bot", - "version": "0.2.2", + "version": "0.2.3", "dependencies": { "adm-zip": "^0.5.12", "better-sqlite3": "^11.5.0", diff --git a/package.json b/package.json index 3f59d3e..cd3cd1a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumi-bot", - "version": "0.2.2", + "version": "0.2.3", "private": true, "type": "commonjs", "scripts": { diff --git a/release-index.json b/release-index.json index 494fbd0..b30e37a 100644 --- a/release-index.json +++ b/release-index.json @@ -2,6 +2,36 @@ "schema_version": 1, "channel": "stable", "releases": [ + { + "version": "0.2.3", + "ref": "refs/tags/v0.2.3", + "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": "Fixes shared form endpoint resolution so timed confirmations and asynchronous repository updates reach their intended routes.", + "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.2", "ref": "refs/tags/v0.2.2", diff --git a/scripts/build-core-repair-patch.js b/scripts/build-core-repair-patch.js index 10a93e5..587e25b 100644 --- a/scripts/build-core-repair-patch.js +++ b/scripts/build-core-repair-patch.js @@ -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.2-repair.zip"); +const destination = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.3-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.2 repair", + name: "Lumi core 0.2.3 repair", target: "core", - from_versions: ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2"], - to_version: "0.2.2", + 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", data_policy: "preserve", dependency_policy: "sync_on_restart", created_at: new Date().toISOString(), diff --git a/scripts/verify-core-repair-patch.js b/scripts/verify-core-repair-patch.js index 0fc1522..e6c1182 100644 --- a/scripts/verify-core-repair-patch.js +++ b/scripts/verify-core-repair-patch.js @@ -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.2-repair.zip"); +const archivePath = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.3-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.2"); +assert.equal(manifest.to_version, "0.2.3"); 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"]); +assert.deepEqual(manifest.from_versions, ["0.1.9", "1.2.0", "0.2.0", "0.2.1", "0.2.2", "0.2.3"]); 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.2"); +assert.equal(JSON.parse(zip.readAsText("package.json")).version, "0.2.3"); 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.2"); + assert.equal(verifyPatchPackage(simulation).to_version, "0.2.3"); 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.2"); + assert.equal(JSON.parse(fs.readFileSync(path.join(simulation, "package.json"), "utf8")).version, "0.2.3"); 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); diff --git a/scripts/verify-destructive-actions.js b/scripts/verify-destructive-actions.js index e866a01..c853914 100644 --- a/scripts/verify-destructive-actions.js +++ b/scripts/verify-destructive-actions.js @@ -77,6 +77,7 @@ function verifySharedInfrastructure() { const appScript = fs.readFileSync(path.join(root, "src", "web", "public", "app.js"), "utf8"); assert(appScript.includes("submitter?.dataset?.confirmMode"), "Submit-button confirmation metadata is ignored"); assert(appScript.includes("submitter?.dataset?.confirmLabel || form.dataset.confirmLabel"), "The clicked submit button cannot override generic confirmation copy"); + assert(appScript.includes('submitter?.hasAttribute?.("formaction") ? submitter.formAction : form.action'), "Submit buttons without formaction incorrectly override their parent form action"); assert(appScript.includes('form.querySelector(\'input[name="confirmation_token"]\')'), "Timed form confirmation tokens are not attached to submitted forms"); assert(appScript.includes("const response = await fetch(action, requestOptions)"), "Async update requests do not submit through the shared confirmed form payload"); assert(!appScript.includes("window.LumiConfirm.destructiveFetch(form.action"), "Timed update forms request a second confirmation instead of reusing the form token"); diff --git a/scripts/verify-release-metadata.js b/scripts/verify-release-metadata.js index 5cc5225..d901e18 100644 --- a/scripts/verify-release-metadata.js +++ b/scripts/verify-release-metadata.js @@ -4,8 +4,8 @@ const path = require("path"); const { findSafeTarget } = require("../src/services/versioning"); const root = path.join(__dirname, ".."); -const releaseVersion = "0.2.2"; -const previousCoreVersion = "0.2.1"; +const releaseVersion = "0.2.3"; +const previousCoreVersion = "0.2.2"; 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.2 and 11 changed plugin/tool packages."); +console.log("Release metadata verification passed: core 0.2.3 and 11 changed plugin/tool packages."); diff --git a/scripts/verify-update-system.js b/scripts/verify-update-system.js index 4253f80..948f560 100644 --- a/scripts/verify-update-system.js +++ b/scripts/verify-update-system.js @@ -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.2", "0.2.1", "0.2.0", "0.1.9"]); +assert.deepEqual(releaseVersions, ["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.0", + current_version: "0.2.2", available_versions: [ + { 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 }, { version: "0.2.0", ref: "refs/tags/v0.2.0", rollback_safe: true }, @@ -62,7 +63,7 @@ const corrected = buildStatus({ channel: "stable" }); assert.equal(corrected.version_correction, true); -assert.equal(corrected.safe_target_version, "0.2.2"); +assert.equal(corrected.safe_target_version, "0.2.3"); assert.equal(corrected.update_available, true); assert.equal(corrected.blocked, false); diff --git a/scripts/verify-webui.js b/scripts/verify-webui.js index 243ad4e..ef3923b 100644 --- a/scripts/verify-webui.js +++ b/scripts/verify-webui.js @@ -195,7 +195,9 @@ function verifySharedUpdateActions() { const dashboard = fs.readFileSync(path.join(root, "src", "web", "views", "admin-dashboard.ejs"), "utf8"); const settings = fs.readFileSync(path.join(root, "src", "web", "views", "admin-settings.ejs"), "utf8"); assert(appSource.includes('button[data-update-action]')); - assert(appSource.includes("submitter?.formAction || form.action")); + assert(appSource.includes('submitter?.hasAttribute?.("formaction") ? submitter.formAction : form.action')); + assert(appSource.includes('submitter?.hasAttribute?.("formmethod") ? submitter.formMethod : form.method')); + assert(!appSource.includes("submitter?.formAction || form.action")); assert(appSource.includes('actionPath.endsWith("/check")')); assert(appSource.includes('form.matches("[data-update-check-form]")')); assert(dashboard.includes('action="/admin/check-update" class="inline-form" data-update-action')); diff --git a/src/web/public/app.js b/src/web/public/app.js index ff55646..00a3d85 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1146,9 +1146,18 @@ let activeDestructive = null; let activeCallbackConfirm = null; + // A button without a formaction can report the current document URL through + // its formAction property. Only prefer the button value when the attribute + // was explicitly authored; otherwise the parent form owns the endpoint. + const effectiveFormAction = (form, submitter = null) => + submitter?.hasAttribute?.("formaction") ? submitter.formAction : form.action; + + const effectiveFormMethod = (form, submitter = null) => + submitter?.hasAttribute?.("formmethod") ? submitter.formMethod : form.method; + const destructiveAction = (form, submitter = null) => { try { - const action = submitter?.formAction || form.action; + const action = effectiveFormAction(form, submitter); return new URL(action, window.location.origin).pathname; } catch { return ""; @@ -1517,7 +1526,7 @@ const submitter = event.submitter || form.querySelector("button[type='submit']"); if (!form.matches("[data-update-action]") && !submitter?.matches?.("[data-update-action]")) return; if (form.matches("[data-update-check-form]")) return; - const action = submitter?.formAction || form.action; + const action = effectiveFormAction(form, submitter); const actionPath = normalizeDestructiveAction(action); if (updateLog && actionPath.startsWith("/admin/updates/") && actionPath.endsWith("/check")) return; event.preventDefault(); @@ -1548,7 +1557,7 @@ appendUpdateLog(`Started ${submitter?.textContent?.trim() || "update action"}.`, "info", form); try { const requestOptions = { - method: submitter?.formMethod || form.method || "POST", + method: effectiveFormMethod(form, submitter) || "POST", body: new FormData(form), headers: { Accept: "application/json" }, credentials: "same-origin" diff --git a/update-manifest.json b/update-manifest.json index b08bcb6..4a17664 100644 --- a/update-manifest.json +++ b/update-manifest.json @@ -1,6 +1,6 @@ { "name": "Lumi Core", - "version": "0.2.2", + "version": "0.2.3", "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, and an optional secured read-only production diagnostics endpoint. 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, 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" @@ -49,6 +49,18 @@ ], "rollback_safe": true, "migration_notes": "Adds stage-specific plugin update preflight/error reporting and an optional secured read-only production diagnostics endpoint; preserved local data is not replaced." + }, + { + "version": "0.2.3", + "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": "Fixes shared browser form endpoint resolution for timed confirmations and repository updates; preserved local data is not replaced." } ] }