mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
Three loose ends from the interface review. Inviting a colleague is a core action and had no entry in the navigation at all. The only route was an unlabelled icon beside the workspace name, or typing the URL. There is now a Members item, translated, which resolves to the active workspace so the static link needs no id. The Teams entry it sits near stays hidden, since that feature is still switched off. A native title= is hover-only, so the icon-only buttons — rename a wall, remove a device from one, manage members — explained themselves on a desktop and said nothing on a touchscreen. Long-pressing one now shows its label. The text was already there and already translated; it simply had no way to reach a finger. The last one is the bug that took a real screen dark. A device row can vanish while its socket is still heartbeating, and the telemetry insert then failed a foreign key. That throw was fatal in a way that is hard to guess: the safe-socket wrapper reads a throwing handler as a broken one and disconnects the socket server-side, and socket.io deliberately does not retry that kind of disconnect — so the player sat doing nothing until a person reloaded it. A heartbeat for a device that no longer exists is an ordinary race, not a fault worth ending a connection over; the write is skipped and the register path answers unpaired, which is the reply that actually helps the client recover. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
240 lines
15 KiB
HTML
240 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="theme-color" content="#111827">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
<link rel="manifest" href="/manifest.json">
|
|
<link rel="icon" href="/assets/icon-192.png">
|
|
<link rel="apple-touch-icon" href="/assets/icon-192.png">
|
|
<title>ScreenTinker</title>
|
|
<link rel="stylesheet" href="/css/variables.css">
|
|
<link rel="stylesheet" href="/css/reset.css">
|
|
<link rel="stylesheet" href="/css/main.css">
|
|
<script src="/socket.io/socket.io.js"></script>
|
|
<!-- OAuth providers loaded on-demand by login.js when needed -->
|
|
</head>
|
|
<body>
|
|
<button class="mobile-menu-btn" id="mobileMenuBtn" aria-label="Toggle navigation menu" aria-expanded="false" aria-controls="sidebar">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
</button>
|
|
<div class="sidebar-backdrop" id="sidebarBackdrop"></div>
|
|
<nav class="sidebar" id="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="logo">
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
|
<line x1="8" y1="21" x2="16" y2="21"/>
|
|
<line x1="12" y1="17" x2="12" y2="21"/>
|
|
</svg>
|
|
<span id="brandName">ScreenTinker</span>
|
|
</div>
|
|
<!-- #38: apply cached white-label before first paint (no ScreenTinker flash) -->
|
|
<script src="/js/brand-prime.js"></script>
|
|
<div class="workspace-switcher" id="workspaceSwitcher"></div>
|
|
</div>
|
|
<ul class="nav-links">
|
|
<li><a href="#/" class="nav-link active" data-view="dashboard">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
|
|
<rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
|
|
</svg>
|
|
<span>Displays</span>
|
|
</a></li>
|
|
<li><a href="#/content" class="nav-link" data-view="content">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
|
|
<polyline points="13 2 13 9 20 9"/>
|
|
</svg>
|
|
<span>Content</span>
|
|
</a></li>
|
|
<li><a href="#/playlists" class="nav-link" data-view="playlists">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/>
|
|
<line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>
|
|
</svg>
|
|
<span>Playlists</span>
|
|
</a></li>
|
|
<li><a href="#/layouts" class="nav-link" data-view="layouts">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/>
|
|
</svg>
|
|
<span>Layouts</span>
|
|
</a></li>
|
|
<li><a href="#/widgets" class="nav-link" data-view="widgets">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="2" width="8" height="8" rx="1"/><rect x="14" y="2" width="8" height="8" rx="1"/>
|
|
<rect x="2" y="14" width="8" height="8" rx="1"/><rect x="14" y="14" width="8" height="8" rx="1"/>
|
|
</svg>
|
|
<span>Widgets</span>
|
|
</a></li>
|
|
<li><a href="#/schedule" class="nav-link" data-view="schedule">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
|
|
</svg>
|
|
<span>Schedule</span>
|
|
</a></li>
|
|
<li><a href="#/walls" class="nav-link" data-view="walls">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="3" width="9" height="8" rx="1"/><rect x="13" y="3" width="9" height="8" rx="1"/>
|
|
<rect x="2" y="13" width="9" height="8" rx="1"/><rect x="13" y="13" width="9" height="8" rx="1"/>
|
|
</svg>
|
|
<span>Video Walls</span>
|
|
</a></li>
|
|
<li><a href="#/reports" class="nav-link" data-view="reports">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>
|
|
</svg>
|
|
<span>Reports</span>
|
|
</a></li>
|
|
<li><a href="#/kiosk" class="nav-link" data-view="kiosk">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><path d="M8 21h8"/><path d="M12 17v4"/>
|
|
<circle cx="12" cy="10" r="1"/>
|
|
</svg>
|
|
<span>Kiosk</span>
|
|
</a></li>
|
|
<li><a href="#/designer" class="nav-link" data-view="designer">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 19l7-7 3 3-7 7-3-3z"/><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/><path d="M2 2l7.586 7.586"/>
|
|
<circle cx="11" cy="11" r="2"/>
|
|
</svg>
|
|
<span>Designer</span>
|
|
</a></li>
|
|
<li><a href="#/activity" class="nav-link" data-view="activity">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
|
</svg>
|
|
<span>Activity</span>
|
|
</a></li>
|
|
<!-- Teams nav hidden while the feature is being redesigned as a user-grouping
|
|
primitive within Workspaces. Route + view kept in place so any existing
|
|
bookmark still loads (and shows the 503 from the API). -->
|
|
<li style="display:none"><a href="#/teams" class="nav-link" data-view="teams">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/>
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
</svg>
|
|
<span>Teams</span>
|
|
</a></li>
|
|
<!-- Members: inviting a colleague is a core action, and the only route to it used to be
|
|
an unlabelled icon beside the workspace name. #/members resolves to the ACTIVE
|
|
workspace so this link can stay static. -->
|
|
<li><a href="#/members" class="nav-link" data-view="members">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/>
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
</svg>
|
|
<span>Members</span>
|
|
</a></li>
|
|
<li><a href="#/help" class="nav-link" data-view="help">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
|
</svg>
|
|
<span>Help</span>
|
|
</a></li>
|
|
<li><a href="#/settings" class="nav-link" data-view="settings">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="3"/>
|
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
</svg>
|
|
<span>Settings</span>
|
|
</a></li>
|
|
<li id="billingNavItem"><a href="#/billing" class="nav-link" data-view="billing">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/>
|
|
<line x1="1" y1="10" x2="23" y2="10"/>
|
|
</svg>
|
|
<span>Subscription</span>
|
|
</a></li>
|
|
<li id="adminNavItem" style="display:none"><a href="#/admin" class="nav-link" data-view="admin">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
</svg>
|
|
<span>Admin</span>
|
|
</a></li>
|
|
</ul>
|
|
<div class="sidebar-footer">
|
|
<div class="connection-status" id="connectionStatus">
|
|
<span class="status-dot offline"></span>
|
|
<span>Disconnected</span>
|
|
</div>
|
|
<div class="version-status" id="versionStatus">
|
|
<span id="versionLabel"></span>
|
|
<span class="version-badge" id="versionBadge" hidden>Update</span>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main class="content" id="app">
|
|
<!-- Views rendered here -->
|
|
</main>
|
|
|
|
<!-- Add Device Modal -->
|
|
<div class="modal-overlay" id="addDeviceModal" style="display:none">
|
|
<div class="modal" style="max-width:560px">
|
|
<div class="modal-header">
|
|
<h3 data-i18n="add_display.title">Add Display</h3>
|
|
<button class="btn-icon" data-close-modal="addDeviceModal">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="modal-description" style="margin-bottom:16px" data-i18n="add_display.intro">Enter the 6-digit pairing code shown on the display.</p>
|
|
<div class="form-group">
|
|
<label data-i18n="add_display.pairing_code">Pairing Code</label>
|
|
<input type="text" id="pairingCodeInput" maxlength="6" pattern="[0-9]{6}" placeholder="000000" class="pairing-input">
|
|
</div>
|
|
<div class="form-group">
|
|
<label data-i18n="add_display.display_name">Display Name (optional)</label>
|
|
<input type="text" id="deviceNameInput" data-i18n-placeholder="add_display.name_placeholder" placeholder="e.g., Lobby TV" class="input">
|
|
</div>
|
|
<div style="background:var(--bg-input,#0f172a);border-radius:6px;padding:10px 12px;margin-top:16px;border:1px solid var(--border,#1e293b)">
|
|
<span style="font-size:11px;color:var(--text-muted,#64748b);font-weight:500" data-i18n="add_display.server_url_label">Server URL</span>
|
|
<code id="addDeviceServerUrl" style="display:block;font-size:13px;margin:2px 0;color:var(--primary,#3b82f6);word-break:break-all"></code>
|
|
<span style="font-size:11px;color:var(--text-muted,#64748b)" data-i18n="add_display.server_url_hint">Use this URL when configuring the Android app</span>
|
|
</div>
|
|
<div style="border-top:1px solid var(--border,#1e293b);margin-top:20px;padding-top:16px">
|
|
<p style="font-size:12px;color:var(--text-muted,#64748b);margin-bottom:10px;font-weight:500" data-i18n="add_display.need_player">Need a player app? Install one to get a pairing code:</p>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
|
|
<a href="/download/apk" class="btn btn-secondary btn-sm" style="text-decoration:none;justify-content:center;font-size:12px">
|
|
🤖 <span data-i18n="add_display.android_apk">Android APK</span>
|
|
</a>
|
|
<a href="/player" target="_blank" class="btn btn-secondary btn-sm" style="text-decoration:none;justify-content:center;font-size:12px">
|
|
🌐 <span data-i18n="add_display.web_player">Web Player</span>
|
|
</a>
|
|
<a href="/scripts/raspberry-pi-setup.sh" class="btn btn-secondary btn-sm" style="text-decoration:none;justify-content:center;font-size:12px">
|
|
🥏 <span data-i18n="add_display.raspberry_pi">Raspberry Pi</span>
|
|
</a>
|
|
<a href="/scripts/windows-setup.bat" class="btn btn-secondary btn-sm" style="text-decoration:none;justify-content:center;font-size:12px">
|
|
💻 <span data-i18n="add_display.windows">Windows</span>
|
|
</a>
|
|
</div>
|
|
<p style="font-size:11px;color:var(--text-muted,#64748b);margin-top:8px;line-height:1.5"><span data-i18n="add_display.smart_tv_prefix">Smart TVs (LG/Samsung): open the built-in browser and navigate to</span> <code id="addDeviceSmartTvUrl" style="background:var(--bg-input,#0f172a);padding:1px 4px;border-radius:3px;white-space:nowrap">/player</code></p>
|
|
</div>
|
|
<div style="border-top:1px solid var(--border,#1e293b);margin-top:16px;padding-top:16px">
|
|
<p style="font-size:12px;color:var(--text-muted,#64748b);margin-bottom:10px;font-weight:500" data-i18n="add_display.owner_prompt">Provisioning a fresh / factory-reset Android panel? Enroll it as device owner for silent updates, reboot & kiosk:</p>
|
|
<button type="button" class="btn btn-secondary btn-sm" id="deviceOwnerQrBtn" style="width:100%;justify-content:center;font-size:12px">
|
|
📲 <span data-i18n="add_display.owner_qr_btn">Show device-owner QR</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" data-close-modal="addDeviceModal" data-i18n="common.cancel">Cancel</button>
|
|
<button class="btn btn-primary" id="pairDeviceBtn" data-i18n="add_display.pair_btn">Pair Display</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast container -->
|
|
<div id="toastContainer" class="toast-container"></div>
|
|
|
|
<script type="module" src="/js/app.js"></script>
|
|
</body>
|
|
</html>
|