Fix OBS overlay preview parity and transparency
This commit is contained in:
parent
6bf84122b0
commit
1cd2a62302
@ -1,5 +1,11 @@
|
|||||||
# Lumi changelog
|
# 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
|
## 0.2.11
|
||||||
|
|
||||||
- Added structured component, activity, event, and request metadata across Core, plugins, platforms, commands, webhooks, diagnostics, and WebUI administrator actions.
|
- Added structured component, activity, event, and request metadata across Core, plugins, platforms, commands, webhooks, diagnostics, and WebUI administrator actions.
|
||||||
|
|||||||
7
TODO.md
7
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
|
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
|
isolated custom frame CSS, cropping, and zoom. An optional OBS Browser Bridge now
|
||||||
syncs supported OBS scene, transition, canvas, output, permission, and source
|
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:
|
Remaining work:
|
||||||
|
|
||||||
@ -677,6 +680,8 @@ This section is for Lumi chat answer feedback and AI Improvement Center work, no
|
|||||||
|
|
||||||
## Done
|
## 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: 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.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.
|
- 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.
|
||||||
|
|||||||
@ -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
|
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.
|
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 a source to move it.
|
||||||
- Drag any corner or edge handle to resize it.
|
- Drag any corner or edge handle to resize it.
|
||||||
- Use arrow keys for small moves, or Shift plus an arrow for larger moves.
|
- 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.
|
security; browser load-timeout recovery still applies.
|
||||||
|
|
||||||
Website sources also support per-edge percentage cropping, zoom, and isolated
|
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
|
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;
|
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
|
use the provider's own appearance settings, crop, and zoom to hide unwanted
|
||||||
can target `iframe`, for example `iframe { filter: saturate(1.2); }`.
|
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
|
## OBS WebSocket
|
||||||
|
|
||||||
|
|||||||
@ -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.11
|
Version: 0.2.12
|
||||||
## Routes
|
## Routes
|
||||||
- GET /api/events
|
- GET /api/events
|
||||||
- POST /api/destructive-confirmations
|
- POST /api/destructive-confirmations
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.11",
|
"version": "0.2.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "lumi-bot",
|
"name": "lumi-bot",
|
||||||
"version": "0.2.11",
|
"version": "0.2.12",
|
||||||
"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.11",
|
"version": "0.2.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -2,6 +2,36 @@
|
|||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"channel": "stable",
|
"channel": "stable",
|
||||||
"releases": [
|
"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",
|
"version": "0.2.11",
|
||||||
"ref": "refs/tags/v0.2.11",
|
"ref": "refs/tags/v0.2.11",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
const assert = require("assert");
|
const assert = require("assert");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const vm = require("vm");
|
||||||
|
|
||||||
const root = path.join(__dirname, "..");
|
const root = path.join(__dirname, "..");
|
||||||
const sandbox = fs.mkdtempSync(path.join(root, ".tmp-lumi-overlays-"));
|
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 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 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 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 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");
|
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-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="text"') && editorView.includes('data-module-fields="web"'));
|
||||||
assert(editorView.includes('data-module-fields="video"') && editorView.includes('data-module-fields="audio"'));
|
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('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-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(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"));
|
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(runtimeScript.includes("window.obsstudio") && runtimeScript.includes("overlay:obs-browser-command"));
|
||||||
assert(rendererScript.includes("buildMedia") && rendererScript.includes("buildWebsite"));
|
assert(rendererScript.includes("buildMedia") && rendererScript.includes("buildWebsite"));
|
||||||
assert(rendererScript.includes('frame.setAttribute("allow", "autoplay; fullscreen")'));
|
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(connectorScript.includes('id: "obs_browser_bridge"') && connectorScript.includes("acceptReport(report"));
|
||||||
assert(routeScript.includes("/obs-bridge") && routeScript.includes("/obs/import"));
|
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 {
|
} finally {
|
||||||
database?.db.close();
|
database?.db.close();
|
||||||
fs.rmSync(sandbox, { recursive: true, force: true });
|
fs.rmSync(sandbox, { recursive: true, force: true });
|
||||||
|
|||||||
@ -4,8 +4,8 @@ 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.11";
|
const releaseVersion = "0.2.12";
|
||||||
const previousCoreVersion = "0.2.10";
|
const previousCoreVersion = "0.2.11";
|
||||||
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" },
|
||||||
@ -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.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.");
|
||||||
|
|||||||
@ -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.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");
|
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.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.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.10", ref: "refs/tags/v0.2.10", rollback_safe: true },
|
||||||
{ version: "0.2.9", ref: "refs/tags/v0.2.9", 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"
|
channel: "stable"
|
||||||
});
|
});
|
||||||
assert.equal(corrected.version_correction, true);
|
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.update_available, true);
|
||||||
assert.equal(corrected.blocked, false);
|
assert.equal(corrected.blocked, false);
|
||||||
|
|
||||||
|
|||||||
5
src/services/obs-browser-defaults.js
Normal file
5
src/services/obs-browser-defaults.js
Normal 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
|
||||||
|
};
|
||||||
@ -34,6 +34,7 @@ const {
|
|||||||
updateOverlay,
|
updateOverlay,
|
||||||
updateScene
|
updateScene
|
||||||
} = require("./overlays");
|
} = require("./overlays");
|
||||||
|
const { OBS_BROWSER_DEFAULT_CSS } = require("./obs-browser-defaults");
|
||||||
const {
|
const {
|
||||||
listOverlayConnectorProviders,
|
listOverlayConnectorProviders,
|
||||||
overlayConnectorManager
|
overlayConnectorManager
|
||||||
@ -219,6 +220,7 @@ function registerPublicOverlayRoutes(app) {
|
|||||||
eventsPath,
|
eventsPath,
|
||||||
healthPath,
|
healthPath,
|
||||||
bridgePath,
|
bridgePath,
|
||||||
|
obsBrowserDefaultCss: OBS_BROWSER_DEFAULT_CSS,
|
||||||
initialStateJson: JSON.stringify(buildPublicState(context.overlay.id, fixed ? context.fixedScene.id : null)).replace(/</g, "\\u003c")
|
initialStateJson: JSON.stringify(buildPublicState(context.overlay.id, fixed ? context.fixedScene.id : null)).replace(/</g, "\\u003c")
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -376,6 +378,7 @@ function registerOverlayAdminRoutes(app) {
|
|||||||
canViewSecrets,
|
canViewSecrets,
|
||||||
canManageObs,
|
canManageObs,
|
||||||
baseUrl: baseUrl(req),
|
baseUrl: baseUrl(req),
|
||||||
|
obsBrowserDefaultCss: OBS_BROWSER_DEFAULT_CSS,
|
||||||
moduleTypes: listOverlayModuleTypes(),
|
moduleTypes: listOverlayModuleTypes(),
|
||||||
editorStateJson: JSON.stringify({
|
editorStateJson: JSON.stringify({
|
||||||
overlayId: overlay.id,
|
overlayId: overlay.id,
|
||||||
|
|||||||
@ -10,6 +10,53 @@
|
|||||||
font: 0.78rem/1.4 var(--lumi-font-mono);
|
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-list-actions,
|
||||||
.overlay-scene-actions,
|
.overlay-scene-actions,
|
||||||
.overlay-module-actions,
|
.overlay-module-actions,
|
||||||
@ -349,7 +396,7 @@
|
|||||||
.overlay-canvas .lumi-overlay-module.is-selected {
|
.overlay-canvas .lumi-overlay-module.is-selected {
|
||||||
z-index: 1000 !important;
|
z-index: 1000 !important;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
outline: 2px solid #ef4444;
|
outline: var(--lumi-editor-outline-size, 2px) solid #ef4444;
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -358,13 +405,13 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
display: none;
|
display: none;
|
||||||
max-width: 12rem;
|
max-width: var(--lumi-editor-label-max-width, 12rem);
|
||||||
padding: 0.2rem 0.4rem;
|
padding: var(--lumi-editor-label-padding-y, 0.2rem) var(--lumi-editor-label-padding-x, 0.4rem);
|
||||||
overflow: hidden;
|
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;
|
background: #ef4444;
|
||||||
color: white;
|
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;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@ -374,8 +421,8 @@
|
|||||||
.overlay-resize-handle {
|
.overlay-resize-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
width: 10px;
|
width: var(--lumi-editor-handle-size, 10px);
|
||||||
height: 10px;
|
height: var(--lumi-editor-handle-size, 10px);
|
||||||
display: none;
|
display: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
@ -457,7 +504,7 @@
|
|||||||
.overlay-url-row { grid-template-columns: 1fr; }
|
.overlay-url-row { grid-template-columns: 1fr; }
|
||||||
.overlay-transform-fields,
|
.overlay-transform-fields,
|
||||||
.overlay-conditional-fields { grid-template-columns: 1fr 1fr; }
|
.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) {
|
@media (max-width: 480px) {
|
||||||
|
|||||||
@ -104,6 +104,7 @@
|
|||||||
frame.sandbox = sandbox;
|
frame.sandbox = sandbox;
|
||||||
frame.referrerPolicy = "no-referrer";
|
frame.referrerPolicy = "no-referrer";
|
||||||
frame.setAttribute("allow", "autoplay; fullscreen");
|
frame.setAttribute("allow", "autoplay; fullscreen");
|
||||||
|
frame.setAttribute("allowtransparency", "true");
|
||||||
frame.style.pointerEvents = options.editor ? "none" : (values.allowPointerEvents ? "auto" : "none");
|
frame.style.pointerEvents = options.editor ? "none" : (values.allowPointerEvents ? "auto" : "none");
|
||||||
|
|
||||||
const layout = webFrameLayout(values);
|
const layout = webFrameLayout(values);
|
||||||
@ -112,9 +113,9 @@
|
|||||||
const shadow = root.attachShadow({ mode: "open" });
|
const shadow = root.attachShadow({ mode: "open" });
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.textContent = `
|
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 {
|
.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}%;
|
width: ${layout.width}%; height: ${layout.height}%;
|
||||||
left: ${layout.left}%; top: ${layout.top}%;
|
left: ${layout.left}%; top: ${layout.top}%;
|
||||||
}
|
}
|
||||||
@ -199,22 +200,47 @@
|
|||||||
|
|
||||||
function render(root, state, options = {}) {
|
function render(root, state, options = {}) {
|
||||||
const canvas = options.canvas || state?.canvas || { width: 1920, height: 1080 };
|
const canvas = options.canvas || state?.canvas || { width: 1920, height: 1080 };
|
||||||
const widthScale = root.clientWidth ? root.clientWidth / finite(canvas.width, 1920) : 1;
|
const layout = stageLayout(root, canvas, options.scale);
|
||||||
const heightScale = root.clientHeight ? root.clientHeight / finite(canvas.height, 1080) : widthScale;
|
const stage = document.createElement("div");
|
||||||
const scale = options.scale || Math.min(widthScale, heightScale);
|
stage.className = "lumi-overlay-stage";
|
||||||
const fragment = document.createDocumentFragment();
|
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();
|
const elements = new Map();
|
||||||
if (state?.enabled !== false && state?.scene) {
|
if (state?.enabled !== false && state?.scene) {
|
||||||
for (const module of state.scene.modules || []) {
|
for (const module of state.scene.modules || []) {
|
||||||
if (module.enabled === false) continue;
|
if (module.enabled === false) continue;
|
||||||
const element = buildModule(module, { ...options, scale });
|
const element = buildModule(module, { ...options, canvas, scale: 1 });
|
||||||
elements.set(module.id, element);
|
elements.set(module.id, element);
|
||||||
fragment.appendChild(element);
|
stage.appendChild(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
root.replaceChildren(fragment);
|
root.replaceChildren(stage);
|
||||||
return elements;
|
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 };
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -5,7 +5,7 @@ body {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: transparent !important;
|
background-color: rgba(0, 0, 0, 0) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@ -19,6 +19,15 @@ body {
|
|||||||
pointer-events: none;
|
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 {
|
.lumi-overlay-module {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@ -50,6 +50,13 @@
|
|||||||
<% } else { %>
|
<% } 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>
|
<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>
|
<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">
|
<div class="overlay-list-actions">
|
||||||
<a class="button subtle" href="<%= controlledUrl %>" target="_blank" rel="noreferrer">Open clean preview</a>
|
<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>
|
<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 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>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>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><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>
|
</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-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-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>
|
<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>
|
<div class="overlay-canvas-source-list" data-canvas-source-list></div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,10 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" />
|
<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" />
|
<meta name="referrer" content="no-referrer" />
|
||||||
<title>Lumi Overlay</title>
|
<title>Lumi Overlay</title>
|
||||||
<link rel="stylesheet" href="/overlay-runtime.css" />
|
<link rel="stylesheet" href="/overlay-runtime.css" />
|
||||||
|
<style id="obs-browser-default-css"><%- obsBrowserDefaultCss %></style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="lumi-overlay-root" aria-live="off"></main>
|
<main id="lumi-overlay-root" aria-live="off"></main>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Lumi Core",
|
"name": "Lumi Core",
|
||||||
"version": "0.2.11",
|
"version": "0.2.12",
|
||||||
"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 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,
|
"rollback_safe": true,
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"Node.js 18 or newer"
|
"Node.js 18 or newer"
|
||||||
@ -157,6 +157,18 @@
|
|||||||
],
|
],
|
||||||
"rollback_safe": true,
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user