Add logging and toasts

This commit is contained in:
2025-12-10 14:01:59 +01:00
parent 8444f1873a
commit 53410dc235
10 changed files with 363 additions and 31 deletions

View File

@@ -1058,3 +1058,92 @@ body {
.avatar-fallback {
border: 1px solid rgba(255, 255, 255, 0.08);
}
.toast-container {
position: fixed;
top: 16px;
right: 16px;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 10000;
max-width: 360px;
}
.toast {
display: grid;
grid-template-columns: auto 1fr;
gap: 12px;
align-items: center;
padding: 12px 14px;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
border: 1px solid rgba(255, 255, 255, 0.08);
background: #0b1221;
color: #e5e7eb;
cursor: pointer;
transition: transform 120ms ease, opacity 120ms ease;
}
.toast:hover {
transform: translateY(-2px);
}
.toast-exit {
opacity: 0;
transform: translateY(-6px);
}
.toast-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #a5b4fc;
box-shadow: 0 0 0 4px rgba(165, 180, 252, 0.16);
}
.toast-message {
margin: 0;
font-size: 14px;
line-height: 1.4;
}
.toast-success {
border-color: rgba(34, 197, 94, 0.35);
background: rgba(16, 185, 129, 0.12);
}
.toast-success .toast-indicator {
background: #34d399;
box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.2);
}
.toast-error {
border-color: rgba(239, 68, 68, 0.35);
background: rgba(248, 113, 113, 0.12);
}
.toast-error .toast-indicator {
background: #f87171;
box-shadow: 0 0 0 4px rgba(248, 113, 113, 0.2);
}
.toast-warning {
border-color: rgba(251, 191, 36, 0.35);
background: rgba(251, 191, 36, 0.12);
}
.toast-warning .toast-indicator {
background: #facc15;
box-shadow: 0 0 0 4px rgba(250, 204, 21, 0.2);
}
.toast-info {
border-color: rgba(96, 165, 250, 0.35);
background: rgba(96, 165, 250, 0.12);
}
.toast-info .toast-indicator {
background: #60a5fa;
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.2);
}

View File

@@ -154,21 +154,48 @@ function connect() {
handleEvent(body);
});
fetchAssets();
}, (error) => {
console.warn('WebSocket connection issue', error);
if (typeof showToast === 'function') {
showToast('Live updates connection interrupted. Retrying may be necessary.', 'warning');
}
});
}
function fetchAssets() {
fetch(`/api/channels/${broadcaster}/assets`).then((r) => r.json()).then(renderAssets);
fetch(`/api/channels/${broadcaster}/assets`)
.then((r) => {
if (!r.ok) {
throw new Error('Failed to load assets');
}
return r.json();
})
.then(renderAssets)
.catch(() => {
if (typeof showToast === 'function') {
showToast('Unable to load assets. Please refresh.', 'error');
}
});
}
function fetchCanvasSettings() {
return fetch(`/api/channels/${broadcaster}/canvas`)
.then((r) => r.json())
.then((r) => {
if (!r.ok) {
throw new Error('Failed to load canvas');
}
return r.json();
})
.then((settings) => {
canvasSettings = settings;
resizeCanvas();
})
.catch(() => resizeCanvas());
.catch(() => {
resizeCanvas();
if (typeof showToast === 'function') {
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
}
});
}
function resizeCanvas() {
@@ -1350,29 +1377,58 @@ function updateVisibility(asset, hidden) {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ hidden })
}).then((r) => r.json()).then((updated) => {
}).then((r) => {
if (!r.ok) {
throw new Error('Failed to update visibility');
}
return r.json();
}).then((updated) => {
storeAsset(updated);
if (updated.hidden) {
stopAudio(updated.id);
if (typeof showToast === 'function') {
showToast('Asset hidden from broadcast.', 'info');
}
} else if (isAudioAsset(updated)) {
playAudioFromCanvas(updated, true);
if (typeof showToast === 'function') {
showToast('Asset is now visible and active.', 'success');
}
} else if (typeof showToast === 'function') {
showToast('Asset is now visible.', 'success');
}
updateRenderState(updated);
drawAndList();
}).catch(() => {
if (typeof showToast === 'function') {
showToast('Unable to change visibility right now.', 'error');
}
});
}
function deleteAsset(asset) {
fetch(`/api/channels/${broadcaster}/assets/${asset.id}`, { method: 'DELETE' }).then(() => {
clearMedia(asset.id);
assets.delete(asset.id);
renderStates.delete(asset.id);
zOrderDirty = true;
if (selectedAssetId === asset.id) {
selectedAssetId = null;
}
drawAndList();
});
fetch(`/api/channels/${broadcaster}/assets/${asset.id}`, { method: 'DELETE' })
.then((response) => {
if (!response.ok) {
throw new Error('Failed to delete asset');
}
clearMedia(asset.id);
assets.delete(asset.id);
renderStates.delete(asset.id);
zOrderDirty = true;
if (selectedAssetId === asset.id) {
selectedAssetId = null;
}
drawAndList();
if (typeof showToast === 'function') {
showToast('Asset deleted.', 'info');
}
})
.catch(() => {
if (typeof showToast === 'function') {
showToast('Unable to delete asset. Please try again.', 'error');
}
});
}
function handleFileSelection(input) {
@@ -1391,7 +1447,9 @@ function uploadAsset(file = null) {
const fileInput = document.getElementById('asset-file');
const selectedFile = file || (fileInput?.files && fileInput.files.length ? fileInput.files[0] : null);
if (!selectedFile) {
alert('Please choose an image, GIF, video, or audio file to upload.');
if (typeof showToast === 'function') {
showToast('Choose an image, GIF, video, or audio file to upload.', 'info');
}
return;
}
const data = new FormData();
@@ -1402,15 +1460,24 @@ function uploadAsset(file = null) {
fetch(`/api/channels/${broadcaster}/assets`, {
method: 'POST',
body: data
}).then(() => {
}).then((response) => {
if (!response.ok) {
throw new Error('Upload failed');
}
if (fileInput) {
fileInput.value = '';
handleFileSelection(fileInput);
}
if (typeof showToast === 'function') {
showToast('Asset uploaded successfully.', 'success');
}
}).catch(() => {
if (fileNameLabel) {
fileNameLabel.textContent = 'Upload failed';
}
if (typeof showToast === 'function') {
showToast('Upload failed. Please try again with a supported file.', 'error');
}
});
}
@@ -1462,12 +1529,21 @@ function persistTransform(asset, silent = false) {
audioPitch: asset.audioPitch,
audioVolume: asset.audioVolume
})
}).then((r) => r.json()).then((updated) => {
}).then((r) => {
if (!r.ok) {
throw new Error('Transform failed');
}
return r.json();
}).then((updated) => {
storeAsset(updated);
updateRenderState(updated);
if (!silent) {
drawAndList();
}
}).catch(() => {
if (!silent && typeof showToast === 'function') {
showToast('Unable to save changes. Please retry.', 'error');
}
});
}

View File

@@ -36,7 +36,19 @@ function connect() {
const body = JSON.parse(payload.body);
handleEvent(body);
});
fetch(`/api/channels/${broadcaster}/assets/visible`).then(r => r.json()).then(renderAssets);
fetch(`/api/channels/${broadcaster}/assets/visible`)
.then((r) => {
if (!r.ok) {
throw new Error('Failed to load assets');
}
return r.json();
})
.then(renderAssets)
.catch(() => {
if (typeof showToast === 'function') {
showToast('Unable to load overlay assets. Retrying may help.', 'error');
}
});
});
}
@@ -51,12 +63,22 @@ function renderAssets(list) {
function fetchCanvasSettings() {
return fetch(`/api/channels/${broadcaster}/canvas`)
.then((r) => r.json())
.then((r) => {
if (!r.ok) {
throw new Error('Failed to load canvas');
}
return r.json();
})
.then((settings) => {
canvasSettings = settings;
resizeCanvas();
})
.catch(() => resizeCanvas());
.catch(() => {
resizeCanvas();
if (typeof showToast === 'function') {
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
}
});
}
function resizeCanvas() {

View File

@@ -57,23 +57,44 @@ function renderAdmins(list) {
function fetchAdmins() {
fetch(`/api/channels/${broadcaster}/admins`)
.then((r) => r.json())
.then((r) => {
if (!r.ok) {
throw new Error('Failed to load admins');
}
return r.json();
})
.then(renderAdmins)
.catch(() => renderAdmins([]));
.catch(() => {
renderAdmins([]);
if (typeof showToast === 'function') {
showToast('Unable to load admins right now. Please try again.', 'error');
}
});
}
function removeAdmin(username) {
if (!username) return;
fetch(`/api/channels/${broadcaster}/admins/${encodeURIComponent(username)}`, {
method: 'DELETE'
}).then(fetchAdmins);
}).then((response) => {
if (!response.ok && typeof showToast === 'function') {
showToast('Failed to remove admin. Please retry.', 'error');
}
fetchAdmins();
}).catch(() => {
if (typeof showToast === 'function') {
showToast('Failed to remove admin. Please retry.', 'error');
}
});
}
function addAdmin() {
const input = document.getElementById('new-admin');
const username = input.value.trim();
if (!username) {
alert('Enter a Twitch username to add as an admin.');
if (typeof showToast === 'function') {
showToast('Enter a Twitch username to add as an admin.', 'info');
}
return;
}
@@ -82,9 +103,20 @@ function addAdmin() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
.then(() => {
.then((response) => {
if (!response.ok) {
throw new Error('Add admin failed');
}
input.value = '';
if (typeof showToast === 'function') {
showToast(`Added @${username} as an admin.`, 'success');
}
fetchAdmins();
})
.catch(() => {
if (typeof showToast === 'function') {
showToast('Unable to add admin right now. Please try again.', 'error');
}
});
}
@@ -97,9 +129,19 @@ function renderCanvasSettings(settings) {
function fetchCanvasSettings() {
fetch(`/api/channels/${broadcaster}/canvas`)
.then((r) => r.json())
.then((r) => {
if (!r.ok) {
throw new Error('Failed to load canvas settings');
}
return r.json();
})
.then(renderCanvasSettings)
.catch(() => renderCanvasSettings({ width: 1920, height: 1080 }));
.catch(() => {
renderCanvasSettings({ width: 1920, height: 1080 });
if (typeof showToast === 'function') {
showToast('Using default canvas size. Unable to load saved settings.', 'warning');
}
});
}
function saveCanvasSettings() {
@@ -109,7 +151,9 @@ function saveCanvasSettings() {
const width = parseFloat(widthInput?.value) || 0;
const height = parseFloat(heightInput?.value) || 0;
if (width <= 0 || height <= 0) {
alert('Please enter a valid width and height.');
if (typeof showToast === 'function') {
showToast('Please enter a valid width and height.', 'info');
}
return;
}
if (status) status.textContent = 'Saving...';
@@ -118,16 +162,27 @@ function saveCanvasSettings() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ width, height })
})
.then((r) => r.json())
.then((r) => {
if (!r.ok) {
throw new Error('Failed to save canvas');
}
return r.json();
})
.then((settings) => {
renderCanvasSettings(settings);
if (status) status.textContent = 'Saved.';
if (typeof showToast === 'function') {
showToast('Canvas size saved successfully.', 'success');
}
setTimeout(() => {
if (status) status.textContent = '';
}, 2000);
})
.catch(() => {
if (status) status.textContent = 'Unable to save right now.';
if (typeof showToast === 'function') {
showToast('Unable to save canvas size. Please retry.', 'error');
}
});
}

View File

@@ -0,0 +1,51 @@
(function () {
const CONTAINER_ID = 'toast-container';
const DEFAULT_DURATION = 4200;
function ensureContainer() {
let container = document.getElementById(CONTAINER_ID);
if (!container) {
container = document.createElement('div');
container.id = CONTAINER_ID;
container.className = 'toast-container';
container.setAttribute('aria-live', 'polite');
container.setAttribute('aria-atomic', 'true');
document.body.appendChild(container);
}
return container;
}
function buildToast(message, type) {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
const indicator = document.createElement('span');
indicator.className = 'toast-indicator';
indicator.setAttribute('aria-hidden', 'true');
const content = document.createElement('div');
content.className = 'toast-message';
content.textContent = message;
toast.appendChild(indicator);
toast.appendChild(content);
return toast;
}
function removeToast(toast) {
if (!toast) return;
toast.classList.add('toast-exit');
setTimeout(() => toast.remove(), 250);
}
window.showToast = function showToast(message, type = 'info', options = {}) {
if (!message) return;
const normalized = ['success', 'error', 'warning', 'info'].includes(type) ? type : 'info';
const duration = typeof options.duration === 'number' ? options.duration : DEFAULT_DURATION;
const container = ensureContainer();
const toast = buildToast(message, normalized);
container.appendChild(toast);
setTimeout(() => removeToast(toast), Math.max(1200, duration));
toast.addEventListener('click', () => removeToast(toast));
};
})();