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: '', folders: [] };
let collapsedFolders = new Set();
let wizardRefreshInterval = null;
let wizardLoadInFlight = false;
const openWizardReportIds = new Set();
const openWizardReportSectionIds = new Set();
let atSignStatuses = {};
let statusRootDomain = '';
function switchLandingTab(tabId) {
const tabs = ['api-v4-config', 'api-v4-console', 'api-v4-register-activate', 'api-v4-wizard', 'activation', 'apkam-management', 'history'];
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 === 'history' && window.loadHistory) {
window.loadHistory();
}
if (tabId === 'api-v4-register-activate' && window.initRegisterActivateTab) {
window.initRegisterActivateTab();
}
if (tabId === 'api-v4-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 || '',
folders: Array.isArray(configData.folders) ? configData.folders : []
};
statusRootDomain = data.rootDomain || '';
if (data.atsigns && data.atsigns.length > 0) {
existingAtSigns = data.atsigns.map(a => a.name.toLowerCase());
displayAtSigns(data.atsigns);
filterAndSortAtSigns();
loadAtSignStatuses();
if (data.pendingAtSign) {
selectAtSign(data.pendingAtSign);
}
} 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);
}
async function loadAtSignStatuses() {
try {
const response = await fetch('/api/atsigns/status');
const data = await response.json();
atSignStatuses = data.statuses || {};
filterAndSortAtSigns();
} catch (e) {
console.error('Error loading atSign statuses:', e);
}
}
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 getFolderForAtSign(name) {
var folders = savedApiConfig.folders || [];
for (var i = 0; i < folders.length; i++) {
if ((folders[i].atsigns || []).indexOf(name) !== -1) return folders[i].name;
}
return '';
}
function buildFolderSelect(atsignName) {
var folders = savedApiConfig.folders || [];
if (folders.length === 0) return '';
var current = getFolderForAtSign(atsignName);
var options = folders.map(function (f) {
var sel = f.name === current ? ' selected' : '';
return `<option value="${escapeHtml(f.name)}"${sel}>${escapeHtml(f.name)}</option>`;
}).join('');
return `<select class="atsign-folder-select" onchange="moveAtSignToFolder('${escapeJsString(atsignName)}', this.value)">
<option value=""${current ? '' : ' selected'}>No folder</option>${options}
</select>`;
}
function renderStatusBadge(atsignName) {
var status = atSignStatuses[atsignName];
if (!status) return '<div class="atsign-status-badge atsign-status-loading" title="Checking status…">…</div>';
if (status.status === 'online') return '<div class="atsign-status-badge atsign-status-online" title="' + escapeHtml(status.secondaryAddress) + '">Online</div>';
if (status.status === 'unreachable') return '<div class="atsign-status-badge atsign-status-unreachable" title="Registered but server not responding: ' + escapeHtml(status.secondaryAddress || '') + '">Unreachable</div>';
if (status.status === 'not_found') return '<div class="atsign-status-badge atsign-status-not-found" title="Not registered in root directory">Not Found</div>';
return '<div class="atsign-status-badge atsign-status-error" title="' + escapeHtml(status.error || 'Lookup failed') + '">Error</div>';
}
function renderAtSignRow(atsign) {
var rootInfo = statusRootDomain ? ' · <span class="atsign-root-domain">' + escapeHtml(statusRootDomain) + '</span>' : '';
return `<div class="atsign-item">
<div>
<div class="atsign-name-row">
<div class="atsign-name">${escapeHtml(atsign.name)}</div>
${renderStatusBadge(atsign.name)}
${Number(atsign.activeAutoApprovalCount || 0) > 0 ? `<div class="atsign-auto-badge">${escapeHtml(String(atsign.activeAutoApprovalCount))} Auto</div>` : ''}
</div>
<div class="atsign-path">${escapeHtml(atsign.path)}${rootInfo}</div>
</div>
<div class="atsign-item-actions">
${buildFolderSelect(atsign.name)}
<button class="select-btn" onclick="selectAtSign('${escapeJsString(atsign.name)}')">Select</button>
</div>
</div>`;
}
function renderAtSignRows(items) {
return items.map(renderAtSignRow).join('');
}
function renderGroupSection(title, subtitle, items) {
return `<div class="atsign-group">
${title ? `<h3>${escapeHtml(title)}</h3>` : ''}
${subtitle ? `<p>${escapeHtml(subtitle)}</p>` : ''}
<div class="atsign-group-list">${renderAtSignRows(items)}</div>
</div>`;
}
function renderFolderSection(folder, items) {
var isCollapsed = collapsedFolders.has(folder.name);
return `<div class="atsign-group">
<div class="folder-group-header" onclick="toggleFolderCollapse('${escapeJsString(folder.name)}')">
<span class="folder-group-toggle ${isCollapsed ? 'collapsed' : ''}">▼</span>
<h3>${escapeHtml(folder.name)}</h3>
<span class="folder-group-count">${items.length}</span>
<div class="folder-group-actions" onclick="event.stopPropagation()">
<button onclick="promptRenameFolder('${escapeJsString(folder.name)}')" title="Rename">✎</button>
<button class="folder-delete-btn" onclick="deleteFolder('${escapeJsString(folder.name)}')" title="Delete">×</button>
</div>
</div>
<div class="atsign-group-list" ${isCollapsed ? 'hidden' : ''}>${renderAtSignRows(items)}</div>
</div>`;
}
function partitionByPostfix(atsigns) {
var postfix = String(savedApiConfig.atsignPostfix || '').trim();
if (!postfix) return { postfixMatches: [], other: atsigns, postfix: '' };
var matches = atsigns.filter(function (a) { return String(a.name || '').endsWith(postfix); });
var other = matches.length > 0
? atsigns.filter(function (a) { return !String(a.name || '').endsWith(postfix); })
: atsigns;
return { postfixMatches: matches, other: other, postfix: postfix };
}
function renderAtSignList(atsigns) {
var container = document.getElementById('atsign-list-container');
var list = document.getElementById('atsign-list');
var folders = savedApiConfig.folders || [];
var sections = [];
if (atsigns.length === 0) {
list.innerHTML = '<div class="empty-state"><p>No Atsigns matching your search.</p></div>';
container.style.display = 'block';
return;
}
var atsignsByName = {};
atsigns.forEach(function (a) { atsignsByName[a.name] = a; });
if (folders.length > 0) {
var folderedNames = new Set();
folders.forEach(function (folder) {
var items = (folder.atsigns || [])
.filter(function (name) { return atsignsByName[name]; })
.map(function (name) { return atsignsByName[name]; });
if (items.length > 0) sections.push(renderFolderSection(folder, items));
(folder.atsigns || []).forEach(function (name) { folderedNames.add(name); });
});
var ungrouped = atsigns.filter(function (a) { return !folderedNames.has(a.name); });
if (ungrouped.length > 0) {
var p = partitionByPostfix(ungrouped);
if (p.postfixMatches.length > 0) {
sections.push(renderGroupSection(p.postfix + ' Atsigns', 'Ungrouped Atsigns matching the configured postfix.', p.postfixMatches));
}
if (p.other.length > 0) {
var otherTitle = p.postfixMatches.length > 0 ? 'Other' : 'Ungrouped';
var otherSubtitle = p.postfixMatches.length > 0 ? 'Remaining ungrouped Atsigns.' : '';
sections.push(renderGroupSection(otherTitle, otherSubtitle, p.other));
}
}
} else {
var p = partitionByPostfix(atsigns);
if (p.postfixMatches.length > 0) {
sections.push(renderGroupSection(p.postfix + ' Atsigns', 'All Atsigns matching the configured postfix.', p.postfixMatches));
}
if (p.other.length > 0) {
sections.push(renderGroupSection(
p.postfixMatches.length > 0 ? 'Other' : '',
p.postfixMatches.length > 0 ? 'All remaining Atsigns that do not match the configured postfix.' : '',
p.other
));
}
}
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';
}
// --- Folder management ---
async function saveFolders() {
try {
var response = await fetch('/api/config/folders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ folders: savedApiConfig.folders })
});
if (!response.ok) {
console.error('Error saving folders: server returned', response.status);
alert('Failed to save folder changes. Please try again.');
}
} catch (e) {
console.error('Error saving folders:', e);
alert('Failed to save folder changes: ' + e.message);
}
filterAndSortAtSigns();
}
function createFolder() {
var input = document.getElementById('folder-create-input');
var name = (input.value || '').trim();
if (!name) return;
var folders = savedApiConfig.folders || [];
if (folders.some(function (f) { return f.name === name; })) return;
folders.push({ name: name, atsigns: [] });
savedApiConfig.folders = folders;
input.value = '';
renderFolderDropdownList();
saveFolders();
}
function deleteFolder(name) {
if (!confirm('Delete folder "' + name + '"? Atsigns in this folder will move to Ungrouped.')) return;
savedApiConfig.folders = (savedApiConfig.folders || []).filter(function (f) { return f.name !== name; });
collapsedFolders.delete(name);
renderFolderDropdownList();
saveFolders();
}
function promptRenameFolder(oldName) {
var newName = prompt('Rename folder:', oldName);
if (!newName || newName.trim() === '' || newName.trim() === oldName) return;
newName = newName.trim();
var folders = savedApiConfig.folders || [];
if (folders.some(function (f) { return f.name === newName; })) return;
var folder = folders.find(function (f) { return f.name === oldName; });
if (!folder) return;
if (collapsedFolders.has(oldName)) {
collapsedFolders.delete(oldName);
collapsedFolders.add(newName);
}
folder.name = newName;
renderFolderDropdownList();
saveFolders();
}
function moveAtSignToFolder(atsignName, folderName) {
var folders = savedApiConfig.folders || [];
folders.forEach(function (f) {
f.atsigns = (f.atsigns || []).filter(function (n) { return n !== atsignName; });
});
if (folderName) {
var target = folders.find(function (f) { return f.name === folderName; });
if (target) target.atsigns.push(atsignName);
}
saveFolders();
}
function toggleFolderCollapse(name) {
if (collapsedFolders.has(name)) {
collapsedFolders.delete(name);
} else {
collapsedFolders.add(name);
}
filterAndSortAtSigns();
}
function toggleFolderDropdown(event) {
event.stopPropagation();
var dropdown = document.getElementById('folder-dropdown');
if (!dropdown) return;
var shouldOpen = dropdown.hidden;
dropdown.hidden = !shouldOpen;
if (shouldOpen) renderFolderDropdownList();
}
function renderFolderDropdownList() {
var listEl = document.getElementById('folder-dropdown-list');
if (!listEl) return;
var folders = savedApiConfig.folders || [];
if (folders.length === 0) {
listEl.innerHTML = '<div class="folder-dropdown-item" style="color:var(--color-text-muted);font-size:12px;">No folders yet</div>';
return;
}
listEl.innerHTML = folders.map(function (f) {
var count = (f.atsigns || []).length;
return `<div class="folder-dropdown-item">
<span class="folder-dropdown-item-name">${escapeHtml(f.name)} <span style="color:var(--color-text-muted);font-size:11px;">(${count})</span></span>
<div class="folder-dropdown-actions">
<button onclick="promptRenameFolder('${escapeJsString(f.name)}')" title="Rename">✎</button>
<button class="folder-delete-btn" onclick="deleteFolder('${escapeJsString(f.name)}')" title="Delete">×</button>
</div>
</div>`;
}).join('');
}
document.addEventListener('click', function (event) {
var dropdown = document.getElementById('folder-dropdown');
if (dropdown && !dropdown.hidden) {
var wrapper = dropdown.parentElement;
if (!wrapper.contains(event.target)) dropdown.hidden = true;
}
});
// --- End folder management ---
async function selectAtSign(atsign, password) {
try {
const payload = { atsign };
if (password) payload.password = password;
const response = await fetch('/api/select-atsign', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
window.location.href = '/';
} else {
const error = await response.json();
if (error.error === 'password_required') {
showPasswordPrompt(atsign);
} else {
const errorMessage = error.error || 'Unknown error';
showErrorNotification(errorMessage, atsign);
}
}
} catch (error) {
console.error('Error selecting atSign:', error);
showErrorNotification('Network error: ' + error.message, atsign);
}
}
function showPasswordPrompt(atsign) {
let overlay = document.getElementById('password-modal-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'password-modal-overlay';
overlay.className = 'modal-overlay';
document.body.appendChild(overlay);
}
overlay.innerHTML = `
<div class="modal-card">
<h2 class="modal-title">Password Required</h2>
<div class="modal-atsign"><strong>${escapeHtml(atsign)}</strong></div>
<p class="modal-message">This atKeys file is password-protected.</p>
<div style="margin: 16px 0;">
<input type="password" id="password-input" placeholder="Enter password"
style="width: 100%; padding: 10px; border: 1px solid var(--color-border); border-radius: var(--radius-sm); font-size: 14px; background: var(--color-surface); color: var(--color-text); box-sizing: border-box;" />
</div>
<div id="password-error" style="color: var(--color-error); font-size: 13px; margin-bottom: 8px; display: none;"></div>
<div class="modal-actions">
<button class="modal-btn" onclick="closePasswordPrompt()">Cancel</button>
<button class="modal-btn modal-btn-primary" id="password-submit-btn" onclick="submitPassword('${escapeJsString(atsign)}')">Unlock</button>
</div>
</div>
`;
overlay.classList.add('modal-visible');
const input = document.getElementById('password-input');
input.focus();
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') submitPassword(atsign);
});
}
async function submitPassword(atsign) {
const input = document.getElementById('password-input');
const errorEl = document.getElementById('password-error');
const submitBtn = document.getElementById('password-submit-btn');
const password = input.value;
if (!password) {
errorEl.textContent = 'Password is required';
errorEl.style.display = 'block';
return;
}
submitBtn.disabled = true;
submitBtn.textContent = 'Unlocking...';
errorEl.style.display = 'none';
try {
const response = await fetch('/api/select-atsign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ atsign, password })
});
if (response.ok) {
window.location.href = '/';
} else {
const error = await response.json();
if (error.error === 'password_required') {
errorEl.textContent = 'Incorrect password. Please try again.';
errorEl.style.display = 'block';
input.value = '';
input.focus();
} else {
closePasswordPrompt();
showErrorNotification(error.error || 'Unknown error', atsign);
}
}
} catch (e) {
errorEl.textContent = 'Network error: ' + e.message;
errorEl.style.display = 'block';
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Unlock';
}
}
function closePasswordPrompt() {
const overlay = document.getElementById('password-modal-overlay');
if (overlay) overlay.classList.remove('modal-visible');
}
// 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 modal-btn-primary" 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();
const password = (document.getElementById('wizard-password')?.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 payload = { requestedAtSigns, otpExpiry, deviceName, appName };
if (password) payload.password = password;
const response = await fetch('/api/wizard/reports', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
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 == null ? '' : value)
.replaceAll('\\', '\\\\')
.replaceAll("'", "\\'")
.replaceAll('\n', '\\n')
.replaceAll('\r', '\\r')
.replaceAll('"', '"');
}
function shellQuote(value) {
return "'" + String(value).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 Available Atsigns 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';
const password = (document.getElementById('onboard-password-input')?.value || '').trim();
const pwArg = password ? ` -P ${shellQuote(password)}` : '';
const command = `at_activate onboard -a ${atsign} -c ${licenseKey}${pwArg}`;
// 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();
''';