import { hostCore, hostState } from "./chunk-KL66PICH.js"; // src/plugins/logbook.ts var bridge = window; var el = (id) => document.getElementById(id); var form = el("log-entry-form"); var callInput = el("log-call"); var freqInput = el("log-freq"); var modeInput = el("log-mode"); var rstSentInput = el("log-rst-sent"); var rstRcvdInput = el("log-rst-rcvd"); var gridInput = el("log-grid"); var nameInput = el("log-name"); var commentInput = el("log-comment"); var operatorInput = el("log-operator"); var rowsBody = el("log-rows"); var summaryEl = el("log-summary"); var workedEl = el("log-worked-before"); var clockEl = el("log-clock"); var stationCallEl = el("log-station-callsign"); var stationRigEl = el("log-station-rig-name"); var stationGridEl = el("log-station-grid"); var filterCall = el("log-filter-call"); var filterBand = el("log-filter-band"); var filterMode = el("log-filter-mode"); var importBtn = el("log-import-btn"); var importFile = el("log-import-file"); var exportLink = el("log-export-btn"); var clearBtn = el("log-clear-btn"); var saveBtn = el("log-save-btn"); var contestIdInput = el("log-contest-id"); var stxInput = el("log-stx"); var srxInput = el("log-srx"); var statisticsRows = el("log-statistics-rows"); var cabrilloContest = el("log-cbr-contest"); var cabrilloCallsign = el("log-cbr-callsign"); var cabrilloOperator = el("log-cbr-operator"); var cabrilloPower = el("log-cbr-power"); var cabrilloScore = el("log-cbr-score"); var cabrilloExport = el("log-cbr-export"); var entryStartedAt = null; var entryRigId = null; var entryRigName = null; var entryGrid = null; var qsos = []; var workedRequest = 0; function canWriteLogbook() { return !hostState.authEnabled || hostState.authRoles.includes("administrator") || hostState.authRoles.includes("write"); } function notify(message, kind) { if (bridge.trxUi.notify) bridge.trxUi.notify(message, kind ? { kind } : void 0); else hostCore.showHint(message, 2e3); } async function getJson(path) { const response = await fetch(path); if (!response.ok) throw new Error(`HTTP ${String(response.status)}`); return await response.json(); } function formatFreq(hz) { if (!Number.isFinite(hz) || hz <= 0) return ""; return String(Number((hz / 1e6).toFixed(6))); } function parseFreq(text) { const value = Number(text.trim().replace(/\s+/g, "").replace(",", ".")); if (!Number.isFinite(value) || value <= 0) return null; return value < 1e5 ? Math.round(value * 1e6) : Math.round(value); } function numberOrNull(text) { const trimmed = (text ?? "").trim(); if (!trimmed || !/^\d+$/.test(trimmed)) return null; const value = Number(trimmed); return Number.isSafeInteger(value) ? value : null; } function utcDate(iso) { const date = new Date(iso); return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10); } function utcTime(iso) { const date = new Date(iso); return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(11, 19); } async function openEntry(seed = {}) { const params = new URLSearchParams(); if (hostState.lastActiveRigId) params.set("remote", hostState.lastActiveRigId); if (seed.decoder) params.set("decoder", seed.decoder); if (seed.call) params.set("call", seed.call); if (seed.gridsquare) params.set("gridsquare", seed.gridsquare); try { const prefill = await getJson(`/api/logbook/prefill?${params.toString()}`); applyPrefill(prefill); } catch (error) { console.error("logbook prefill failed", error); } } function applyPrefill(prefill) { entryStartedAt = prefill.started_at; entryRigId = prefill.rig_id; entryRigName = prefill.my_rig; if (freqInput) freqInput.value = formatFreq(prefill.freq_hz); if (modeInput) modeInput.value = prefill.submode ?? prefill.mode; if (callInput && prefill.call) callInput.value = prefill.call; if (gridInput && prefill.gridsquare) gridInput.value = prefill.gridsquare; if (stationRigEl) stationRigEl.textContent = prefill.my_rig ?? "no rig"; showClock(prefill); updateWorkedBefore(); } function showClock(prefill) { if (!clockEl) return; const time = utcTime(prefill.started_at); const drift = Math.abs(Date.now() - prefill.epoch_ms); clockEl.textContent = drift > 1e3 ? `${time}Z · your clock is ${(drift / 1e3).toFixed(0)}s out` : `${time}Z`; clockEl.classList.toggle("is-adrift", drift > 1e3); } function resetEntry() { for (const input of [callInput, rstSentInput, rstRcvdInput, gridInput, nameInput, commentInput, srxInput]) { if (input) input.value = ""; } if (workedEl) workedEl.textContent = ""; void openEntry(); } async function submitEntry(event) { event.preventDefault(); const call = callInput?.value.trim() ?? ""; if (!call) { notify("A contact needs a callsign", "error"); callInput?.focus(); return; } const freqHz = parseFreq(freqInput?.value ?? ""); if (freqHz == null) { notify("A contact needs a frequency", "error"); freqInput?.focus(); return; } const typedMode = (modeInput?.value ?? "").trim().toUpperCase(); const isSideband = typedMode === "USB" || typedMode === "LSB"; const body = { // The server stamps the time; this is the one it gave when the entry // opened, so a contact logged five minutes later keeps the time it started. started_at: entryStartedAt, call, freq_hz: freqHz, mode: isSideband ? "SSB" : typedMode, submode: isSideband ? typedMode : null, rst_sent: rstSentInput?.value ?? null, rst_rcvd: rstRcvdInput?.value ?? null, gridsquare: gridInput?.value ?? null, name: nameInput?.value ?? null, comment: commentInput?.value ?? null, contest_id: contestIdInput?.value ?? null, // The exchange is a number when it is a serial and a word when it is a // zone or a section; both are kept, and the log writes whichever it has. stx: numberOrNull(stxInput?.value), stx_string: numberOrNull(stxInput?.value) == null ? stxInput?.value ?? null : null, srx: numberOrNull(srxInput?.value), srx_string: numberOrNull(srxInput?.value) == null ? srxInput?.value ?? null : null, station_callsign: stationCallEl?.textContent?.trim() ?? null, operator: operatorInput?.value ?? null, my_gridsquare: entryGrid, my_rig: entryRigName, rig_id: entryRigId }; if (saveBtn) saveBtn.disabled = true; try { const response = await fetch("/api/logbook", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); if (!response.ok) { const detail = await response.json().catch(() => ({})); throw new Error(detail.error ?? `HTTP ${String(response.status)}`); } notify(`${call} logged`); const sent = numberOrNull(stxInput?.value); resetEntry(); if (stxInput && sent != null) stxInput.value = String(sent + 1); await refreshLog(); } catch (error) { notify(`Could not log: ${error instanceof Error ? error.message : String(error)}`, "error"); } finally { if (saveBtn) saveBtn.disabled = false; } } function updateWorkedBefore() { if (!workedEl) return; const call = callInput?.value.trim() ?? ""; if (call.length < 3) { workedEl.textContent = ""; return; } const request = ++workedRequest; void getJson( `/api/logbook/worked/${encodeURIComponent(call)}` ).then((answer) => { if (request !== workedRequest || !workedEl) return; if (answer.worked.length === 0) { workedEl.textContent = "Not worked before"; workedEl.classList.remove("is-worked"); return; } const where = answer.worked.map((entry) => `${entry.band} ${entry.mode}`).join(", "); workedEl.textContent = `Worked before: ${where}`; workedEl.classList.add("is-worked"); }).catch(() => { }); } function currentQuery() { const params = new URLSearchParams(); const call = filterCall?.value.trim(); if (call) params.set("call", call); if (filterBand?.value) params.set("band", filterBand.value); if (filterMode?.value) params.set("mode", filterMode.value); return params.toString(); } async function refreshLog() { try { const query = currentQuery(); const answer = await getJson( `/api/logbook${query ? `?${query}` : ""}` ); qsos = answer.qsos; renderRows(); renderFilterOptions(); if (summaryEl) { summaryEl.textContent = query ? `${String(qsos.length)} of ${String(answer.total)} contacts` : `${String(answer.total)} contact${answer.total === 1 ? "" : "s"}`; } if (exportLink) exportLink.href = `/api/logbook/export.adi${query ? `?${query}` : ""}`; await refreshStatistics(); } catch (error) { console.error("logbook read failed", error); } } async function refreshStatistics() { if (!statisticsRows) return; try { const answer = await getJson("/api/logbook/statistics"); if (answer.bands.length === 0) { statisticsRows.innerHTML = '