Files
UNITRONIX d1e31c0c26 feat(generator): allow custom download portal product label
Operators can override or hide download.product_* on the public
download page via bundle branding without rebuilding installers.

Refs #386

Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow)
2026-08-29 21:58:03 +02:00

176 lines
9.5 KiB
HTML

<%
// Public download portal — branded per bundle. No auth, no shared layout.
// Bundle ID itself is the access token.
const brand = bundle.branding || {};
const gb = typeof globalBranding !== 'undefined' && globalBranding ? globalBranding : {};
const primary = brand.primary_color || '#2563eb';
const accent = brand.accent_color || '#1e293b';
const companyName = brand.company_name || gb.appName || 'BetterDesk Support';
const shortText = brand.short_text || '';
const logo = brand.logo_data_url || gb.logoUrl || '';
const contactEmail = brand.contact_email || '';
const contactPhone = brand.contact_phone || '';
const contactUrl = brand.contact_url || '';
const portalBackground = gb.background || '';
const showPoweredBy = gb.showPoweredBy !== false;
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
%><!DOCTYPE html>
<html lang="<%= brand.default_lang || 'en' %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= esc(companyName) %> — <%= t('download.title') %></title>
<link rel="stylesheet" href="/css/agent-download.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%232563eb'/%3E%3Ctext x='50' y='68' font-size='60' text-anchor='middle' fill='white' font-family='Arial'%3EB%3C/text%3E%3C/svg%3E">
<style>
:root {
--brand-primary: <%= esc(primary) %>;
--brand-accent: <%= esc(accent) %>;
}
<% if (portalBackground) { %>
body {
background: <%- portalBackground %>;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
<% } %>
</style>
</head>
<body>
<main class="portal">
<header class="portal-header">
<% if (logo) { %>
<img src="<%= logo %>" alt="<%= esc(companyName) %>" class="portal-logo">
<% } else { %>
<div class="portal-logo portal-logo-fallback">
<svg viewBox="0 0 24 24" width="56" height="56" fill="currentColor" aria-hidden="true"><path d="M12 1 3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z"/></svg>
</div>
<% } %>
<h1><%= esc(companyName) %></h1>
<% if (shortText) { %>
<p class="portal-subtitle"><%= esc(shortText) %></p>
<% } %>
</header>
<section class="portal-intro">
<h2><%= t('download.choose_platform') %></h2>
<p><%= t('download.choose_platform_hint') %></p>
<% if (typeof productType !== 'undefined' && !brand.hide_product_type) { %>
<p class="portal-product-type">
<% if (brand.product_label) { %>
<%= esc(brand.product_label) %>
<% } else if (productType === 'rdclient') { %>
<%= t('download.product_rdclient') %>
<% } else if (productType === 'support-agent') { %>
<%= t('download.product_support_agent') %>
<% } else { %>
<%= t('download.product_agent_client') %>
<% } %>
</p>
<% } %>
</section>
<section class="portal-grid" id="portal-grid">
<% platforms.forEach(p => { %>
<article class="portal-card" data-platform="<%= p.platform %>" data-arch="<%= p.arch %>" data-format="<%= p.format %>" data-status="<%= p.status %>">
<div class="card-icon">
<% if (p.platform === 'windows') { %>
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"><path d="M2 5.4 9.6 4.3v7.4H2zM10.6 4.1 22 2.5v9.2H10.6zM2 12.7h7.6v7.4L2 18.9zM10.6 12.7H22v9.2l-11.4-1.6z"/></svg>
<% } else if (p.format === 'deb') { %>
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><text x="12" y="16" text-anchor="middle" font-size="9" font-weight="bold">DEB</text></svg>
<% } else if (p.format === 'rpm') { %>
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><text x="12" y="16" text-anchor="middle" font-size="9" font-weight="bold">RPM</text></svg>
<% } else { %>
<svg viewBox="0 0 24 24" width="48" height="48" fill="currentColor"><path d="M12 2 4 6v6c0 5 3.5 9.4 8 10 4.5-.6 8-5 8-10V6z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
<% } %>
</div>
<h3><%= esc(p.label) %></h3>
<p class="card-arch"><%= esc(p.arch) %></p>
<div class="card-status" data-status-slot>
<% if (p.status === 'ready') { %>
<a class="btn btn-primary" href="/api/d/<%= bundle.public_id %>/download/<%= p.platform %>/<%= p.arch %>/<%= p.format %>">
<%= t('download.download') %>
</a>
<% } else if (p.status === 'failed') { %>
<span class="status-badge status-failed"><%= t('download.failed') %></span>
<% } else { %>
<span class="status-badge status-pending">
<span class="spinner"></span>
<%= t('download.building') %>
</span>
<% } %>
</div>
</article>
<% }); %>
</section>
<% if (contactEmail || contactPhone || contactUrl) { %>
<section class="portal-contact">
<h2><%= t('download.need_help') %></h2>
<ul class="contact-list">
<% if (contactEmail) { %>
<li><span class="material-icons">mail</span><a href="mailto:<%= esc(contactEmail) %>"><%= esc(contactEmail) %></a></li>
<% } %>
<% if (contactPhone) { %>
<li><span class="material-icons">phone</span><a href="tel:<%= esc(contactPhone) %>"><%= esc(contactPhone) %></a></li>
<% } %>
<% if (contactUrl) { %>
<li><span class="material-icons">language</span><a href="<%= esc(contactUrl) %>" target="_blank" rel="noopener"><%= esc(contactUrl) %></a></li>
<% } %>
</ul>
</section>
<% } %>
<footer class="portal-footer">
<% if (showPoweredBy) { %>
<p><%= t('download.powered_by') %> <strong>BetterDesk</strong></p>
<% } %>
</footer>
</main>
<script nonce="<%= typeof cspNonce !== 'undefined' ? cspNonce : '' %>">
// Poll manifest every 8s while at least one platform is still building.
(function () {
const publicId = <%- JSON.stringify(bundle.public_id) %>;
const t = <%- JSON.stringify({
building: t('download.building'),
failed: t('download.failed'),
download: t('download.download'),
}) %>;
function render(p) {
const slot = document.querySelector(
`[data-platform="${p.platform}"][data-arch="${p.arch}"][data-format="${p.format}"] [data-status-slot]`
);
if (!slot) return;
if (p.status === 'ready') {
slot.innerHTML = `<a class="btn btn-primary" href="/api/d/${publicId}/download/${p.platform}/${p.arch}/${p.format}">${t.download}</a>`;
} else if (p.status === 'failed') {
slot.innerHTML = `<span class="status-badge status-failed">${t.failed}</span>`;
} else {
slot.innerHTML = `<span class="status-badge status-pending"><span class="spinner"></span>${t.building}</span>`;
}
}
let timer = null;
async function poll() {
try {
const r = await fetch(`/api/d/${publicId}/manifest`, { credentials: 'omit' });
if (!r.ok) return;
const j = await r.json();
if (!j || !j.success) return;
const platforms = j.data.platforms || [];
platforms.forEach(render);
const stillPending = platforms.some(p => p.status !== 'ready' && p.status !== 'failed');
if (!stillPending && timer) { clearInterval(timer); timer = null; }
} catch (_) { /* network blip — keep polling */ }
}
// Only start polling if there's something pending.
const hasPending = !!document.querySelector('.portal-card[data-status]:not([data-status="ready"]):not([data-status="failed"])');
if (hasPending) { timer = setInterval(poll, 8000); }
})();
</script>
</body>
</html>