Fix clobbered form endpoints
This commit is contained in:
parent
a6e785a11b
commit
3274778e40
@ -1,5 +1,10 @@
|
|||||||
# Lumi changelog
|
# Lumi changelog
|
||||||
|
|
||||||
|
## 0.2.9
|
||||||
|
|
||||||
|
- Fixed forms containing fields named `action` or `method` being able to shadow their own submission endpoint and send requests to an invalid URL.
|
||||||
|
- Added a shared clobber-safe form action/method resolver and reused it in update, settings, and Lumi AI feedback workflows.
|
||||||
|
|
||||||
## 0.2.8
|
## 0.2.8
|
||||||
|
|
||||||
- Fixed Lumi AI Improvement Center actions that could return an unhelpful generic failure for no-change, dismiss, edit, and restore operations.
|
- Fixed Lumi AI Improvement Center actions that could return an unhelpful generic failure for no-change, dismiss, edit, and restore operations.
|
||||||
|
|||||||
1
TODO.md
1
TODO.md
@ -677,6 +677,7 @@ This section is for Lumi chat answer feedback and AI Improvement Center work, no
|
|||||||
|
|
||||||
## Done
|
## Done
|
||||||
|
|
||||||
|
- 2026-07-18: Fixed core 0.2.9 and Lumi AI 0.8.5 forms whose `action` or `method` fields could shadow the form endpoint, added a shared clobber-safe resolver, and migrated the affected feedback/settings requests to it.
|
||||||
- 2026-07-18: Fixed Lumi AI 0.8.4 Improvement Center state changes for no-change, dismiss, edit, and restore actions with an explicit authenticated JSON contract, actionable errors, safe diagnostics, and focused regression coverage.
|
- 2026-07-18: Fixed Lumi AI 0.8.4 Improvement Center state changes for no-change, dismiss, edit, and restore actions with an explicit authenticated JSON contract, actionable errors, safe diagnostics, and focused regression coverage.
|
||||||
- 2026-07-18: Fixed private reverse-proxy HTTPS recognition for production diagnostics in core 0.2.5 without globally trusting client forwarding headers, and taught the local client to consume the repository's env-style `.secrets` file and short diagnostic variable names directly.
|
- 2026-07-18: Fixed private reverse-proxy HTTPS recognition for production diagnostics in core 0.2.5 without globally trusting client forwarding headers, and taught the local client to consume the repository's env-style `.secrets` file and short diagnostic variable names directly.
|
||||||
- 2026-07-18: Fixed Windows/network-share plugin updates in core 0.2.6 by leaving live preserved plugin data in place and transactionally replacing only code, including automatic code rollback coverage.
|
- 2026-07-18: Fixed Windows/network-share plugin updates in core 0.2.6 by leaving live preserved plugin data in place and transactionally replacing only code, including automatic code rollback coverage.
|
||||||
|
|||||||
@ -31,6 +31,8 @@ Review actions use an authenticated JSON response even when Lumi is behind a
|
|||||||
reverse proxy. If a save cannot complete, the page distinguishes an expired
|
reverse proxy. If a save cannot complete, the page distinguishes an expired
|
||||||
session, insufficient access, a feedback record changed elsewhere, and a
|
session, insufficient access, a feedback record changed elsewhere, and a
|
||||||
server-side storage problem instead of showing a generic failure.
|
server-side storage problem instead of showing a generic failure.
|
||||||
|
Lumi resolves the authored form endpoint directly, so fields named `action`
|
||||||
|
cannot replace the destination URL in the browser.
|
||||||
|
|
||||||
## Advanced tools
|
## Advanced tools
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ editable: false
|
|||||||
Lumi is the core web UI and bot runtime.
|
Lumi is the core web UI and bot runtime.
|
||||||
## Runtime
|
## Runtime
|
||||||
Package: lumi-bot
|
Package: lumi-bot
|
||||||
Version: 0.2.8
|
Version: 0.2.9
|
||||||
## Routes
|
## Routes
|
||||||
- GET /api/events
|
- GET /api/events
|
||||||
- POST /api/destructive-confirmations
|
- POST /api/destructive-confirmations
|
||||||
|
|||||||
@ -14,7 +14,7 @@ editable: false
|
|||||||
Managed local AI provider and scoped WebUI assistant for Lumi.
|
Managed local AI provider and scoped WebUI assistant for Lumi.
|
||||||
## Metadata
|
## Metadata
|
||||||
Plugin ID: lumi_ai
|
Plugin ID: lumi_ai
|
||||||
Version: 0.8.4
|
Version: 0.8.5
|
||||||
Default state: enabled
|
Default state: enabled
|
||||||
## Web Routes
|
## Web Routes
|
||||||
- /plugins/lumi_ai
|
- /plugins/lumi_ai
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.8",
|
"version": "0.2.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.8",
|
"version": "0.2.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.12",
|
"adm-zip": "^0.5.12",
|
||||||
"better-sqlite3": "^11.5.0",
|
"better-sqlite3": "^11.5.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.8",
|
"version": "0.2.9",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
# Lumi AI changelog
|
# Lumi AI changelog
|
||||||
|
|
||||||
|
## 0.8.5
|
||||||
|
|
||||||
|
- Fixed Improvement Center actions returning HTTP 404 because their hidden `action` field could shadow the form's endpoint URL in the browser.
|
||||||
|
- Reused Lumi's shared clobber-safe form resolver for feedback actions and asynchronous Lumi AI settings/download forms.
|
||||||
|
|
||||||
## 0.8.4
|
## 0.8.4
|
||||||
|
|
||||||
- Fixed no-change, dismiss, edit, and restore actions in the Improvement Center by using an explicit authenticated JSON response contract.
|
- Fixed no-change, dismiss, edit, and restore actions in the Improvement Center by using an explicit authenticated JSON response contract.
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"id": "lumi_ai",
|
"id": "lumi_ai",
|
||||||
"name": "Lumi AI",
|
"name": "Lumi AI",
|
||||||
"version": "0.8.4",
|
"version": "0.8.5",
|
||||||
"description": "Managed local AI provider and scoped WebUI assistant for Lumi.",
|
"description": "Managed local AI provider and scoped WebUI assistant for Lumi.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"compatible_from": "0.8.3",
|
"compatible_from": "0.8.4",
|
||||||
"migration_notes": "Existing models, runtimes, settings, feedback, corrections, metrics, and tool data are retained. Feedback actions now use a reliable JSON response contract and report actionable failures; no manual migration is required.",
|
"migration_notes": "Existing models, runtimes, settings, feedback, corrections, metrics, and tool data are retained. Feedback and settings forms now use Lumi's shared clobber-safe endpoint resolver; no manual migration is required.",
|
||||||
"rollback_safe": true
|
"rollback_safe": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,12 +121,13 @@
|
|||||||
submitter.textContent = "Saving…";
|
submitter.textContent = "Saving…";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
const action = window.LumiForms?.action(form) || form.getAttribute("action") || window.location.href;
|
||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
for (const [key, value] of new FormData(form).entries()) {
|
for (const [key, value] of new FormData(form).entries()) {
|
||||||
if (typeof value === "string") body.append(key, value);
|
if (typeof value === "string") body.append(key, value);
|
||||||
}
|
}
|
||||||
const response = await fetch(form.action, {
|
const response = await fetch(action, {
|
||||||
method: form.method || "POST",
|
method: window.LumiForms?.method(form) || form.getAttribute("method") || "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
const button = form.querySelector("[data-ai-download-button]");
|
const button = form.querySelector("[data-ai-download-button]");
|
||||||
window.LumiStateButton?.setState(button, "loading", { busy: true });
|
window.LumiStateButton?.setState(button, "loading", { busy: true });
|
||||||
try {
|
try {
|
||||||
const response = await fetch(form.action, {
|
const response = await fetch(window.LumiForms?.action(form) || form.getAttribute("action") || window.location.href, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Accept": "application/json" },
|
headers: { "Accept": "application/json" },
|
||||||
body: new FormData(form)
|
body: new FormData(form)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<%- include("../../../src/web/views/partials/layout-top", { title }) %>
|
<%- include("../../../src/web/views/partials/layout-top", { title }) %>
|
||||||
<link rel="stylesheet" href="/plugins/lumi_ai/assets/settings.css?v=0.8.4" />
|
<link rel="stylesheet" href="/plugins/lumi_ai/assets/settings.css?v=0.8.5" />
|
||||||
<link rel="stylesheet" href="/plugins/lumi_ai/assets/improvement-center.css?v=0.8.4" />
|
<link rel="stylesheet" href="/plugins/lumi_ai/assets/improvement-center.css?v=0.8.5" />
|
||||||
|
|
||||||
<% const statusLabel = (status) => ({ pending: "New", flagged: "Needs attention", verified: "Reviewed by moderator", approved: "Ready to apply", implemented: "Applied", rejected: "Dismissed", reviewed: "No change needed", archived: "Archived" }[status] || status); %>
|
<% const statusLabel = (status) => ({ pending: "New", flagged: "Needs attention", verified: "Reviewed by moderator", approved: "Ready to apply", implemented: "Applied", rejected: "Dismissed", reviewed: "No change needed", archived: "Archived" }[status] || status); %>
|
||||||
<section class="ai-titlebar improvement-titlebar">
|
<section class="ai-titlebar improvement-titlebar">
|
||||||
@ -188,5 +188,5 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/plugins/lumi_ai/assets/improvement-center.js?v=0.8.4" defer></script>
|
<script src="/plugins/lumi_ai/assets/improvement-center.js?v=0.8.5" defer></script>
|
||||||
<%- include("../../../src/web/views/partials/layout-bottom") %>
|
<%- include("../../../src/web/views/partials/layout-bottom") %>
|
||||||
|
|||||||
@ -2,6 +2,36 @@
|
|||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"releases": [
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "0.2.9",
|
||||||
|
"ref": "refs/tags/v0.2.9",
|
||||||
|
"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 browser form endpoint shadowing in Lumi AI feedback and shared settings workflows while preserving all existing feedback, settings, models, corrections, and plugin data.",
|
||||||
|
"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.5",
|
||||||
|
"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.8",
|
"version": "0.2.8",
|
||||||
"ref": "refs/tags/v0.2.8",
|
"ref": "refs/tags/v0.2.8",
|
||||||
|
|||||||
@ -77,7 +77,8 @@ function verifySharedInfrastructure() {
|
|||||||
const appScript = fs.readFileSync(path.join(root, "src", "web", "public", "app.js"), "utf8");
|
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?.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?.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?.getAttribute?.("action")'), "Shared form actions do not resist controls named action shadowing the endpoint");
|
||||||
|
assert(appScript.includes("window.LumiForms = Object.freeze"), "Plugins cannot reuse the shared clobber-safe form helper");
|
||||||
assert(appScript.includes('form.querySelector(\'input[name="confirmation_token"]\')'), "Timed form confirmation tokens are not attached to submitted forms");
|
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("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");
|
assert(!appScript.includes("window.LumiConfirm.destructiveFetch(form.action"), "Timed update forms request a second confirmation instead of reusing the form token");
|
||||||
@ -97,6 +98,7 @@ function verifySharedInfrastructure() {
|
|||||||
assert(improvementView.includes("/corrections/<%= entry.id %>/delete"));
|
assert(improvementView.includes("/corrections/<%= entry.id %>/delete"));
|
||||||
const improvementScript = fs.readFileSync(path.join(root, "plugins", "lumi_ai", "public", "improvement-center.js"), "utf8");
|
const improvementScript = fs.readFileSync(path.join(root, "plugins", "lumi_ai", "public", "improvement-center.js"), "utf8");
|
||||||
assert.equal(improvementScript.includes("window.confirm"), false, "Improvement Center still uses the legacy browser confirmation");
|
assert.equal(improvementScript.includes("window.confirm"), false, "Improvement Center still uses the legacy browser confirmation");
|
||||||
|
assert(improvementScript.includes("window.LumiForms?.action(form)"), "Improvement Center bypasses the shared clobber-safe form endpoint helper");
|
||||||
}
|
}
|
||||||
|
|
||||||
const controls = verifyViews();
|
const controls = verifyViews();
|
||||||
|
|||||||
@ -4,15 +4,15 @@ const path = require("path");
|
|||||||
const { findSafeTarget } = require("../src/services/versioning");
|
const { findSafeTarget } = require("../src/services/versioning");
|
||||||
|
|
||||||
const root = path.join(__dirname, "..");
|
const root = path.join(__dirname, "..");
|
||||||
const releaseVersion = "0.2.8";
|
const releaseVersion = "0.2.9";
|
||||||
const previousCoreVersion = "0.2.7";
|
const previousCoreVersion = "0.2.8";
|
||||||
const earliestCompatibleCoreVersion = "0.1.9";
|
const earliestCompatibleCoreVersion = "0.1.9";
|
||||||
const changedPlugins = {
|
const changedPlugins = {
|
||||||
"auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" },
|
"auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" },
|
||||||
birthday: { from: "0.1.2", to: "0.1.3", knowledge: "birthday" },
|
birthday: { from: "0.1.2", to: "0.1.3", knowledge: "birthday" },
|
||||||
"economy-framework": { from: "0.2.9", to: "0.2.10", knowledge: "economy-framework" },
|
"economy-framework": { from: "0.2.9", to: "0.2.10", knowledge: "economy-framework" },
|
||||||
"expression-interaction": { from: "0.2.0", to: "0.2.1", knowledge: "expression-interaction" },
|
"expression-interaction": { from: "0.2.0", to: "0.2.1", knowledge: "expression-interaction" },
|
||||||
lumi_ai: { from: "0.8.3", to: "0.8.4", knowledge: "lumi-ai" },
|
lumi_ai: { from: "0.8.4", to: "0.8.5", knowledge: "lumi-ai" },
|
||||||
moderation: { from: "0.1.4", to: "0.1.5", knowledge: "moderation" },
|
moderation: { from: "0.1.4", to: "0.1.5", knowledge: "moderation" },
|
||||||
okf: { from: "0.1.0", to: "0.1.1", knowledge: "okf" },
|
okf: { from: "0.1.0", to: "0.1.1", knowledge: "okf" },
|
||||||
quotes: { from: "0.1.1", to: "0.1.2", knowledge: "quotes" },
|
quotes: { from: "0.1.1", to: "0.1.2", knowledge: "quotes" },
|
||||||
@ -87,4 +87,4 @@ assert.equal(webSearch.minimum_lumi_version, "0.2.0");
|
|||||||
assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2");
|
assert.equal(webSearch.minimum_lumi_ai_version, "0.8.2");
|
||||||
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
|
assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true);
|
||||||
|
|
||||||
console.log("Release metadata verification passed: core 0.2.8, Lumi AI 0.8.4, and synchronized package metadata.");
|
console.log("Release metadata verification passed: core 0.2.9, Lumi AI 0.8.5, and synchronized package metadata.");
|
||||||
|
|||||||
@ -16,7 +16,7 @@ function readJson(relativePath) {
|
|||||||
|
|
||||||
const releaseIndex = readJson("release-index.json");
|
const releaseIndex = readJson("release-index.json");
|
||||||
const releaseVersions = releaseIndex.releases.map((release) => release.version);
|
const releaseVersions = releaseIndex.releases.map((release) => release.version);
|
||||||
assert.deepEqual(releaseVersions, ["0.2.8", "0.2.7", "0.2.6", "0.2.5", "0.2.4", "0.2.3", "0.2.2", "0.2.1", "0.2.0", "0.1.9"]);
|
assert.deepEqual(releaseVersions, ["0.2.9", "0.2.8", "0.2.7", "0.2.6", "0.2.5", "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");
|
assert.equal(new Set(releaseVersions).size, releaseVersions.length, "release versions must be unique");
|
||||||
for (const release of releaseIndex.releases) {
|
for (const release of releaseIndex.releases) {
|
||||||
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
assert.equal(normalizeRepositoryRef(release.ref), release.ref);
|
||||||
@ -37,6 +37,7 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) {
|
|||||||
const baseTarget = {
|
const baseTarget = {
|
||||||
current_version: "0.2.4",
|
current_version: "0.2.4",
|
||||||
available_versions: [
|
available_versions: [
|
||||||
|
{ version: "0.2.9", ref: "refs/tags/v0.2.9", rollback_safe: true },
|
||||||
{ version: "0.2.8", ref: "refs/tags/v0.2.8", rollback_safe: true },
|
{ version: "0.2.8", ref: "refs/tags/v0.2.8", rollback_safe: true },
|
||||||
{ version: "0.2.7", ref: "refs/tags/v0.2.7", rollback_safe: true },
|
{ version: "0.2.7", ref: "refs/tags/v0.2.7", rollback_safe: true },
|
||||||
{ version: "0.2.6", ref: "refs/tags/v0.2.6", rollback_safe: true },
|
{ version: "0.2.6", ref: "refs/tags/v0.2.6", rollback_safe: true },
|
||||||
@ -68,7 +69,7 @@ const corrected = buildStatus({
|
|||||||
channel: "stable"
|
channel: "stable"
|
||||||
});
|
});
|
||||||
assert.equal(corrected.version_correction, true);
|
assert.equal(corrected.version_correction, true);
|
||||||
assert.equal(corrected.safe_target_version, "0.2.8");
|
assert.equal(corrected.safe_target_version, "0.2.9");
|
||||||
assert.equal(corrected.update_available, true);
|
assert.equal(corrected.update_available, true);
|
||||||
assert.equal(corrected.blocked, false);
|
assert.equal(corrected.blocked, false);
|
||||||
|
|
||||||
|
|||||||
@ -195,8 +195,9 @@ function verifySharedUpdateActions() {
|
|||||||
const dashboard = fs.readFileSync(path.join(root, "src", "web", "views", "admin-dashboard.ejs"), "utf8");
|
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");
|
const settings = fs.readFileSync(path.join(root, "src", "web", "views", "admin-settings.ejs"), "utf8");
|
||||||
assert(appSource.includes('button[data-update-action]'));
|
assert(appSource.includes('button[data-update-action]'));
|
||||||
assert(appSource.includes('submitter?.hasAttribute?.("formaction") ? submitter.formAction : form.action'));
|
assert(appSource.includes('form?.getAttribute?.("action")'));
|
||||||
assert(appSource.includes('submitter?.hasAttribute?.("formmethod") ? submitter.formMethod : form.method'));
|
assert(appSource.includes("window.LumiForms = Object.freeze"));
|
||||||
|
assert(appSource.includes('form?.getAttribute?.("method")'));
|
||||||
assert(!appSource.includes("submitter?.formAction || form.action"));
|
assert(!appSource.includes("submitter?.formAction || form.action"));
|
||||||
assert(appSource.includes('actionPath.endsWith("/check")'));
|
assert(appSource.includes('actionPath.endsWith("/check")'));
|
||||||
assert(appSource.includes('form.matches("[data-update-check-form]")'));
|
assert(appSource.includes('form.matches("[data-update-check-form]")'));
|
||||||
|
|||||||
@ -1146,14 +1146,19 @@
|
|||||||
let activeDestructive = null;
|
let activeDestructive = null;
|
||||||
let activeCallbackConfirm = null;
|
let activeCallbackConfirm = null;
|
||||||
|
|
||||||
// A button without a formaction can report the current document URL through
|
// Read authored attributes instead of form.action/form.method. Controls named
|
||||||
// its formAction property. Only prefer the button value when the attribute
|
// "action" or "method" can shadow those DOM properties and turn the endpoint
|
||||||
// was explicitly authored; otherwise the parent form owns the endpoint.
|
// into an HTML element rather than a URL.
|
||||||
const effectiveFormAction = (form, submitter = null) =>
|
const effectiveFormAction = (form, submitter = null) =>
|
||||||
submitter?.hasAttribute?.("formaction") ? submitter.formAction : form.action;
|
submitter?.getAttribute?.("formaction") || form?.getAttribute?.("action") || window.location.href;
|
||||||
|
|
||||||
const effectiveFormMethod = (form, submitter = null) =>
|
const effectiveFormMethod = (form, submitter = null) =>
|
||||||
submitter?.hasAttribute?.("formmethod") ? submitter.formMethod : form.method;
|
submitter?.getAttribute?.("formmethod") || form?.getAttribute?.("method") || "get";
|
||||||
|
|
||||||
|
window.LumiForms = Object.freeze({
|
||||||
|
action: effectiveFormAction,
|
||||||
|
method: effectiveFormMethod
|
||||||
|
});
|
||||||
|
|
||||||
const destructiveAction = (form, submitter = null) => {
|
const destructiveAction = (form, submitter = null) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -114,12 +114,13 @@
|
|||||||
status.textContent = "Saving...";
|
status.textContent = "Saving...";
|
||||||
try {
|
try {
|
||||||
for (const form of forms) {
|
for (const form of forms) {
|
||||||
|
const action = window.LumiForms?.action(form) || form.getAttribute("action") || window.location.href;
|
||||||
const body = new URLSearchParams();
|
const body = new URLSearchParams();
|
||||||
for (const [key, value] of new FormData(form).entries()) {
|
for (const [key, value] of new FormData(form).entries()) {
|
||||||
if (typeof value === "string") body.append(key, value);
|
if (typeof value === "string") body.append(key, value);
|
||||||
}
|
}
|
||||||
const response = await fetch(form.action || window.location.href, {
|
const response = await fetch(action, {
|
||||||
method: form.method || "POST",
|
method: window.LumiForms?.method(form) || form.getAttribute("method") || "POST",
|
||||||
body,
|
body,
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "text/html,application/json",
|
Accept: "text/html,application/json",
|
||||||
@ -127,7 +128,7 @@
|
|||||||
},
|
},
|
||||||
redirect: "follow"
|
redirect: "follow"
|
||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(`Save failed for ${form.action || "settings form"}.`);
|
if (!response.ok) throw new Error(`Save failed for ${action || "settings form"}.`);
|
||||||
form._lumiSnapshot = snapshotForm(form);
|
form._lumiSnapshot = snapshotForm(form);
|
||||||
}
|
}
|
||||||
status.textContent = "Saved. Reloading...";
|
status.textContent = "Saved. Reloading...";
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Lumi Core",
|
"name": "Lumi Core",
|
||||||
"version": "0.2.8",
|
"version": "0.2.9",
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"released_at": "2026-07-18",
|
"released_at": "2026-07-18",
|
||||||
"compatible_from": "0.1.9",
|
"compatible_from": "0.1.9",
|
||||||
@ -8,7 +8,7 @@
|
|||||||
"replaces_versions": [
|
"replaces_versions": [
|
||||||
"1.2.0"
|
"1.2.0"
|
||||||
],
|
],
|
||||||
"migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics, Windows/network-share-safe plugin code replacement, and reliable Lumi AI feedback review actions. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, feedback, and secrets.",
|
"migration_notes": "Includes the 1.2.0 version correction, production plugin-update diagnostics, secured read-only production diagnostics, Windows/network-share-safe plugin code replacement, and clobber-safe shared form submission. Lumi synchronizes runtime dependencies on restart and preserves settings, databases, plugin data, community knowledge, AI models, runtimes, uploads, logs, feedback, and secrets.",
|
||||||
"rollback_safe": true,
|
"rollback_safe": true,
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"Node.js 18 or newer"
|
"Node.js 18 or newer"
|
||||||
@ -121,6 +121,18 @@
|
|||||||
],
|
],
|
||||||
"rollback_safe": true,
|
"rollback_safe": true,
|
||||||
"migration_notes": "Fixes Lumi AI feedback review actions and failure reporting; existing feedback, corrections, models, settings, and plugin data are preserved."
|
"migration_notes": "Fixes Lumi AI feedback review actions and failure reporting; existing feedback, corrections, models, settings, and plugin data are preserved."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.2.9",
|
||||||
|
"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 shadowing and Lumi AI feedback 404 responses; existing feedback, settings, models, corrections, and plugin data are preserved."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user