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('&', '&amp;')
      .replaceAll('<', '&lt;')
      .replaceAll('>', '&gt;')
      .replaceAll('"', '&quot;')
      .replaceAll("'", '&#39;');
}

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();
''';