Fix OBS overlay preview parity and transparency

This commit is contained in:
Franz Rolfsvaag 2026-07-18 21:55:41 +02:00
parent 6bf84122b0
commit 1cd2a62302
18 changed files with 233 additions and 38 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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

4
package-lock.json generated
View File

@ -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",

View File

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

View File

@ -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",

View File

@ -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('<style id="obs-browser-default-css"><%- obsBrowserDefaultCss %></style>'));
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 });

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.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.");

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.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);

View File

@ -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
};

View File

@ -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(/</g, "\\u003c")
});
});
@ -376,6 +378,7 @@ function registerOverlayAdminRoutes(app) {
canViewSecrets,
canManageObs,
baseUrl: baseUrl(req),
obsBrowserDefaultCss: OBS_BROWSER_DEFAULT_CSS,
moduleTypes: listOverlayModuleTypes(),
editorStateJson: JSON.stringify({
overlayId: overlay.id,

View File

@ -10,6 +10,53 @@
font: 0.78rem/1.4 var(--lumi-font-mono);
}
.overlay-browser-source-setup {
margin-top: var(--lumi-space-3);
padding: var(--lumi-space-3);
border: 1px solid var(--lumi-border);
border-radius: var(--lumi-radius-sm);
background: var(--lumi-surface-subtle);
}
.overlay-browser-source-setup > 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) {

View File

@ -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 };
})();

View File

@ -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;

View File

@ -50,6 +50,13 @@
<% } else { %>
<p class="hint">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 <strong>Control audio via OBS</strong> in the Browser Source properties.</p>
<div class="overlay-url-row"><input value="<%= controlledUrl %>" readonly aria-label="Private OBS link" /><button type="button" class="button subtle" data-copy-value="<%= controlledUrl %>">Copy OBS link</button></div>
<details class="overlay-browser-source-setup">
<summary>OBS Browser Source setup</summary>
<p class="hint">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.</p>
<div class="overlay-browser-dimensions" aria-label="OBS Browser Source dimensions"><div><span>Width</span><strong><%= overlay.canvas_width || 1920 %></strong></div><div><span>Height</span><strong><%= overlay.canvas_height || 1080 %></strong></div></div>
<label class="overlay-copy-label">Custom CSS <button type="button" class="overlay-help" data-tooltip="This is OBS's standard transparent Browser Source CSS. Lumi also applies it to the clean render page automatically.">?</button></label>
<div class="overlay-url-row"><input value="<%= obsBrowserDefaultCss %>" readonly aria-label="OBS default Custom CSS" /><button type="button" class="button subtle" data-copy-value="<%= obsBrowserDefaultCss %>">Copy CSS</button></div>
</details>
<div class="overlay-list-actions">
<a class="button subtle" href="<%= controlledUrl %>" target="_blank" rel="noreferrer">Open clean preview</a>
<form method="post" action="/admin/overlays/<%= overlay.id %>/token/revoke" class="inline-form" data-confirm-mode="modal" data-confirm-title="Replace private links" data-confirm-text="Replace every current OBS link for this overlay? You will need to paste the new links into OBS." data-confirm-label="Replace links"><button class="button danger" type="submit">Replace private links</button></form>
@ -144,7 +151,7 @@
<label>Crop bottom %<input type="number" name="crop_bottom" min="0" max="95" step="0.1" value="<%= value(config,"cropBottom",0) %>" /></label>
<label>Crop left %<input type="number" name="crop_left" min="0" max="95" step="0.1" value="<%= value(config,"cropLeft",0) %>" /></label>
<label>Zoom<input type="number" name="zoom" min="0.1" max="5" step="0.1" value="<%= value(config,"zoom",1) %>" /></label>
<label class="overlay-css-field">Custom frame CSS <button type="button" class="overlay-help" data-tooltip="CSS is isolated to this source frame. Browser security does not allow Lumi to select elements inside a different website; use crop and zoom to hide unwanted areas.">?</button><textarea name="custom_css" rows="6" maxlength="12000" placeholder="iframe { filter: saturate(1.2); }"><%= value(config,"customCss","") %></textarea></label>
<label class="overlay-css-field">Frame appearance CSS <button type="button" class="overlay-help" data-tooltip="This can style the frame itself. Browser security prevents Lumi from changing elements or a background explicitly drawn inside a different website; use that provider's settings, crop, and zoom for its content.">?</button><textarea name="custom_css" rows="6" maxlength="12000" placeholder=".lumi-overlay-frame { filter: saturate(1.2); }"><%= value(config,"customCss","") %></textarea></label>
<div><div class="button-group"><button type="button" class="button subtle" data-refresh-source="<%= module.id %>">Refresh now</button><button type="button" class="button subtle" data-check-source="<%= module.id %>">Check website</button></div><small class="hint" data-source-health="<%= module.id %>"></small></div>
</div>
@ -192,7 +199,7 @@
<div class="overlay-canvas-frame" style="--overlay-aspect: <%= overlay.canvas_width || 1920 %> / <%= overlay.canvas_height || 1080 %>"><div class="overlay-canvas" data-overlay-canvas tabindex="0" aria-label="Visual overlay editor"><div class="overlay-canvas-layer" data-overlay-canvas-layer></div><div class="overlay-snap-guide guide-x" data-guide-x></div><div class="overlay-snap-guide guide-y" data-guide-y></div></div></div>
<div class="overlay-editor-readout"><span data-selected-name>No source selected</span><span data-selected-position></span></div>
<div class="overlay-editor-actions"><button type="button" class="button subtle" data-editor-center>Center</button><button type="button" class="button subtle" data-editor-fit>Fit to canvas</button><button type="button" class="button subtle" data-editor-reset>Reset box</button></div>
<p class="hint">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 <span data-snap-hint>off</span>.</p>
<p class="hint">Rendered at <strong><%= overlay.canvas_width || 1920 %> × <%= overlay.canvas_height || 1080 %></strong>, 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 <span data-snap-hint>off</span>.</p>
<div class="overlay-canvas-source-list" data-canvas-source-list></div>
</aside>
</div>

View File

@ -3,10 +3,10 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
<meta name="color-scheme" content="dark" />
<meta name="referrer" content="no-referrer" />
<title>Lumi Overlay</title>
<link rel="stylesheet" href="/overlay-runtime.css" />
<style id="obs-browser-default-css"><%- obsBrowserDefaultCss %></style>
</head>
<body>
<main id="lumi-overlay-root" aria-live="off"></main>

View File

@ -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."
}
]
}