diff --git a/frontend/js/views/designer.js b/frontend/js/views/designer.js
index 745dd6c..72b2ade 100644
--- a/frontend/js/views/designer.js
+++ b/frontend/js/views/designer.js
@@ -508,16 +508,19 @@ function updateLayers() {
function generateInnerHTML() {
let html = '';
elements.forEach((el, i) => {
+ // Use vw units for font sizes (same as designer preview) so output scales to any viewport
+ const fs = el.fontSize / 10;
+ const fsLabel = el.fontSize / 15;
switch (el.type) {
case 'text':
- html += `
${el.text}
`;
+ html += `${el.text}
`;
break;
case 'clock':
- html += `
+ html += `
`;
break;
case 'date':
- html += `
+ html += `
`;
break;
case 'image':
@@ -530,19 +533,19 @@ function generateInnerHTML() {
html += ``;
break;
case 'weather':
- html += `Loading...
+ html += `Loading...
`;
break;
case 'ticker':
html += `
+ Loading...
`;
break;
case 'countdown':
html += `
+ ${el.label}
+
`;
break;
case 'webpage':
diff --git a/server/db/database.js b/server/db/database.js
index 78cace2..56f5218 100644
--- a/server/db/database.js
+++ b/server/db/database.js
@@ -59,6 +59,38 @@ for (const sql of migrations) {
try { db.exec(sql); } catch (e) { /* already exists */ }
}
+// Fix assignments table: make content_id nullable (SQLite requires table rebuild)
+try {
+ const colInfo = db.prepare("PRAGMA table_info(assignments)").all();
+ const contentCol = colInfo.find(c => c.name === 'content_id');
+ if (contentCol && contentCol.notnull === 1) {
+ console.log('Migrating assignments table: making content_id nullable...');
+ db.exec(`
+ CREATE TABLE assignments_new (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ device_id TEXT NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
+ content_id TEXT REFERENCES content(id) ON DELETE CASCADE,
+ widget_id TEXT REFERENCES widgets(id) ON DELETE CASCADE,
+ zone_id TEXT,
+ sort_order INTEGER NOT NULL DEFAULT 0,
+ duration_sec INTEGER NOT NULL DEFAULT 10,
+ schedule_start TEXT,
+ schedule_end TEXT,
+ schedule_days TEXT,
+ enabled INTEGER NOT NULL DEFAULT 1,
+ muted INTEGER DEFAULT 0,
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
+ );
+ INSERT INTO assignments_new SELECT id, device_id, content_id, widget_id, zone_id, sort_order, duration_sec, schedule_start, schedule_end, schedule_days, enabled, muted, created_at FROM assignments;
+ DROP TABLE assignments;
+ ALTER TABLE assignments_new RENAME TO assignments;
+ `);
+ console.log('Assignments table migrated successfully.');
+ }
+} catch (e) {
+ console.error('Assignments migration error:', e.message);
+}
+
// Prune old telemetry (keep last 24h worth at 15s intervals = ~5760, cap at 6000)
function pruneTelemetry(deviceId) {
db.prepare(`
diff --git a/server/player/index.html b/server/player/index.html
index f76eb18..9bd2838 100644
--- a/server/player/index.html
+++ b/server/player/index.html
@@ -436,7 +436,7 @@
const newItems = data.assignments || [];
// Build fingerprint from id + url + filename to detect any content change
- const fingerprint = (items) => items.map(a => `${a.content_id}|${a.remote_url || ''}|${a.filepath || ''}|${a.filename || ''}`).join(',');
+ const fingerprint = (items) => items.map(a => `${a.content_id || ''}|${a.widget_id || ''}|${a.remote_url || ''}|${a.filepath || ''}|${a.filename || ''}`).join(',');
const newFp = fingerprint(newItems);
const oldFp = fingerprint(playlist);
@@ -693,6 +693,14 @@
container.appendChild(img);
// Auto advance for images
setTimeout(nextItem, (item.duration_sec || 10) * 1000);
+ } else if (item.widget_id) {
+ const iframe = document.createElement('iframe');
+ iframe.src = `${serverUrl}/api/widgets/${item.widget_id}/render`;
+ iframe.style.cssText = 'width:100%;height:100%;border:none;background:#000';
+ iframe.allow = 'autoplay; fullscreen';
+ container.appendChild(iframe);
+ // Auto advance for widgets
+ setTimeout(nextItem, (item.duration_sec || 30) * 1000);
}
}
}
diff --git a/server/routes/widgets.js b/server/routes/widgets.js
index cbb3b93..c180165 100644
--- a/server/routes/widgets.js
+++ b/server/routes/widgets.js
@@ -207,18 +207,26 @@ load(); setInterval(load, 300000);
}
function renderText(c) {
+ // Designer preview uses fontSize/10 vw, but older published HTML used fontSize*10.8 px.
+ // Convert any px-based font sizes to vw so they scale to any viewport: px / 108 = vw
+ let html = c.html || 'Empty text widget
';
+ html = html.replace(/font-size:\s*([\d.]+)px/g, (match, px) => {
+ return `font-size:${(parseFloat(px) / 108).toFixed(2)}vw`;
+ });
return `${c.html || 'Empty text widget
'}`;
+${html}