import { api } from '../api.js';
import { showToast } from '../components/toast.js';
import { getLanguage, setLanguage, getAvailableLanguages } from '../i18n.js';
export async function render(container) {
const serverUrl = `${window.location.protocol}//${window.location.host}`;
const user = JSON.parse(localStorage.getItem('user') || '{}');
const isSuperAdmin = user.role === 'superadmin';
const isAdmin = user.role === 'admin' || isSuperAdmin;
container.innerHTML = `
${isAdmin ? `
License
MIT License - all features included.
${isSuperAdmin ? 'Platform admin tools are in the Admin page.
' : ''}
` : ''}
Server Information
Server URL
${serverUrl}
Use this URL when setting up the Android app
API Endpoint
${serverUrl}/api
Setup Guide
- Install the ScreenTinker APK on your Apolosign portable TV via sideloading
- Open the app and enter this server URL:
${serverUrl}
- The app will display a 6-digit pairing code
- Click "Add Display" on the dashboard and enter the pairing code
- Upload content in the Content Library
- Assign content to the display's Playlist
${isAdmin ? `
` : ''}
Your Data
Export or import your devices, content, layouts, schedules, and all settings. Use this to migrate between cloud and self-hosted instances.
Language
`;
if (isAdmin) {
loadUsers();
loadWhiteLabel();
// Support token generator
document.getElementById('generateSupportBtn')?.addEventListener('click', async () => {
const org = document.getElementById('supportOrg').value.trim() || 'Customer';
const hours = parseInt(document.getElementById('supportHours').value) || 4;
try {
const token = localStorage.getItem('token');
const res = await fetch('/api/auth/support/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({ org, hours, reason: 'Support session' })
});
const data = await res.json();
if (res.ok) {
document.getElementById('supportTokenOutput').value = data.token;
document.getElementById('supportTokenResult').style.display = 'block';
showToast(`Support token generated (valid ${hours}h)`, 'success');
} else showToast(data.error, 'error');
} catch (err) { showToast(err.message, 'error'); }
});
}
// Export data handler
document.getElementById('exportDataBtn')?.addEventListener('click', () => {
const includeFiles = document.getElementById('exportIncludeFiles')?.checked;
const token = localStorage.getItem('token');
const url = `/api/status/export?token=${token}${includeFiles ? '&include_files=true' : ''}`;
window.location.href = url;
});
// Import data handler
document.getElementById('importDataBtn')?.addEventListener('click', () => {
document.getElementById('importFileInput').click();
});
document.getElementById('importFileInput')?.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const isZip = file.name.endsWith('.zip') || file.type === 'application/zip';
const statusEl = document.getElementById('importStatus');
statusEl.style.display = 'block';
statusEl.style.background = 'var(--bg-secondary)';
statusEl.style.border = '1px solid var(--border)';
statusEl.style.color = 'var(--text-secondary)';
statusEl.textContent = 'Reading file...';
try {
let data;
if (isZip) {
// For ZIP, show basic info and skip preview parsing
data = { format: 'screentinker-export-v1', _isZip: true };
statusEl.innerHTML = `ZIP export detected: ${file.name} (${(file.size / 1048576).toFixed(1)} MB)
Contains data + media files.
`;
} else {
const text = await file.text();
data = JSON.parse(text);
if (!data.format || !data.format.startsWith('screentinker-export')) {
statusEl.style.color = 'var(--danger)';
statusEl.textContent = 'Invalid file. Must be a ScreenTinker export JSON or ZIP.';
return;
}
const summary = [
data.devices?.length ? `${data.devices.length} devices` : null,
data.content?.length ? `${data.content.length} content items` : null,
data.widgets?.length ? `${data.widgets.length} widgets` : null,
data.layouts?.length ? `${data.layouts.length} layouts` : null,
data.schedules?.length ? `${data.schedules.length} schedules` : null,
data.video_walls?.length ? `${data.video_walls.length} video walls` : null,
data.kiosk_pages?.length ? `${data.kiosk_pages.length} kiosk pages` : null,
].filter(Boolean).join(', ');
statusEl.innerHTML = `Found: ${summary || 'empty export'}.
From: ${data.user?.email || 'unknown'} (exported ${data.exported_at?.split('T')[0] || 'unknown'})
`;
}
document.getElementById('cancelImportBtn').onclick = () => { statusEl.style.display = 'none'; e.target.value = ''; };
document.getElementById('confirmImportBtn').onclick = async () => {
statusEl.innerHTML = isZip ? 'Uploading and importing... This may take a moment for large files.' : 'Importing...';
try {
const token = localStorage.getItem('token');
let res;
if (isZip) {
const formData = new FormData();
formData.append('file', file);
res = await fetch('/api/status/import', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: formData,
});
} else {
res = await fetch('/api/status/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify(data),
});
}
const result = await res.json();
if (res.ok) {
const imported = Object.entries(result.stats).filter(([k,v]) => v > 0 && k !== 'files_restored').map(([k,v]) => `${v} ${k}`).join(', ');
statusEl.style.color = 'var(--success)';
let html = `Import complete: ${imported}.`;
if (result.device_pairings?.length) {
html += `
Device Pairing Codes:
` +
result.device_pairings.map(d => `| ${d.name} | ${d.pairing_code} |
`).join('') +
`
Enter these codes on each device to re-link them. All assignments and schedules will be preserved.`;
}
html += `
${(result.notes || []).map(n => '• ' + n).join('
')}`;
statusEl.innerHTML = html;
showToast('Data imported successfully', 'success');
} else {
statusEl.style.color = 'var(--danger)';
statusEl.textContent = result.error || 'Import failed';
}
} catch (err) {
statusEl.style.color = 'var(--danger)';
statusEl.textContent = 'Import failed: ' + err.message;
}
e.target.value = '';
};
} catch (err) {
statusEl.style.color = 'var(--danger)';
statusEl.textContent = 'Failed to read file: ' + err.message;
}
});
document.getElementById('langSelect')?.addEventListener('change', (e) => {
setLanguage(e.target.value);
showToast('Language changed. Refresh for full effect.', 'info');
});
}
async function loadWhiteLabel() {
const token = localStorage.getItem('token');
const headers = { Authorization: `Bearer ${token}` };
// Only show white-label for enterprise/superadmin
const user = JSON.parse(localStorage.getItem('user') || '{}');
const section = document.getElementById('whiteLabelSection');
if (section && user.plan_id !== 'enterprise' && user.role !== 'superadmin') {
section.innerHTML = `
White Label / Branding
Custom branding is available on the Enterprise plan
View Plans
`;
return;
}
try {
const res = await fetch('/api/white-label', { headers });
const wl = await res.json();
if (wl.brand_name) document.getElementById('wlBrandName').value = wl.brand_name;
if (wl.logo_url) document.getElementById('wlLogoUrl').value = wl.logo_url;
if (wl.primary_color) document.getElementById('wlPrimaryColor').value = wl.primary_color;
if (wl.bg_color) document.getElementById('wlBgColor').value = wl.bg_color;
if (wl.custom_domain) document.getElementById('wlDomain').value = wl.custom_domain;
if (wl.favicon_url) document.getElementById('wlFavicon').value = wl.favicon_url;
if (wl.custom_css) document.getElementById('wlCustomCss').value = wl.custom_css;
if (wl.hide_branding) document.getElementById('wlHideBranding').checked = true;
} catch {}
document.getElementById('saveWhiteLabelBtn')?.addEventListener('click', async () => {
try {
await fetch('/api/white-label', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
brand_name: document.getElementById('wlBrandName').value,
logo_url: document.getElementById('wlLogoUrl').value,
primary_color: document.getElementById('wlPrimaryColor').value,
bg_color: document.getElementById('wlBgColor').value,
custom_domain: document.getElementById('wlDomain').value,
favicon_url: document.getElementById('wlFavicon').value,
custom_css: document.getElementById('wlCustomCss').value,
hide_branding: document.getElementById('wlHideBranding').checked ? 1 : 0,
})
});
showToast('Branding saved', 'success');
} catch (err) {
showToast(err.message, 'error');
}
});
document.getElementById('previewWhiteLabelBtn')?.addEventListener('click', () => {
const primary = document.getElementById('wlPrimaryColor').value;
const bg = document.getElementById('wlBgColor').value;
document.documentElement.style.setProperty('--accent', primary);
document.documentElement.style.setProperty('--bg-primary', bg);
showToast('Preview applied (refresh to reset)', 'info');
});
}
async function loadUsers() {
const el = document.getElementById('userManagement');
if (!el) return;
try {
const [users, plans] = await Promise.all([
api.getUsers(),
fetch('/api/subscription/plans').then(r => r.json())
]);
const currentUser = JSON.parse(localStorage.getItem('user') || '{}');
el.innerHTML = `
| User |
Auth |
Role |
Plan |
Actions |
${users.map(u => `
|
${u.name || u.email}
${u.email}
|
${u.auth_provider}
|
${u.role}
|
|
${u.id !== currentUser.id ? `` : 'You'}
|
`).join('')}
${users.length} user(s) registered
`;
// Plan change handlers
el.querySelectorAll('.plan-select').forEach(select => {
select.addEventListener('change', async () => {
const userId = select.dataset.userId;
const planId = select.value;
try {
await api.assignPlan(userId, planId);
showToast('Plan updated', 'success');
} catch (err) {
showToast(err.message, 'error');
loadUsers(); // Revert
}
});
});
// Delete user handlers
el.querySelectorAll('.delete-user-btn').forEach(btn => {
let confirming = false;
btn.addEventListener('click', async () => {
if (confirming) {
try {
await api.deleteUser(btn.dataset.userId);
showToast('User removed', 'success');
loadUsers();
} catch (err) {
showToast(err.message, 'error');
}
return;
}
confirming = true;
btn.textContent = 'Confirm?';
btn.style.background = 'var(--danger)';
btn.style.color = 'white';
setTimeout(() => {
confirming = false;
btn.textContent = 'Remove';
btn.style.background = '';
btn.style.color = '';
}, 3000);
});
});
} catch (err) {
el.innerHTML = `${err.message}
`;
}
}
export function cleanup() {}