TinyGS - Advanced Hex Transmit

HTML

<h2>TinyGS Advanced Dashboard</h2>

        <div class="card">
        <strong>Station Status</strong>
        <ul id="gsstatus" class="status-list">
            <li>
                <span class="status-label">Frequency:</span>
                <span id="stat-freq" class="status-value">433.000 MHz</span>
            </li>
            <li>
                <span class="status-label">Mode:</span>
                <span id="stat-mode" class="status-value">LoRa</span>
            </li>
            <li>
                <span class="status-label">Uptime:</span>
                <span id="stat-uptime" class="status-value">00:42:15</span>
            </li>
        </ul>
    </div>

    <div class="card" style="min-width: 400px; max-width: 500px;">
        <div style="display: flex; justify-content: space-between; align-items: center;">
            <strong>Transaction Log (Local)</strong>
            <button onclick="exportTxLog()"
                style="font-size: 0.8em; padding: 3px 8px; cursor: pointer; background: #28a745; color: white; border: none; border-radius: 3px;">Export
                JSON</button>
        </div>
        <div
            style="max-height: 120px; overflow-y: auto; margin-top: 10px; font-size: 0.85em; border-top: 1px solid #ddd; padding-top: 5px;">
            <table style="width: 100%; margin: 0; text-align: left;">
                <thead>
                    <tr style="color: #666;">
                        <th style="padding-bottom: 5px;">Timestamp (ISO)</th>
                        <th style="padding-bottom: 5px;">Payload (Hex)</th>
                    </tr>
                </thead>
                <tbody id="txLogBody">
                    <tr>
                        <td colspan="2" style="text-align: center; color: #999;">No local logs found.</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>

    <div id="t1" style="height:50px;"></div>

   ...

CSS

/* Reusing styles from IOTWEBCONF_DASHBOARD_STYLE_INNER in html.h */
body {
  font-family: Arial, sans-serif;
  background-color: #f4f4f4;
  text-align: center;
  padding: 20px;
}

table {
  margin: 20px auto;
  border-collapse: collapse;
}

h3 {
  text-align: center;
}

.card {
  margin: 10px;
  text-align: left;
  font-family: Arial;
  border: 3px groove;
  border-radius: 0.3rem;
  display: inline-block;
  padding: 15px;
  min-width: 300px;
  background: #fff;
  vertical-align: top;
}

td {
  padding: 0 10px;
}

.G {
  color: green;
  font-weight: bold;
}

.R {
  color: red;
  font-weight: bold;
}

/* Specific styles for the Advanced Hex Tool */
.tx-input {
  width: 100%;
  font-family: monospace;
  padding: 8px;
  margin: 10px 0;
  box-sizing: border-box;
  text-transform: uppercase;
}

.legal-box {
  font-size: 0.85em;
  background: #fff3cd;
  padding: 10px;
  border: 1px solid #ffeeba;
  border-radius: 4px;
  margin-bottom: 10px;
}

.btn-send {
  width: 100%;
  padding: 10px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-weight: bold;
}

.btn-send:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

.timer-display {
  font-size: 0.9em;
  color: #666;
  margin-top: 5px;
}

select {
  width: 100%;
  padding: 5px;
  margin-bottom: 10px;
}

.byte-counter {
  float: right;
  font-size: 0.9em;
  color: #666;
  font-weight: normal;
}

  .status-list {
            list-style: none;
            padding: 0;
            margin: 10px 0;
            text-align: left;
        }

        .status-list li {
            display: flex;
            justify-content: space-between;
            padding: 6px 0;
            border-bottom: 1px dashed #eee;
            font-size: 0.95em;
        }

        .status-label {
            font-weight: bold;
            color: #444;
        }

      ...

JavaScript

let currentCallsignHex = '';

function injectTemplate() {
  const template = document.getElementById('template').value;
  if (template) {
    document.getElementById('hexInput').value = template;
    validateHex();
  }
}
// --- LOGGING UTILITIES ---
const LOG_KEY = 'tinygs_tx_history';
const MAX_LOGS = 100; // Keep the last 100 TXs to prevent unlimited growth

function loadTxLogs() {
  try {
    const logs = localStorage.getItem(LOG_KEY);
    return logs ? JSON.parse(logs) : [];
  } catch (e) {
    return [];
  }
}

function saveTxLog(hexPayload) {
  let logs = loadTxLogs();
  const entry = {
    timestamp: new Date().toISOString(),
    payload: hexPayload,
  };
  logs.unshift(entry); // Add to the beginning of the array

  if (logs.length > MAX_LOGS) {
    logs.pop(); // Remove oldest to stay within limit
  }

  localStorage.setItem(LOG_KEY, JSON.stringify(logs));
  renderTxLog();
}

function renderTxLog() {
  const logs = loadTxLogs();
  const tbody = document.getElementById('txLogBody');

  if (logs.length === 0) {
    tbody.innerHTML =
      '<tr><td colspan="2" style="text-align: center; color: #999;">No local logs found.</td></tr>';
    return;
  }

  tbody.innerHTML = '';
  logs.forEach((log) => {
    const tr = document.createElement('tr');

    // Format Timestamp (Show just time & date for space, hover for full ISO)
    const tdTime = document.createElement('td');
    const d = new Date(log.timestamp);
    tdTime.innerText = d.toISOString().replace('T', ' ').substring(0, 19);
    tdTime.title = log.timestamp;
    tdTime.style.paddingRight = '10px';

    // Format Payload (Truncate to 36 chars)
    const tdPayload = document.createElement('td');
    tdPayload.style.fontFamily = 'monospace';
    if (log.payload.length > 36) {
      tdPayload.innerText = log.payload.substring(0, 36) + '...';
      tdPayload.title = log.payload; // Show full hex on hover
    } else {
      tdPayload.innerText...