diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f4b102..a8e7ef5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Lumi changelog +## 0.2.1 + +- Fixed repository update checks being handled twice and incorrectly ending in a failed button state after a successful result. +- Simplified update-result wording and moved exact core/plugin version selection into collapsed, module-specific advanced sections. + ## 0.2.0 - Added the OBS overlay management and Browser Source system, including scenes, live updates, visual editing, multimedia, external web sources, and OBS bridge extension points. diff --git a/TODO.md b/TODO.md index 6281aa3..8401122 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: 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. - 2026-07-18: Audited the 0.1.9-to-0.2.0 update boundary, removed core's direct dependency on OKF plugin files, made OBS WebSocket optional, added locked runtime dependency repair, corrected 1.2.0 to 0.2.0, added immutable release metadata and exact-version core/plugin installs, and produced a checksummed data-preserving core repair patch. ### 2026-07-18 diff --git a/knowledge/core/lumi-core.md b/knowledge/core/lumi-core.md index e9f453f..ffbf964 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.0 +Version: 0.2.1 ## Routes - GET /api/events - POST /api/destructive-confirmations diff --git a/package-lock.json b/package-lock.json index 7d468f2..fdb5856 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lumi-bot", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lumi-bot", - "version": "0.2.0", + "version": "0.2.1", "dependencies": { "adm-zip": "^0.5.12", "better-sqlite3": "^11.5.0", diff --git a/package.json b/package.json index ea0c388..3f5e90f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumi-bot", - "version": "0.2.0", + "version": "0.2.1", "private": true, "type": "commonjs", "scripts": { diff --git a/release-index.json b/release-index.json index 6ba2995..ca94a84 100644 --- a/release-index.json +++ b/release-index.json @@ -2,6 +2,36 @@ "schema_version": 1, "channel": "stable", "releases": [ + { + "version": "0.2.1", + "ref": "refs/tags/v0.2.1", + "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 update-check status handling and makes exact version selection collapsed, module-specific, and easier to understand.", + "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.0", "ref": "refs/tags/v0.2.0", diff --git a/scripts/build-core-repair-patch.js b/scripts/build-core-repair-patch.js index 36a9b23..edd0a92 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.0-repair.zip"); +const destination = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.1-repair.zip"); const files = [ "CHANGELOG.md", "README.md", @@ -21,6 +21,7 @@ const files = [ "scripts/build-core-repair-patch.js", "scripts/verify-core-repair-patch.js", "scripts/verify-update-system.js", + "scripts/verify-webui.js", "src/services/dependency-manager.js", "src/services/overlay-connectors.js", "src/services/repo-update.js", @@ -28,6 +29,7 @@ const files = [ "src/services/update-manager.js", "src/services/update-repository.js", "src/web/server.js", + "src/web/public/app.js", "src/web/views/admin-updates.ejs" ]; @@ -45,10 +47,10 @@ for (const relativePath of files) { const manifest = { schema_version: 1, - name: "Lumi core 0.2.0 repair", + name: "Lumi core 0.2.1 repair", target: "core", - from_versions: ["0.1.9", "1.2.0", "0.2.0"], - to_version: "0.2.0", + from_versions: ["0.1.9", "1.2.0", "0.2.0", "0.2.1"], + to_version: "0.2.1", 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 30eef8f..8b4f335 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.0-repair.zip"); +const archivePath = path.join(root, "dist", "lumi-core-1.2.0-to-0.2.1-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.0"); +assert.equal(manifest.to_version, "0.2.1"); assert.equal(manifest.data_policy, "preserve"); -assert.deepEqual(manifest.from_versions, ["0.1.9", "1.2.0", "0.2.0"]); +assert.deepEqual(manifest.from_versions, ["0.1.9", "1.2.0", "0.2.0", "0.2.1"]); 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.0"); +assert.equal(JSON.parse(zip.readAsText("package.json")).version, "0.2.1"); 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.0"); + assert.equal(verifyPatchPackage(simulation).to_version, "0.2.1"); 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.0"); + assert.equal(JSON.parse(fs.readFileSync(path.join(simulation, "package.json"), "utf8")).version, "0.2.1"); assert.equal(fs.existsSync(path.join(simulation, "src", "services", "dependency-manager.js")), true); } finally { fs.rmSync(simulation, { recursive: true, force: true }); diff --git a/scripts/verify-release-metadata.js b/scripts/verify-release-metadata.js index 13a5b42..df99d51 100644 --- a/scripts/verify-release-metadata.js +++ b/scripts/verify-release-metadata.js @@ -4,8 +4,9 @@ const path = require("path"); const { findSafeTarget } = require("../src/services/versioning"); const root = path.join(__dirname, ".."); -const releaseVersion = "0.2.0"; -const previousCoreVersion = "0.1.9"; +const releaseVersion = "0.2.1"; +const previousCoreVersion = "0.2.0"; +const earliestCompatibleCoreVersion = "0.1.9"; const changedPlugins = { "auto-vc": { from: "0.1.5", to: "0.1.6", knowledge: "auto-vc" }, birthday: { from: "0.1.2", to: "0.1.3", knowledge: "birthday" }, @@ -44,13 +45,14 @@ assert.equal(packageLock.version, releaseVersion); assert.equal(packageLock.packages?.[""]?.version, releaseVersion); assert.equal(coreManifest.version, releaseVersion); assert.equal(coreManifest.channel, "stable"); -assert.equal(coreManifest.compatible_from, previousCoreVersion); +assert.equal(coreManifest.compatible_from, earliestCompatibleCoreVersion); assert.equal(coreManifest.rollback_safe, true); assert.deepEqual(coreManifest.replaces_versions, ["1.2.0"]); assert.equal(releaseIndex.releases[0].version, releaseVersion); assert.equal(releaseIndex.releases[0].ref, `refs/tags/v${releaseVersion}`); assert.equal(releaseIndex.releases[1].version, previousCoreVersion); assert.equal(releaseIndex.releases[1].ref, `refs/tags/v${previousCoreVersion}`); +assert.equal(releaseIndex.releases[2].version, earliestCompatibleCoreVersion); assert.equal(hasVersionHeading(readText("CHANGELOG.md"), releaseVersion), true); assert.match(readText("knowledge/core/lumi-core.md"), new RegExp(`^Version: ${escapeRegex(releaseVersion)}$`, "m")); @@ -81,8 +83,8 @@ for (const [pluginId, expected] of Object.entries(changedPlugins)) { const webSearch = readJson("plugins/lumi_ai_web_search/tool_info.json"); assert.equal(webSearch.version, "0.1.1"); -assert.equal(webSearch.minimum_lumi_version, releaseVersion); +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.0 and 11 changed plugin/tool packages."); +console.log("Release metadata verification passed: core 0.2.1 and 11 changed plugin/tool packages."); diff --git a/scripts/verify-update-system.js b/scripts/verify-update-system.js index 1d3fca2..7628df3 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.0", "0.1.9"]); +assert.deepEqual(releaseVersions, ["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); @@ -37,6 +37,7 @@ for (const [toolId, version] of Object.entries(currentRelease.tools)) { const baseTarget = { current_version: "0.2.0", available_versions: [ + { 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 }, { version: "0.1.9", ref: "refs/tags/v0.1.9", rollback_safe: true } ], @@ -60,7 +61,7 @@ const corrected = buildStatus({ channel: "stable" }); assert.equal(corrected.version_correction, true); -assert.equal(corrected.safe_target_version, "0.2.0"); +assert.equal(corrected.safe_target_version, "0.2.1"); assert.equal(corrected.update_available, true); assert.equal(corrected.blocked, false); diff --git a/scripts/verify-webui.js b/scripts/verify-webui.js index fb3f317..61b7038 100644 --- a/scripts/verify-webui.js +++ b/scripts/verify-webui.js @@ -191,15 +191,23 @@ function verifyUpdateCachePreserved() { function verifySharedUpdateActions() { const appSource = fs.readFileSync(path.join(root, "src", "web", "public", "app.js"), "utf8"); const serverSource = fs.readFileSync(path.join(root, "src", "web", "server.js"), "utf8"); + const updates = fs.readFileSync(path.join(root, "src", "web", "views", "admin-updates.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"); assert(appSource.includes('button[data-update-action]')); 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')); assert(settings.includes('formaction=\\"/admin/check-update\\" formmethod=\\"post\\" data-update-action')); assert(serverSource.includes('sendUpdateResult(req, res, {\n status,')); assert(serverSource.includes('message: "Core update applied. Lumi will restart in a few seconds."')); + assert(serverSource.includes('`Core version ${status.core.safe_target_version} is available.`')); + assert(serverSource.includes('`${plugin.name} version ${plugin.safe_target_version} is available.`')); + assert(updates.includes("data-update-check-form")); + assert(updates.includes('class="lumi-expandable-settings update-version-picker"')); + assert(!updates.includes("reinstall current")); + assert(!updates.includes(" · core <%= release.core_version %>")); } function verifyHomepageEmbeds() { diff --git a/src/services/update-index.js b/src/services/update-index.js index 908586b..c7e9270 100644 --- a/src/services/update-index.js +++ b/src/services/update-index.js @@ -292,9 +292,9 @@ function buildStatus({ kind, id, name, currentVersion, manifest, changelog, sour rollback_safe: false, unversioned: true }; - const replacementTarget = versions.find((entry) => + const replacementTarget = versions.filter((entry) => Array.isArray(entry.replaces_versions) && entry.replaces_versions.includes(currentVersion) - ); + ).at(-1); const targetResult = replacementTarget ? { target: replacementTarget, diff --git a/src/web/public/app.js b/src/web/public/app.js index 9b5034e..7802af7 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1515,6 +1515,7 @@ form.addEventListener("submit", async (event) => { 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 actionPath = normalizeDestructiveAction(action); if (updateLog && actionPath.startsWith("/admin/updates/") && actionPath.endsWith("/check")) return; diff --git a/src/web/server.js b/src/web/server.js index e3cc390..7affb7c 100644 --- a/src/web/server.js +++ b/src/web/server.js @@ -6594,10 +6594,10 @@ function createWebServer({ loadPlugins, discordClient }) { sendUpdateResult(req, res, { status, message: status.core.update_available - ? `Safe core target ${status.core.safe_target_version} is available.` + ? `Core version ${status.core.safe_target_version} is available.` : status.core.blocked ? status.core.blocked_reason - : "No core updates found." + : "Lumi core is already up to date." }); } catch (error) { sendUpdateError(req, res, error); @@ -6751,10 +6751,10 @@ function createWebServer({ loadPlugins, discordClient }) { sendUpdateResult(req, res, { plugin, message: plugin.update_available - ? `Safe plugin target ${plugin.safe_target_version} is available.` + ? `${plugin.name} version ${plugin.safe_target_version} is available.` : plugin.blocked ? plugin.blocked_reason - : "No plugin updates found." + : `${plugin.name} is already up to date.` }); } catch (error) { sendUpdateError(req, res, error); diff --git a/src/web/views/admin-updates.ejs b/src/web/views/admin-updates.ejs index 871795e..22934ba 100644 --- a/src/web/views/admin-updates.ejs +++ b/src/web/views/admin-updates.ejs @@ -12,7 +12,7 @@ const toolBadgeClass = (item) => item?.blocked ? "danger" : item?.update_available || item?.installed === false ? "warning" : "success"; const toolBadgeText = (item) => item?.blocked ? "Blocked" : item?.installed === false ? "Available" : item?.update_available ? "Update available" : "Current"; const changelogItems = (item) => Array.isArray(item?.changelog_range) ? item.changelog_range : []; - const applyLabel = (item) => item?.unversioned_update ? "Review and update" : "Use recommended version"; + const applyLabel = (item) => item?.unversioned_update ? "Review and continue" : item?.installed === false ? "Install plugin" : "Apply update"; const applyConfirmText = (item, label) => { const base = `${label}: create a backup, prepare recovery, apply ${item?.safe_target_version || "the selected update"}, and verify before finishing.`; if (!item?.unversioned_update) return base; @@ -129,7 +129,7 @@ <% } %>