diff --git a/CHANGELOG.md b/CHANGELOG.md index 169ca8d..0583c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Lumi changelog +## 0.2.12 + +- Fixed OBS Browser Source transparency by applying OBS's standard transparent-page CSS to clean overlay pages and embedded frame surfaces. +- Made the management preview and public OBS output share one fixed full-resolution canvas, preventing responsive alert pages from changing text size or position in the smaller editor preview. +- Added clear, copyable OBS Browser Source width, height, and default Custom CSS guidance to each overlay. + ## 0.2.11 - Added structured component, activity, event, and request metadata across Core, plugins, platforms, commands, webhooks, diagnostics, and WebUI administrator actions. diff --git a/TODO.md b/TODO.md index e0a8ee4..1e34e6c 100644 --- a/TODO.md +++ b/TODO.md @@ -19,7 +19,10 @@ website-overlay health checks plus manual and automatic refresh. Video and audio sources now include playback controls and OBS-routable sound; web sources include isolated custom frame CSS, cropping, and zoom. An optional OBS Browser Bridge now syncs supported OBS scene, transition, canvas, output, permission, and source -state without WebSocket credentials, with a one-action setup importer. +state without WebSocket credentials, with a one-action setup importer. The clean +render page and editor now share a fixed full-resolution canvas, transparent OBS +defaults, and explicit Browser Source dimensions so responsive third-party alerts +render consistently in the preview and OBS. Remaining work: @@ -677,6 +680,8 @@ This section is for Lumi chat answer feedback and AI Improvement Center work, no ## Done +- 2026-07-18: Fixed OBS overlay transparency and preview fidelity by applying OBS's exact default Browser Source CSS, rendering the editor and clean output from the same full-resolution canvas coordinate system, keeping iframe surfaces transparent, and showing the required OBS width/height plus copyable CSS in setup. + - 2026-07-18: Reworked shared logging repo-wide with structured component/category/event/request metadata, recursive secret redaction, bounded details, automatic retention, scoped plugin loggers, platform/command/webhook/lifecycle/admin HTTP auditing, live `/admin/logs` updates, complete server-side filters and summaries, and text/JSONL exports. - 2026-07-18: Fixed core 0.2.10 local Windows startup under Node.js 24 by resolving npm's JavaScript CLI through the active Node installation, retaining a command-shell fallback, and verifying automatic dependency repair through the real launcher on port 3000. - 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. diff --git a/docs/obs-overlays.md b/docs/obs-overlays.md index 3d7e155..3acb7fe 100644 --- a/docs/obs-overlays.md +++ b/docs/obs-overlays.md @@ -11,6 +11,13 @@ The editor uses the same basic model as OBS: scenes contain ordered sources, and the selected source has a red box with resize handles. On a wide screen the preview stays beside the settings; on smaller screens it moves above them. +The preview and clean OBS page now render one fixed, full-resolution canvas and +scale that finished canvas to the available display area. Embedded alert pages +therefore receive the same CSS-pixel viewport in the editor and in OBS instead +of switching responsive layouts when the management preview is smaller. Set the +OBS Browser Source width and height to the Lumi canvas dimensions shown in the +editor (normally 1920 by 1080). + - Drag a source to move it. - Drag any corner or edge handle to resize it. - Use arrow keys for small moves, or Shift plus an arrow for larger moves. @@ -91,11 +98,24 @@ addresses are periodically refreshed but are not fetched by the Lumi server for security; browser load-timeout recovery still applies. Website sources also support per-edge percentage cropping, zoom, and isolated -custom frame CSS. Custom CSS is scoped to the source frame so it cannot restyle +frame appearance CSS. Custom CSS is scoped to the source frame so it cannot restyle the Lumi render page or other sources. Standard browser cross-origin security does not allow parent-page CSS to select elements inside a third-party iframe; -use crop and zoom to hide unwanted portions of an external site. Frame-level CSS -can target `iframe`, for example `iframe { filter: saturate(1.2); }`. +use the provider's own appearance settings, crop, and zoom to hide unwanted +portions of an external site. Frame-level CSS can target +`.lumi-overlay-frame`, for example +`.lumi-overlay-frame { filter: saturate(1.2); }`. + +Lumi applies OBS's standard transparent Browser Source CSS to every clean render +page and shows the same copyable value in the Browser Source setup section: + +```css +body { background-color: rgba(0, 0, 0, 0); margin: 0px auto; overflow: hidden; } +``` + +The Lumi document, canvas stage, and embedded frame surface are transparent by +default. A third-party page can still explicitly paint its own background; a +normal parent iframe cannot override that cross-origin page CSS. ## OBS WebSocket diff --git a/knowledge/core/lumi-core.md b/knowledge/core/lumi-core.md index 6ba8300..8383a8c 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.11 +Version: 0.2.12 ## Routes - GET /api/events - POST /api/destructive-confirmations diff --git a/package-lock.json b/package-lock.json index 8db439a..bc450b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lumi-bot", - "version": "0.2.11", + "version": "0.2.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lumi-bot", - "version": "0.2.11", + "version": "0.2.12", "dependencies": { "adm-zip": "^0.5.12", "better-sqlite3": "^11.5.0", diff --git a/package.json b/package.json index 1ed62c5..0ec4226 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lumi-bot", - "version": "0.2.11", + "version": "0.2.12", "private": true, "type": "commonjs", "scripts": { diff --git a/release-index.json b/release-index.json index 9e1c07d..9b1b185 100644 --- a/release-index.json +++ b/release-index.json @@ -2,6 +2,36 @@ "schema_version": 1, "channel": "stable", "releases": [ + { + "version": "0.2.12", + "ref": "refs/tags/v0.2.12", + "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 OBS overlay transparency and preview-to-output layout parity without changing overlay records, tokens, scenes, sources, settings, databases, plugin data, models, uploads, feedback, or secrets.", + "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.11", "ref": "refs/tags/v0.2.11", diff --git a/scripts/verify-overlays.js b/scripts/verify-overlays.js index 1261c1b..621354a 100644 --- a/scripts/verify-overlays.js +++ b/scripts/verify-overlays.js @@ -1,6 +1,7 @@ const assert = require("assert"); const fs = require("fs"); const path = require("path"); +const vm = require("vm"); const root = path.join(__dirname, ".."); const sandbox = fs.mkdtempSync(path.join(root, ".tmp-lumi-overlays-")); @@ -197,12 +198,15 @@ try { const editorScript = fs.readFileSync(path.join(root, "src", "web", "public", "overlay-management.js"), "utf8"); const runtimeScript = fs.readFileSync(path.join(root, "src", "web", "public", "overlay-runtime.js"), "utf8"); const rendererScript = fs.readFileSync(path.join(root, "src", "web", "public", "overlay-renderer.js"), "utf8"); + const renderView = fs.readFileSync(path.join(root, "src", "web", "views", "overlay-render.ejs"), "utf8"); + const { OBS_BROWSER_DEFAULT_CSS } = require(path.join(root, "src", "services", "obs-browser-defaults.js")); const connectorScript = fs.readFileSync(path.join(root, "src", "services", "overlay-connectors.js"), "utf8"); const routeScript = fs.readFileSync(path.join(root, "src", "services", "overlay-routes.js"), "utf8"); assert(editorView.includes("data-overlay-canvas"), "management page must include a visual canvas"); assert(editorView.includes('data-module-fields="text"') && editorView.includes('data-module-fields="web"')); assert(editorView.includes('data-module-fields="video"') && editorView.includes('data-module-fields="audio"')); assert(editorView.includes('name="custom_css"') && editorView.includes('name="crop_left"')); + assert(editorView.includes("obsBrowserDefaultCss") && editorView.includes("OBS Browser Source setup")); assert(editorView.includes("data-color-picker") && editorView.includes("overlay-transform-disclosure")); assert(editorView.includes('data-confirm-action="/admin/overlays/<%= overlay.id %>/modules/<%= module.id %>/delete"'), "source deletion must use the shared timed-confirmation action flow"); assert(editorScript.includes("data-resize-handle") && editorScript.includes("snapTargets")); @@ -211,10 +215,30 @@ try { assert(runtimeScript.includes("window.obsstudio") && runtimeScript.includes("overlay:obs-browser-command")); assert(rendererScript.includes("buildMedia") && rendererScript.includes("buildWebsite")); assert(rendererScript.includes('frame.setAttribute("allow", "autoplay; fullscreen")')); + assert(rendererScript.includes('frame.setAttribute("allowtransparency", "true")')); + assert(rendererScript.includes('className = "lumi-overlay-stage"'), "preview and OBS must share a fixed canvas stage"); + assert.strictEqual(OBS_BROWSER_DEFAULT_CSS, "body { background-color: rgba(0, 0, 0, 0); margin: 0px auto; overflow: hidden; }"); + assert(renderView.includes('')); + const rendererContext = { window: { matchMedia: () => ({ matches: false }) }, URL }; + vm.runInNewContext(rendererScript, rendererContext); + const previewLayout = rendererContext.window.LumiOverlayRenderer.stageLayout( + { clientWidth: 480, clientHeight: 270 }, + { width: 1920, height: 1080 } + ); + const obsLayout = rendererContext.window.LumiOverlayRenderer.stageLayout( + { clientWidth: 1920, clientHeight: 1080 }, + { width: 1920, height: 1080 } + ); + assert.strictEqual(previewLayout.width, 1920); + assert.strictEqual(previewLayout.height, 1080); + assert.strictEqual(previewLayout.scaleX, 0.25); + assert.strictEqual(previewLayout.scaleY, 0.25); + assert.strictEqual(obsLayout.scaleX, 1); + assert.strictEqual(obsLayout.scaleY, 1); assert(connectorScript.includes('id: "obs_browser_bridge"') && connectorScript.includes("acceptReport(report")); assert(routeScript.includes("/obs-bridge") && routeScript.includes("/obs/import")); - console.log("Overlay verification passed: CRUD, visual editor hooks, transforms, layout controls, external recovery, token revocation, encrypted OBS credentials, and permissions."); + console.log("Overlay verification passed: CRUD, fixed-canvas preview parity, transparent OBS defaults, layout controls, external recovery, token revocation, encrypted OBS credentials, and permissions."); } finally { database?.db.close(); fs.rmSync(sandbox, { recursive: true, force: true }); diff --git a/scripts/verify-release-metadata.js b/scripts/verify-release-metadata.js index 550ad0d..8155c0c 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.11"; -const previousCoreVersion = "0.2.10"; +const releaseVersion = "0.2.12"; +const previousCoreVersion = "0.2.11"; 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, "0.8.2"); assert.equal(hasVersionHeading(readText("plugins/lumi_ai_web_search/CHANGELOG.md"), webSearch.version), true); -console.log("Release metadata verification passed: core 0.2.11, Lumi AI 0.8.5, and synchronized package metadata."); +console.log("Release metadata verification passed: core 0.2.12, Lumi AI 0.8.5, and synchronized package metadata."); diff --git a/scripts/verify-update-system.js b/scripts/verify-update-system.js index 740b456..80130c3 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.11", "0.2.10", "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.deepEqual(releaseVersions, ["0.2.12", "0.2.11", "0.2.10", "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"); 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.4", available_versions: [ + { version: "0.2.12", ref: "refs/tags/v0.2.12", rollback_safe: true }, { version: "0.2.11", ref: "refs/tags/v0.2.11", rollback_safe: true }, { version: "0.2.10", ref: "refs/tags/v0.2.10", rollback_safe: true }, { version: "0.2.9", ref: "refs/tags/v0.2.9", rollback_safe: true }, @@ -71,7 +72,7 @@ const corrected = buildStatus({ channel: "stable" }); assert.equal(corrected.version_correction, true); -assert.equal(corrected.safe_target_version, "0.2.11"); +assert.equal(corrected.safe_target_version, "0.2.12"); assert.equal(corrected.update_available, true); assert.equal(corrected.blocked, false); diff --git a/src/services/obs-browser-defaults.js b/src/services/obs-browser-defaults.js new file mode 100644 index 0000000..e6289ea --- /dev/null +++ b/src/services/obs-browser-defaults.js @@ -0,0 +1,5 @@ +const OBS_BROWSER_DEFAULT_CSS = "body { background-color: rgba(0, 0, 0, 0); margin: 0px auto; overflow: hidden; }"; + +module.exports = { + OBS_BROWSER_DEFAULT_CSS +}; diff --git a/src/services/overlay-routes.js b/src/services/overlay-routes.js index 9c6069a..eb7be15 100644 --- a/src/services/overlay-routes.js +++ b/src/services/overlay-routes.js @@ -34,6 +34,7 @@ const { updateOverlay, updateScene } = require("./overlays"); +const { OBS_BROWSER_DEFAULT_CSS } = require("./obs-browser-defaults"); const { listOverlayConnectorProviders, overlayConnectorManager @@ -219,6 +220,7 @@ function registerPublicOverlayRoutes(app) { eventsPath, healthPath, bridgePath, + obsBrowserDefaultCss: OBS_BROWSER_DEFAULT_CSS, initialStateJson: JSON.stringify(buildPublicState(context.overlay.id, fixed ? context.fixedScene.id : null)).replace(/ summary { + cursor: pointer; + font-weight: 800; +} + +.overlay-browser-source-setup[open] > summary { + margin-bottom: var(--lumi-space-3); +} + +.overlay-browser-dimensions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--lumi-space-2); + margin: var(--lumi-space-3) 0; +} + +.overlay-browser-dimensions > div { + padding: var(--lumi-space-2) var(--lumi-space-3); + border: 1px solid var(--lumi-border); + border-radius: var(--lumi-radius-sm); + background: var(--lumi-surface-raised); +} + +.overlay-browser-dimensions span, +.overlay-browser-dimensions strong, +.overlay-copy-label { + display: block; +} + +.overlay-browser-dimensions span { + color: var(--lumi-text-muted); + font-size: 0.78rem; +} + +.overlay-copy-label { + margin-bottom: var(--lumi-space-1); + font-weight: 700; +} + .overlay-list-actions, .overlay-scene-actions, .overlay-module-actions, @@ -349,7 +396,7 @@ .overlay-canvas .lumi-overlay-module.is-selected { z-index: 1000 !important; overflow: visible; - outline: 2px solid #ef4444; + outline: var(--lumi-editor-outline-size, 2px) solid #ef4444; outline-offset: -1px; } @@ -358,13 +405,13 @@ left: 0; bottom: 100%; display: none; - max-width: 12rem; - padding: 0.2rem 0.4rem; + max-width: var(--lumi-editor-label-max-width, 12rem); + padding: var(--lumi-editor-label-padding-y, 0.2rem) var(--lumi-editor-label-padding-x, 0.4rem); overflow: hidden; - border-radius: 4px 4px 0 0; + border-radius: var(--lumi-editor-label-radius, 4px) var(--lumi-editor-label-radius, 4px) 0 0; background: #ef4444; color: white; - font: 700 0.7rem/1.2 system-ui, sans-serif; + font: 700 var(--lumi-editor-label-font-size, 0.7rem)/1.2 system-ui, sans-serif; text-overflow: ellipsis; white-space: nowrap; } @@ -374,8 +421,8 @@ .overlay-resize-handle { position: absolute; z-index: 5; - width: 10px; - height: 10px; + width: var(--lumi-editor-handle-size, 10px); + height: var(--lumi-editor-handle-size, 10px); display: none; padding: 0; border: 1px solid white; @@ -457,7 +504,7 @@ .overlay-url-row { grid-template-columns: 1fr; } .overlay-transform-fields, .overlay-conditional-fields { grid-template-columns: 1fr 1fr; } - .overlay-resize-handle { width: 14px; height: 14px; } + .overlay-resize-handle { width: var(--lumi-editor-handle-size, 14px); height: var(--lumi-editor-handle-size, 14px); } } @media (max-width: 480px) { diff --git a/src/web/public/overlay-renderer.js b/src/web/public/overlay-renderer.js index df3eb97..2f05065 100644 --- a/src/web/public/overlay-renderer.js +++ b/src/web/public/overlay-renderer.js @@ -104,6 +104,7 @@ frame.sandbox = sandbox; frame.referrerPolicy = "no-referrer"; frame.setAttribute("allow", "autoplay; fullscreen"); + frame.setAttribute("allowtransparency", "true"); frame.style.pointerEvents = options.editor ? "none" : (values.allowPointerEvents ? "auto" : "none"); const layout = webFrameLayout(values); @@ -112,9 +113,9 @@ const shadow = root.attachShadow({ mode: "open" }); const style = document.createElement("style"); style.textContent = ` - :host { display: block; position: relative; width: 100%; height: 100%; overflow: hidden; } + :host { display: block; position: relative; width: 100%; height: 100%; overflow: hidden; background-color: rgba(0, 0, 0, 0); } .lumi-overlay-frame { - position: absolute; display: block; border: 0; background: transparent; + position: absolute; display: block; border: 0; background-color: rgba(0, 0, 0, 0); width: ${layout.width}%; height: ${layout.height}%; left: ${layout.left}%; top: ${layout.top}%; } @@ -199,22 +200,47 @@ function render(root, state, options = {}) { const canvas = options.canvas || state?.canvas || { width: 1920, height: 1080 }; - const widthScale = root.clientWidth ? root.clientWidth / finite(canvas.width, 1920) : 1; - const heightScale = root.clientHeight ? root.clientHeight / finite(canvas.height, 1080) : widthScale; - const scale = options.scale || Math.min(widthScale, heightScale); - const fragment = document.createDocumentFragment(); + const layout = stageLayout(root, canvas, options.scale); + const stage = document.createElement("div"); + stage.className = "lumi-overlay-stage"; + stage.style.width = `${layout.width}px`; + stage.style.height = `${layout.height}px`; + stage.style.transform = `scale(${layout.scaleX}, ${layout.scaleY})`; + if (options.editor) { + const editorScale = Math.max(0.01, Math.min(layout.scaleX, layout.scaleY)); + const handleSize = window.matchMedia?.("(max-width: 700px)").matches ? 14 : 10; + stage.style.setProperty("--lumi-editor-handle-size", `${handleSize / editorScale}px`); + stage.style.setProperty("--lumi-editor-outline-size", `${2 / editorScale}px`); + stage.style.setProperty("--lumi-editor-label-font-size", `${11.2 / editorScale}px`); + stage.style.setProperty("--lumi-editor-label-max-width", `${192 / editorScale}px`); + stage.style.setProperty("--lumi-editor-label-padding-y", `${3.2 / editorScale}px`); + stage.style.setProperty("--lumi-editor-label-padding-x", `${6.4 / editorScale}px`); + stage.style.setProperty("--lumi-editor-label-radius", `${4 / editorScale}px`); + } const elements = new Map(); if (state?.enabled !== false && state?.scene) { for (const module of state.scene.modules || []) { if (module.enabled === false) continue; - const element = buildModule(module, { ...options, scale }); + const element = buildModule(module, { ...options, canvas, scale: 1 }); elements.set(module.id, element); - fragment.appendChild(element); + stage.appendChild(element); } } - root.replaceChildren(fragment); + root.replaceChildren(stage); return elements; } - window.LumiOverlayRenderer = { anchors, applyBox, buildModule, findSourceContent, render, restartMediaElement }; + function stageLayout(root, canvas, scaleOverride) { + const width = Math.max(1, finite(canvas?.width, 1920)); + const height = Math.max(1, finite(canvas?.height, 1080)); + const override = finite(scaleOverride, 0); + return { + width, + height, + scaleX: override > 0 ? override : (root?.clientWidth ? root.clientWidth / width : 1), + scaleY: override > 0 ? override : (root?.clientHeight ? root.clientHeight / height : 1) + }; + } + + window.LumiOverlayRenderer = { anchors, applyBox, buildModule, findSourceContent, render, restartMediaElement, stageLayout }; })(); diff --git a/src/web/public/overlay-runtime.css b/src/web/public/overlay-runtime.css index 6d06c2a..6da4027 100644 --- a/src/web/public/overlay-runtime.css +++ b/src/web/public/overlay-runtime.css @@ -5,7 +5,7 @@ body { height: 100%; margin: 0; overflow: hidden; - background: transparent !important; + background-color: rgba(0, 0, 0, 0) !important; } body { @@ -19,6 +19,15 @@ body { pointer-events: none; } +.lumi-overlay-stage { + position: absolute; + left: 0; + top: 0; + overflow: hidden; + background-color: rgba(0, 0, 0, 0); + transform-origin: 0 0; +} + .lumi-overlay-module { position: absolute; box-sizing: border-box; diff --git a/src/web/views/admin-overlay-detail.ejs b/src/web/views/admin-overlay-detail.ejs index 2cc2113..7ea177e 100644 --- a/src/web/views/admin-overlay-detail.ejs +++ b/src/web/views/admin-overlay-detail.ejs @@ -50,6 +50,13 @@ <% } else { %>

Add this as a Browser Source in OBS. It follows the live scene selected below. Keep the link private. If the overlay uses video, audio, or alert sounds, enable Control audio via OBS in the Browser Source properties.

+
+ OBS Browser Source setup +

Use Lumi's canvas size for the Browser Source so websites, text, and alerts use the same layout in OBS as they do in this preview.

+
Width<%= overlay.canvas_width || 1920 %>
Height<%= overlay.canvas_height || 1080 %>
+ +
+
Open clean preview
@@ -144,7 +151,7 @@ - +
@@ -192,7 +199,7 @@
No source selected
-

Drag a source to move it. Drag its red handles to resize. Arrow keys nudge it; Shift moves farther. Hold Ctrl or Cmd to temporarily turn snapping off.

+

Rendered at <%= overlay.canvas_width || 1920 %> × <%= overlay.canvas_height || 1080 %>, then scaled down to fit here. Drag a source to move it. Drag its red handles to resize. Arrow keys nudge it; Shift moves farther. Hold Ctrl or Cmd to temporarily turn snapping off.

diff --git a/src/web/views/overlay-render.ejs b/src/web/views/overlay-render.ejs index b5e8eff..272ec3e 100644 --- a/src/web/views/overlay-render.ejs +++ b/src/web/views/overlay-render.ejs @@ -3,10 +3,10 @@ - Lumi Overlay +
diff --git a/update-manifest.json b/update-manifest.json index 1db7f89..470dbf9 100644 --- a/update-manifest.json +++ b/update-manifest.json @@ -1,6 +1,6 @@ { "name": "Lumi Core", - "version": "0.2.11", + "version": "0.2.12", "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 diagnostics, Windows/network-share-safe updates, Node.js 24-compatible dependency startup, and structured redacted logging with automatic retention. Lumi preserves existing settings, databases, logs, plugin data, community knowledge, AI models, runtimes, uploads, feedback, and secrets.", + "migration_notes": "Includes the 1.2.0 version correction, production diagnostics, Windows/network-share-safe updates, Node.js 24-compatible dependency startup, structured redacted logging, and OBS overlay transparency/layout parity fixes. Lumi preserves existing overlays, tokens, scenes, sources, settings, databases, logs, plugin data, community knowledge, AI models, runtimes, uploads, feedback, and secrets.", "rollback_safe": true, "requirements": [ "Node.js 18 or newer" @@ -157,6 +157,18 @@ ], "rollback_safe": true, "migration_notes": "Adds structured logging columns, redaction, retention, and the enhanced administrator log viewer; existing log rows and all other local data remain preserved." + }, + { + "version": "0.2.12", + "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 OBS overlay transparency and preview-to-output layout parity; existing overlay data, tokens, scenes, sources, and all other local data remain preserved." } ] }