landingJs constant
String
const landingJs
Landing page JavaScript
Implementation
static const String landingJs = r'''// Store existing atSigns for validation
let existingAtSigns = [];
let allAtSignsData = [];
let savedApiConfig = { registrarApiKey: '', atsignPrefix: '', atsignPostfix: '' };
let wizardRefreshInterval = null;
let wizardLoadInFlight = false;
const openWizardReportIds = new Set();
const openWizardReportSectionIds = new Set();
function switchLandingTab(tabId) {
const tabs = ['api-v4-config', 'api-v4-console', 'api-v4-activation', 'api-v4-transactions', 'activation', 'wizard', 'apkam-management'];
const isApiV4Tab = tabId.startsWith('api-v4');
const apiV4Trigger = document.getElementById('tab-btn-api-v4');
tabs.forEach((tab) => {
const button = document.getElementById(`tab-btn-${tab}`);
const panel = document.getElementById(`tab-${tab}`);
const isActive = tab === tabId;
if (button) {
button.classList.toggle('active', isActive);
button.setAttribute('aria-selected', isActive ? 'true' : 'false');
}
if (panel) {
panel.classList.toggle('active', isActive);
panel.hidden = !isActive;
}
});
if (apiV4Trigger) {
apiV4Trigger.classList.toggle('active', isApiV4Tab);
apiV4Trigger.setAttribute('aria-expanded', 'false');
}
closeLandingApiV4Menu();
if (tabId === 'api-v4-transactions' && window.loadApiV4Transactions) {
window.loadApiV4Transactions();
}
if (tabId === 'api-v4-activation' && window.loadActivationCramKeys) {
window.loadActivationCramKeys();
}
if (tabId === 'wizard') {
loadWizardReports();
if (!wizardRefreshInterval) {
wizardRefreshInterval = setInterval(loadWizardReports, 1000);
}
} else if (wizardRefreshInterval) {
clearInterval(wizardRefreshInterval);
wizardRefreshInterval = null;
}
}
function goToBasicActivation() {
switchLandingTab('activation');
}
function toggleLandingApiV4Menu(event) {
event.preventDefault();
event.stopPropagation();
const menu = document.getElementById('tab-menu-api-v4');
const trigger = document.getElementById('tab-btn-api-v4');
if (!menu || !trigger) return;
const shouldOpen = menu.hidden;
menu.hidden = !shouldOpen;
trigger.setAttribute('aria-expanded', shouldOpen ? 'true' : 'false');
}
function closeLandingApiV4Menu() {
const menu = document.getElementById('tab-menu-api-v4');
const trigger = document.getElementById('tab-btn-api-v4');
if (menu) {
menu.hidden = true;
}
if (trigger) {
trigger.setAttribute('aria-expanded', 'false');
}
}
document.addEventListener('click', function (event) {
const menu = document.getElementById('tab-menu-api-v4');
const trigger = document.getElementById('tab-btn-api-v4');
if (!menu || !trigger) return;
if (!menu.hidden && !menu.contains(event.target) && !trigger.contains(event.target)) {
closeLandingApiV4Menu();
}
});
// Load available atSigns on page load
async function loadAtSigns() {
try {
const [atSignsResponse, configResponse] = await Promise.all([
fetch('/api/atsigns'),
fetch('/api/config')
]);
const data = await atSignsResponse.json();
let configData = {};
try {
configData = await configResponse.json();
} catch (_) {
configData = {};
}
document.getElementById('loading').style.display = 'none';
savedApiConfig = {
registrarApiKey: configData.registrarApiKey || '',
atsignPrefix: configData.atsignPrefix || '',
atsignPostfix: configData.atsignPostfix || ''
};
if (data.atsigns && data.atsigns.length > 0) {
existingAtSigns = data.atsigns.map(a => a.name.toLowerCase());
displayAtSigns(data.atsigns);
filterAndSortAtSigns();
} else {
existingAtSigns = [];
displayEmptyState();
}
} catch (error) {
console.error('Error loading atSigns:', error);
document.getElementById('loading').innerHTML = `
<p class="loading-error">Error loading atSigns: ${escapeHtml(error.message)}</p>
<button class="loading-retry-btn" onclick="loadAtSigns()">Retry</button>
`;
}
}
function displayAtSigns(atsigns) {
allAtSignsData = atsigns;
const toolbar = document.getElementById('atsign-toolbar');
if (toolbar) toolbar.style.display = 'flex';
renderAtSignList(atsigns);
}
function filterAndSortAtSigns() {
const searchTerm = (document.getElementById('atsign-search-input')?.value || '').toLowerCase().trim();
const sortMode = document.getElementById('atsign-sort-select')?.value || 'default';
const matchCount = document.getElementById('atsign-match-count');
let filtered = allAtSignsData;
if (searchTerm) {
filtered = filtered.filter(function (atsign) {
return String(atsign.name || '').toLowerCase().includes(searchTerm);
});
}
if (sortMode === 'az') {
filtered = filtered.slice().sort(function (a, b) {
return String(a.name || '').localeCompare(String(b.name || ''));
});
} else if (sortMode === 'za') {
filtered = filtered.slice().sort(function (a, b) {
return String(b.name || '').localeCompare(String(a.name || ''));
});
}
if (matchCount) {
if (searchTerm) {
matchCount.textContent = `${filtered.length} of ${allAtSignsData.length}`;
} else {
matchCount.textContent = `${allAtSignsData.length} atsigns`;
}
}
renderAtSignList(filtered);
}
function renderAtSignList(atsigns) {
const container = document.getElementById('atsign-list-container');
const list = document.getElementById('atsign-list');
const configuredPostfix = String(savedApiConfig.atsignPostfix || '').trim();
const postfixMatches = configuredPostfix
? atsigns.filter((atsign) => String(atsign.name || '').endsWith(configuredPostfix))
: [];
const otherAtSigns = postfixMatches.length > 0
? atsigns.filter((atsign) => !String(atsign.name || '').endsWith(configuredPostfix))
: atsigns;
const renderAtSignRows = function (items) {
return items.map((atsign) => `
<div class="atsign-item">
<div>
<div class="atsign-name-row">
<div class="atsign-name">${escapeHtml(atsign.name)}</div>
${Number(atsign.activeAutoApprovalCount || 0) > 0 ? `<div class="atsign-auto-badge">${escapeHtml(String(atsign.activeAutoApprovalCount))} Auto</div>` : ''}
</div>
<div class="atsign-path">${escapeHtml(atsign.path)}</div>
</div>
<button class="select-btn" onclick="selectAtSign('${escapeJsString(atsign.name)}')">
Select
</button>
</div>
`).join('');
};
const sections = [];
if (atsigns.length === 0) {
list.innerHTML = '<div class="empty-state"><p>No atsigns matching your search.</p></div>';
container.style.display = 'block';
return;
}
if (postfixMatches.length > 0) {
sections.push(`
<div class="atsign-group">
<h3>${escapeHtml(configuredPostfix)} Atsigns</h3>
<p>All Atsigns matching the configured postfix.</p>
<div class="atsign-group-list">
${renderAtSignRows(postfixMatches)}
</div>
</div>
`);
}
if (otherAtSigns.length > 0) {
sections.push(`
<div class="atsign-group">
<h3>${postfixMatches.length > 0 ? 'Other' : 'Available Atsigns'}</h3>
<p>${postfixMatches.length > 0 ? 'All remaining Atsigns that do not match the configured postfix.' : 'All available Atsigns found in ~/.atsign/keys/.'}</p>
<div class="atsign-group-list">
${renderAtSignRows(otherAtSigns)}
</div>
</div>
`);
}
list.innerHTML = sections.join('');
container.style.display = 'block';
}
function displayEmptyState() {
const container = document.getElementById('atsign-list-container');
const list = document.getElementById('atsign-list');
list.innerHTML = `
<div class="empty-state">
<h3>No atSigns Found</h3>
<p>No onboarded atSigns were found in ~/.atsign/keys/</p>
<p>Onboard one from the Activation tab to get started.</p>
<button class="onboard-btn" onclick="goToBasicActivation()">Go to Activation</button>
</div>
`;
container.style.display = 'block';
}
async function selectAtSign(atsign) {
try {
const response = await fetch('/api/select-atsign', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ atsign })
});
if (response.ok) {
// Redirect to main app
window.location.href = '/';
} else {
const error = await response.json();
const errorMessage = error.error || 'Unknown error';
showErrorNotification(errorMessage, atsign);
}
} catch (error) {
console.error('Error selecting atSign:', error);
showErrorNotification('Network error: ' + error.message, atsign);
}
}
// Show error notification
function showErrorNotification(message, atsign) {
let overlay = document.getElementById('error-modal-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'error-modal-overlay';
overlay.className = 'modal-overlay';
document.body.appendChild(overlay);
}
overlay.innerHTML = `
<div class="modal-card">
<h2 class="modal-title modal-title-error">Connection Failed</h2>
<div class="modal-atsign"><strong>${escapeHtml(atsign)}</strong></div>
<p class="modal-message">${escapeHtml(message)}</p>
<div class="modal-actions">
<button class="modal-btn" onclick="closeErrorNotification()">OK</button>
</div>
</div>
`;
overlay.classList.add('modal-visible');
}
function closeErrorNotification() {
const overlay = document.getElementById('error-modal-overlay');
if (overlay) {
overlay.classList.remove('modal-visible');
}
}
function formatWizardTimestamp(value) {
try {
return new Date(value).toLocaleString();
} catch (_) {
return value;
}
}
function toggleWizardCreateForm() {
const form = document.getElementById('wizard-create-form');
if (!form) return;
form.hidden = !form.hidden;
}
function renderWizardReports(reports) {
const container = document.getElementById('wizard-report-list');
if (!container) return;
if (!reports || reports.length === 0) {
container.innerHTML = '<div class="api-v4-transaction-card"><div class="api-v4-transaction-meta">No wizard reports yet.</div></div>';
return;
}
container.innerHTML = reports.map((report) => {
const inputs = report.inputs || {};
const rows = Array.isArray(report.rows) ? report.rows : [];
const logs = Array.isArray(report.logs) ? report.logs : [];
const stepStatus = report.stepStatus || {};
const wizardSteps = [
{ key: 'registerBatch', label: 'Register Batch' },
{ key: 'activate', label: 'Activate' },
{ key: 'generateOtp', label: 'Generate OTP' },
{ key: 'createAutoApproval', label: 'Create Auto Approval' }
];
return `
<details class="api-v4-transaction-card" data-wizard-report-id="${escapeHtml(report.id || '')}" ${openWizardReportIds.has(String(report.id || '')) ? 'open' : ''}>
<summary class="api-v4-transaction-summary">
<div class="api-v4-transaction-title-wrap">
<div class="api-v4-transaction-title">${escapeHtml(report.id || 'Wizard Report')}</div>
<div class="api-v4-transaction-subtitle">${escapeHtml((report.status || 'draft').replaceAll('_', ' '))}</div>
</div>
<div class="api-v4-transaction-summary-right">
<button type="button" class="api-v4-secondary" data-run-wizard-report="${escapeHtml(report.id || '')}" ${report.status === 'running' ? 'disabled' : ''}>Run</button>
<span class="api-v4-transaction-created">${escapeHtml(formatWizardTimestamp(report.createdAtUtc || ''))}</span>
</div>
</summary>
<div class="api-v4-transaction-body wizard-report-body">
<div class="api-v4-transaction-meta"><strong>Requested Atsigns:</strong> ${escapeHtml((inputs.requestedAtSigns || []).join(', '))}</div>
<div class="api-v4-transaction-meta"><strong>OTP Expiry:</strong> ${escapeHtml(inputs.otpExpiry || '')}</div>
<div class="api-v4-transaction-meta"><strong>Device Name Regex:</strong> ${escapeHtml(inputs.deviceName || '')}</div>
<div class="api-v4-transaction-meta"><strong>App Name Regex:</strong> ${escapeHtml(inputs.appName || '')}</div>
<div class="api-v4-transaction-meta"><strong>data.json:</strong> ${escapeHtml(report.dataPath || '')}</div>
<div class="api-v4-transaction-meta"><strong>report.csv:</strong> ${escapeHtml(report.csvPath || '')}</div>
<div class="api-v4-transaction-meta"><strong>Current Step:</strong> ${escapeHtml(formatWizardCurrentStep(report.currentStep || report.status || 'pending'))}</div>
<div class="wizard-progress-bar" aria-label="Wizard progress">
${wizardSteps.map((step, index) => `
<div class="wizard-progress-segment wizard-progress-${escapeHtml(getWizardStepTone(report, step.key))}">
<span class="wizard-progress-index">${index + 1}</span>
<span class="wizard-progress-label-wrap">
<span class="wizard-progress-label">${escapeHtml(step.label)}</span>
<span class="wizard-progress-state">${escapeHtml(formatWizardStepStatus(report, step.key))}</span>
</span>
</div>
`).join('')}
</div>
<div class="api-v4-action-row">
<button type="button" class="api-v4-secondary" data-open-wizard-report-file="${escapeHtml(report.id || '')}" data-wizard-file-type="csv">Open report.csv</button>
<button type="button" class="api-v4-secondary" data-open-wizard-report-file="${escapeHtml(report.id || '')}" data-wizard-file-type="folder">Open Folder</button>
</div>
<details class="api-v4-transaction-raw" data-wizard-report-section-id="${escapeHtml(getWizardReportSectionId(report.id || '', 'csv-preview'))}" ${isWizardReportSectionOpen(report.id || '', 'csv-preview', true) ? 'open' : ''}>
<summary>report.csv Preview</summary>
<div class="wizard-report-table-wrap">${renderWizardCsvPreview(rows, inputs)}</div>
</details>
<div class="wizard-step-grid">
<div class="wizard-step-card"><strong>Register Batch</strong><span>${escapeHtml(stepStatus.registerBatch || 'pending')}</span></div>
<div class="wizard-step-card"><strong>Activate</strong><span>${escapeHtml(stepStatus.activate || 'pending')}</span></div>
<div class="wizard-step-card"><strong>Generate OTP</strong><span>${escapeHtml(stepStatus.generateOtp || 'pending')}</span></div>
<div class="wizard-step-card"><strong>Create Auto Approval</strong><span>${escapeHtml(stepStatus.createAutoApproval || 'pending')}</span></div>
</div>
<details class="api-v4-transaction-raw" data-wizard-report-section-id="${escapeHtml(getWizardReportSectionId(report.id || '', 'rows'))}" ${isWizardReportSectionOpen(report.id || '', 'rows') ? 'open' : ''}>
<summary>Rows</summary>
<pre class="api-v4-transaction-json">${escapeHtml(JSON.stringify(rows, null, 2))}</pre>
</details>
<details class="api-v4-transaction-raw" data-wizard-report-section-id="${escapeHtml(getWizardReportSectionId(report.id || '', 'logs'))}" ${isWizardReportSectionOpen(report.id || '', 'logs') ? 'open' : ''}>
<summary>Logs</summary>
<div class="wizard-log-list">${renderWizardLogs(logs)}</div>
</details>
</div>
</details>
`;
}).join('');
container.querySelectorAll('[data-run-wizard-report]').forEach((button) => {
button.addEventListener('click', async function (event) {
event.preventDefault();
event.stopPropagation();
const reportId = button.getAttribute('data-run-wizard-report') || '';
await runWizardReport(reportId);
});
});
container.querySelectorAll('[data-open-wizard-report-file]').forEach((button) => {
button.addEventListener('click', async function (event) {
event.preventDefault();
event.stopPropagation();
const reportId = button.getAttribute('data-open-wizard-report-file') || '';
const fileType = button.getAttribute('data-wizard-file-type') || '';
await openWizardReportFile(reportId, fileType);
});
});
container.querySelectorAll('[data-wizard-report-id]').forEach((details) => {
details.addEventListener('toggle', function () {
const reportId = details.getAttribute('data-wizard-report-id') || '';
if (!reportId) return;
if (details.open) {
openWizardReportIds.add(reportId);
} else {
openWizardReportIds.delete(reportId);
}
});
});
container.querySelectorAll('[data-wizard-report-section-id]').forEach((details) => {
details.addEventListener('toggle', function () {
const sectionId = details.getAttribute('data-wizard-report-section-id') || '';
if (!sectionId) return;
if (details.open) {
openWizardReportSectionIds.add(sectionId);
} else {
openWizardReportSectionIds.delete(sectionId);
}
});
});
}
async function loadWizardReports() {
const status = document.getElementById('wizard-status');
const container = document.getElementById('wizard-report-list');
if (!status || !container) return;
if (wizardLoadInFlight) return;
try {
wizardLoadInFlight = true;
const response = await fetch('/api/wizard/reports');
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to load wizard reports');
}
const validIds = new Set((data.reports || []).map((report) => String(report.id || '')));
Array.from(openWizardReportIds).forEach((reportId) => {
if (!validIds.has(reportId)) {
openWizardReportIds.delete(reportId);
}
});
renderWizardReports(data.reports || []);
const reports = data.reports || [];
const runningCount = reports.filter((report) => String(report.status || '').toLowerCase() === 'running').length;
status.textContent = runningCount > 0
? `Live updates: ${runningCount} running, ${reports.length} total reports`
: `Loaded ${reports.length} reports`;
} catch (error) {
status.textContent = `Load failed: ${error.message}`;
container.innerHTML = '<div class="api-v4-transaction-card"><div class="api-v4-transaction-meta">Failed to load wizard reports.</div></div>';
} finally {
wizardLoadInFlight = false;
}
}
async function createWizardReport() {
const status = document.getElementById('wizard-status');
try {
const requestedAtSigns = (document.getElementById('wizard-atsigns')?.value || '')
.split('\n')
.map((item) => item.trim())
.filter(Boolean);
const otpExpiry = (document.getElementById('wizard-otp-expiry')?.value || '7d').trim() || '7d';
const deviceName = (document.getElementById('wizard-device-name')?.value || '').trim();
const appName = (document.getElementById('wizard-app-name')?.value || '').trim();
if (requestedAtSigns.length === 0 || !deviceName || !appName) {
throw new Error('Enter Atsigns, OTP expiry, device name, and app name');
}
if (status) status.textContent = 'Saving report...';
const response = await fetch('/api/wizard/reports', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
requestedAtSigns,
otpExpiry,
deviceName,
appName
})
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to create wizard report');
}
const form = document.getElementById('wizard-create-form');
if (form) form.hidden = true;
await loadWizardReports();
if (status) status.textContent = `Saved report ${data.report && data.report.id ? data.report.id : ''}`;
} catch (error) {
if (status) status.textContent = `Save failed: ${error.message}`;
}
}
async function runWizardReport(reportId) {
const status = document.getElementById('wizard-status');
try {
openWizardReportIds.add(reportId);
if (status) status.textContent = `Starting report ${reportId}...`;
const response = await fetch(`/api/wizard/reports/${encodeURIComponent(reportId)}/run`, {
method: 'POST'
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to run wizard report');
}
await loadWizardReports();
if (status) status.textContent = `Started report ${reportId}`;
} catch (error) {
if (status) status.textContent = `Run failed: ${error.message}`;
}
}
function getWizardStepTone(report, stepKey) {
const stepStatus = (report && report.stepStatus) ? report.stepStatus : {};
const normalized = String(stepStatus[stepKey] || '').trim().toLowerCase();
const currentStep = String((report && report.currentStep) || '').trim();
const reportStatus = String((report && report.status) || '').trim().toLowerCase();
if (normalized === 'success' || normalized === 'completed') return 'completed';
if (normalized === 'partial') return 'warning';
if (normalized === 'error' || normalized === 'failed') return 'error';
if (reportStatus === 'running' && currentStep === stepKey) return 'running';
if (normalized === 'running' || normalized === 'in_progress' || normalized === 'started') return 'running';
return 'pending';
}
function formatWizardStepStatus(report, stepKey) {
const stepStatus = (report && report.stepStatus) ? report.stepStatus : {};
const value = String(stepStatus[stepKey] || '').trim();
if (value) {
return value.replaceAll('_', ' ');
}
if (String((report && report.status) || '').toLowerCase() === 'running' && String((report && report.currentStep) || '') === stepKey) {
return 'running';
}
return 'pending';
}
function formatWizardCurrentStep(value) {
const normalized = String(value || '').trim();
if (!normalized) return 'pending';
if (normalized === 'registerBatch') return 'Register Batch';
if (normalized === 'activate') return 'Activate';
if (normalized === 'generateOtp') return 'Generate OTP';
if (normalized === 'createAutoApproval') return 'Create Auto Approval';
if (normalized === 'complete') return 'Complete';
return normalized.replaceAll('_', ' ');
}
function renderWizardLogs(logs) {
if (!Array.isArray(logs) || logs.length === 0) {
return '<div class="wizard-log-entry"><span class="wizard-log-message">No logs yet.</span></div>';
}
return logs.map((log) => {
const timestamp = formatWizardTimestamp(log && log.timestampUtc ? log.timestampUtc : '');
const message = log && log.message ? log.message : '';
return `
<div class="wizard-log-entry">
<span class="wizard-log-timestamp">${escapeHtml(timestamp)}</span>
<span class="wizard-log-message">${escapeHtml(message)}</span>
</div>
`;
}).join('');
}
function renderWizardCsvPreview(rows, inputs) {
if (!Array.isArray(rows) || rows.length === 0) {
return '<div class="api-v4-transaction-meta">No report rows yet.</div>';
}
return `
<table class="wizard-report-table">
<thead>
<tr>
<th>Atsign</th>
<th>OTP</th>
<th>OTP Expiry Timestamp</th>
<th>Device Name Regex</th>
<th>App Name Regex</th>
<th>Manager Key File Location</th>
</tr>
</thead>
<tbody>
${rows.map((row) => `
<tr>
<td>${escapeHtml(row && row.atSign ? row.atSign : '')}</td>
<td>${escapeHtml(row && row.otp ? row.otp : '')}</td>
<td>${escapeHtml(row && row.otpExpiresAtUtc ? row.otpExpiresAtUtc : '')}</td>
<td>${escapeHtml(inputs && inputs.deviceName ? inputs.deviceName : '')}</td>
<td>${escapeHtml(inputs && inputs.appName ? inputs.appName : '')}</td>
<td>${escapeHtml(row && row.managerKeyFileLocation ? row.managerKeyFileLocation : '')}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
function getWizardReportSectionId(reportId, sectionName) {
return `${String(reportId || '')}::${String(sectionName || '')}`;
}
function isWizardReportSectionOpen(reportId, sectionName, defaultOpen = false) {
const sectionId = getWizardReportSectionId(reportId, sectionName);
return openWizardReportSectionIds.has(sectionId) || defaultOpen;
}
async function openWizardReportFile(reportId, fileType) {
const status = document.getElementById('wizard-status');
try {
if (status) {
status.textContent = fileType === 'folder'
? `Opening folder for ${reportId}...`
: `Opening report.csv for ${reportId}...`;
}
const response = await fetch(`/api/wizard/reports/${encodeURIComponent(reportId)}/open-file`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ fileType })
});
const data = await response.json();
if (!response.ok || data.error) {
throw new Error(data.error || 'Failed to open wizard report file');
}
if (status) {
status.textContent = fileType === 'folder'
? `Opened folder for ${reportId}`
: `Opened report.csv for ${reportId}`;
}
} catch (error) {
if (status) status.textContent = `Open failed: ${error.message}`;
}
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
function escapeJsString(value) {
return String(value)
.replaceAll('\\', '\\\\')
.replaceAll("'", "\\'");
}
// Exit server with confirmation
async function exitServer() {
const confirmed = confirm('Are you sure you want to shutdown the server?\n\nThis will:\n• Stop the web server\n• Close this browser tab\n• Exit the at_activate_web application');
if (!confirmed) {
return;
}
try {
// Send shutdown request to server
const response = await fetch('/api/shutdown', {
method: 'POST'
});
if (response.ok) {
// Show shutdown message
document.body.innerHTML = '<div class="shutdown-screen"><div class="shutdown-title">Server shutting down...</div><div class="shutdown-subtitle">You can close this tab</div></div>';
// Wait a moment then close the tab
setTimeout(() => {
window.close();
}, 1500);
} else {
alert('Failed to shutdown server. You may need to stop it manually (Ctrl+C in terminal).');
}
} catch (error) {
console.error('Error shutting down server:', error);
alert('Error shutting down server: ' + error.message);
}
}
// Onboarding Functions
let pollingInterval = null;
let pollingStartTime = null;
function generateCommand() {
const atsignInput = document.getElementById('atsign-input').value.trim();
const licenseKey = document.getElementById('license-key-input').value.trim();
if (!atsignInput || !licenseKey) {
document.getElementById('command-section').style.display = 'none';
return;
}
// Normalize atSign
const atsign = atsignInput.startsWith('@') ? atsignInput : '@' + atsignInput;
// Check if this atSign already exists
if (existingAtSigns.includes(atsign.toLowerCase())) {
// Show error message instead of command
document.getElementById('command-section').style.display = 'block';
document.getElementById('command-box').innerHTML = '<span class="command-box-error">This atSign is already onboarded. Select it from the Select atSign tab or use a different atSign.</span>';
document.getElementById('copy-btn').style.display = 'none';
// Stop any existing polling
if (pollingInterval) {
clearInterval(pollingInterval);
pollingInterval = null;
}
document.getElementById('status-section').classList.remove('active');
return;
}
// Show copy button if it was hidden
document.getElementById('copy-btn').style.display = 'block';
// Generate the onboarding command
const command = `at_activate onboard -a ${atsign} -c ${licenseKey}`;
// Display the command
document.getElementById('command-box').textContent = command;
document.getElementById('command-section').style.display = 'block';
// Start polling if not already started
if (!pollingInterval) {
startPolling(atsign);
}
}
function copyCommand() {
const commandText = document.getElementById('command-box').textContent;
// Copy to clipboard
navigator.clipboard.writeText(commandText).then(() => {
// Show visual feedback
const btn = document.getElementById('copy-btn');
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = 'Copy Command';
btn.classList.remove('copied');
}, 2000);
}).catch(err => {
console.error('Failed to copy:', err);
alert('Failed to copy to clipboard');
});
}
function startPolling(expectedAtSign) {
// Show status section
document.getElementById('status-section').classList.add('active');
pollingStartTime = Date.now();
// Poll every 2 seconds
pollingInterval = setInterval(async () => {
try {
const response = await fetch('/api/check-onboarding');
const data = await response.json();
if (data.newAtsigns && data.newAtsigns.length > 0) {
// Check if our expected atSign is in the list
const found = data.newAtsigns.find(a =>
a.name.toLowerCase() === expectedAtSign.toLowerCase()
);
if (found) {
// Success! Onboarding detected
clearInterval(pollingInterval);
pollingInterval = null;
// Update status
const statusSection = document.getElementById('status-section');
statusSection.innerHTML = `
<div class="status-text status-text-success">Onboarding complete!</div>
<small class="status-text-muted">Redirecting to ${escapeHtml(expectedAtSign)}...</small>
`;
// Auto-select the newly onboarded atSign
setTimeout(() => {
selectAtSign(expectedAtSign);
}, 1500);
}
}
// Optional: Stop polling after 5 minutes
if (Date.now() - pollingStartTime > 5 * 60 * 1000) {
clearInterval(pollingInterval);
pollingInterval = null;
const statusSection = document.getElementById('status-section');
statusSection.innerHTML = `
<div class="status-text status-text-error">Polling timeout</div>
<small class="status-text-muted">Please refresh the page after onboarding.</small>
`;
}
} catch (error) {
console.error('Polling error:', error);
}
}, 2000); // Poll every 2 seconds
}
// Load atSigns when page loads
loadAtSigns();
''';