[feat](trx-frontend-http): implement scheduler UI improvements (P0–P3)
Implement all 15 scheduler improvement tasks from docs/scheduler_improvements.md: P0 — Usability Fixes: - Highlight active entry in time-span table with sch-active class - Bookmark existence validation on save with toast error - Dirty-state indicator for satellite section via markDirty bridge P1 — Information Density & Clarity: - Show local time alongside UTC in entry table and timeline - Expand entry details by default with localStorage persistence - Richer "Now Playing" status card with freq, mode, active decoders P2 — Interaction Improvements: - Inline entry editing directly in table rows - Drag-to-reorder entries with HTML5 drag-and-drop - Timeline click-to-add with pre-filled hour range - Improved extra-channels management with chip list and dropdown P3 — Feature Enhancements: - Grayline location lookup by Maidenhead grid square - Expanded satellite preset library (NOAA 15/18/19, ISS, SO-50) - Scheduler activity log with ring buffer backend and UI - Timeline interleave visualization with alternating color stripes - Keyboard shortcuts (Shift+R/N/P) for scheduler control https://claude.ai/code/session_01VFLAHs1UMzPso3GWSQP9wJ Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1113,6 +1113,10 @@
|
||||
<div class="now-playing-card">
|
||||
<div id="scheduler-status-card" class="sch-status-card">No activity yet.</div>
|
||||
</div>
|
||||
<details class="sch-activity-log-details" id="scheduler-activity-log-wrap" style="display:none;">
|
||||
<summary>Activity Log</summary>
|
||||
<div id="scheduler-activity-log" class="sch-activity-log"></div>
|
||||
</details>
|
||||
<div class="sch-row">
|
||||
<label class="sch-label">Mode
|
||||
<select id="scheduler-mode-select" class="status-input" aria-label="Scheduler mode">
|
||||
@@ -1133,6 +1137,9 @@
|
||||
<label class="sch-label">Longitude (°)
|
||||
<input type="number" id="scheduler-gl-lon" class="status-input" step="0.001" placeholder="e.g. 18.646" />
|
||||
</label>
|
||||
<label class="sch-label">Grid square
|
||||
<input type="text" id="scheduler-gl-grid" class="status-input" placeholder="e.g. JO94" maxlength="8" style="width:7rem;" />
|
||||
</label>
|
||||
<label class="sch-label">Transition window (min)
|
||||
<input type="number" id="scheduler-gl-window" class="status-input" min="5" max="120" value="20" />
|
||||
</label>
|
||||
@@ -1204,11 +1211,11 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<details class="sch-ts-details">
|
||||
<details class="sch-ts-details" open>
|
||||
<summary>Entry details</summary>
|
||||
<table class="sch-ts-table">
|
||||
<thead>
|
||||
<tr><th>Start</th><th>End</th><th>Center freq</th><th>Primary bookmark</th><th>Extra channels</th><th>Label</th><th>Interleave (min)</th><th>REC</th><th></th></tr>
|
||||
<tr><th class="sch-drag-th"></th><th>Start</th><th>End</th><th>Center freq</th><th>Primary bookmark</th><th>Extra channels</th><th>Label</th><th>Interleave (min)</th><th>REC</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody id="scheduler-ts-tbody"></tbody>
|
||||
</table>
|
||||
@@ -1254,6 +1261,11 @@
|
||||
<option value="">— custom —</option>
|
||||
<option value="METEOR-M2 3|57166">Meteor-M2 3 (137.900 MHz LRPT)</option>
|
||||
<option value="METEOR-M2-4|59051">Meteor-M2-4 (137.900 MHz LRPT)</option>
|
||||
<option value="NOAA 15|25338">NOAA 15 (137.620 MHz APT)</option>
|
||||
<option value="NOAA 18|28654">NOAA 18 (137.9125 MHz APT)</option>
|
||||
<option value="NOAA 19|33591">NOAA 19 (137.100 MHz APT)</option>
|
||||
<option value="ISS|25544">ISS (145.825 MHz APRS)</option>
|
||||
<option value="SO-50|27607">SO-50 (436.795 MHz FM)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="bm-label">Satellite name
|
||||
|
||||
@@ -55,6 +55,11 @@
|
||||
return typeof b.getBookmarks === "function" ? b.getBookmarks() : [];
|
||||
}
|
||||
|
||||
function markDirty() {
|
||||
var b = getBridge();
|
||||
if (typeof b.markDirty === "function") b.markDirty();
|
||||
}
|
||||
|
||||
function bmName(id) {
|
||||
const bm = getBookmarks().find(function (b) { return b.id === id; });
|
||||
return bm ? bm.name : String(id || "");
|
||||
@@ -208,6 +213,7 @@
|
||||
var sat = ensureSatelliteConfig();
|
||||
sat.entries.splice(idx, 1);
|
||||
renderEntries();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// ── Form: open ────────────────────────────────────────────────────
|
||||
@@ -275,6 +281,7 @@
|
||||
|
||||
closeForm();
|
||||
renderEntries();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// ── Preset change handler ─────────────────────────────────────────
|
||||
@@ -290,6 +297,12 @@
|
||||
if (dom.enabled) {
|
||||
dom.enabled.addEventListener("change", function () {
|
||||
if (dom.body) dom.body.style.display = dom.enabled.checked ? "" : "none";
|
||||
markDirty();
|
||||
});
|
||||
}
|
||||
if (dom.pretune) {
|
||||
dom.pretune.addEventListener("input", function () {
|
||||
markDirty();
|
||||
});
|
||||
}
|
||||
if (dom.addBtn) dom.addBtn.addEventListener("click", function () { openForm(null, null); });
|
||||
|
||||
@@ -337,6 +337,7 @@
|
||||
currentSchedulerStatus = st || null;
|
||||
renderStatus(st);
|
||||
renderSchedulerInterleaveStatus();
|
||||
renderActivityLog();
|
||||
renderSatPassStatus();
|
||||
})
|
||||
.catch(function () {});
|
||||
@@ -364,7 +365,57 @@
|
||||
const satLabel = st.active_satellite
|
||||
? " [SAT: " + st.active_satellite + "]"
|
||||
: "";
|
||||
el.textContent = "Last applied: " + name + satLabel + ts;
|
||||
var details = "";
|
||||
if (st.freq_hz) {
|
||||
details += formatFreq(st.freq_hz);
|
||||
if (st.mode) details += " \u00B7 " + st.mode;
|
||||
if (st.active_decoders && st.active_decoders.length > 0) {
|
||||
details += " \u00B7 " + st.active_decoders.join(", ") + " active";
|
||||
}
|
||||
}
|
||||
if (details) {
|
||||
el.innerHTML = "Last applied: " + escHtml(name) + satLabel + ts +
|
||||
'<br><span class="sch-status-detail">' + escHtml(details) + '</span>';
|
||||
} else {
|
||||
el.textContent = "Last applied: " + name + satLabel + ts;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Activity log
|
||||
// -------------------------------------------------------------------------
|
||||
function apiGetSchedulerLog(rigId) {
|
||||
return fetch("/scheduler/" + encodeURIComponent(rigId) + "/log").then(function (r) {
|
||||
return r.ok ? r.json() : [];
|
||||
});
|
||||
}
|
||||
|
||||
function renderActivityLog() {
|
||||
var wrap = document.getElementById("scheduler-activity-log-wrap");
|
||||
var container = document.getElementById("scheduler-activity-log");
|
||||
if (!wrap || !container || !currentRigId) return;
|
||||
|
||||
apiGetSchedulerLog(currentRigId).then(function (entries) {
|
||||
if (!entries || entries.length === 0) {
|
||||
wrap.style.display = "none";
|
||||
return;
|
||||
}
|
||||
wrap.style.display = "";
|
||||
var html = entries.slice().reverse().map(function (e) {
|
||||
var d = new Date(e.utc * 1000);
|
||||
var ts = d.toUTCString();
|
||||
var action = e.action || "unknown";
|
||||
var label = e.entry_label || "";
|
||||
var bm = e.bookmark_name || "";
|
||||
return '<div class="sch-log-entry">' +
|
||||
'<span class="sch-log-time">' + escHtml(ts) + '</span> ' +
|
||||
'<span class="sch-log-action">' + escHtml(action) + '</span> ' +
|
||||
(bm ? '<span class="sch-log-bm">' + escHtml(bm) + '</span>' : '') +
|
||||
(label ? ' <span class="sch-log-label">(' + escHtml(label) + ')</span>' : '') +
|
||||
'</div>';
|
||||
}).join("");
|
||||
container.innerHTML = html;
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -402,6 +453,10 @@
|
||||
const lon = gl.lon != null ? gl.lon : (typeof serverLon !== "undefined" ? serverLon : "");
|
||||
setInputValue("scheduler-gl-lat", lat != null ? lat : "");
|
||||
setInputValue("scheduler-gl-lon", lon != null ? lon : "");
|
||||
var gridEl = document.getElementById("scheduler-gl-grid");
|
||||
if (gridEl && lat !== "" && lon !== "") {
|
||||
gridEl.value = latLonToGrid(lat, lon);
|
||||
}
|
||||
setInputValue("scheduler-gl-window", gl.transition_window_min != null ? gl.transition_window_min : 20);
|
||||
renderBookmarkSelect("scheduler-gl-dawn", gl.dawn_bookmark_id);
|
||||
renderBookmarkSelect("scheduler-gl-day", gl.day_bookmark_id);
|
||||
@@ -413,6 +468,10 @@
|
||||
const lon = typeof serverLon !== "undefined" ? serverLon : "";
|
||||
setInputValue("scheduler-gl-lat", lat != null ? lat : "");
|
||||
setInputValue("scheduler-gl-lon", lon != null ? lon : "");
|
||||
var gridEl2 = document.getElementById("scheduler-gl-grid");
|
||||
if (gridEl2 && lat !== "" && lon !== "") {
|
||||
gridEl2.value = latLonToGrid(lat, lon);
|
||||
}
|
||||
setInputValue("scheduler-gl-window", 20);
|
||||
renderBookmarkSelect("scheduler-gl-dawn", null);
|
||||
renderBookmarkSelect("scheduler-gl-day", null);
|
||||
@@ -596,7 +655,7 @@
|
||||
}
|
||||
|
||||
var W = 1000;
|
||||
var H = 62;
|
||||
var H = 80;
|
||||
var BAR_Y = 6;
|
||||
var BAR_H = 30;
|
||||
var TICK_Y = BAR_Y + BAR_H + 2;
|
||||
@@ -634,6 +693,31 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Interleave stripes for overlapping entries
|
||||
var interleaveMin = currentConfig && currentConfig.interleave_min ? Number(currentConfig.interleave_min) : 0;
|
||||
if (interleaveMin > 0 && entries.length > 1) {
|
||||
// Find overlap regions where 2+ entries are active
|
||||
for (var m = 0; m < 1440; m += interleaveMin) {
|
||||
var overlapping = [];
|
||||
entries.forEach(function (entry, idx) {
|
||||
if (schedulerEntryIsActive(entry, m)) {
|
||||
overlapping.push(idx);
|
||||
}
|
||||
});
|
||||
if (overlapping.length > 1) {
|
||||
var stripeX = (m / 1440) * W;
|
||||
var stripeW = Math.max(1, (interleaveMin / 1440) * W);
|
||||
// Determine which entry "owns" this stripe via cycle position
|
||||
var cyclePos = m % (interleaveMin * overlapping.length);
|
||||
var ownerSlot = Math.floor(cyclePos / interleaveMin);
|
||||
var ownerIdx = overlapping[ownerSlot % overlapping.length];
|
||||
var stripeColor = TIMELINE_COLORS[ownerIdx % TIMELINE_COLORS.length];
|
||||
svg += '<rect x="' + stripeX.toFixed(1) + '" y="' + (BAR_Y + BAR_H - 5) + '" width="' + stripeW.toFixed(1) +
|
||||
'" height="5" fill="' + stripeColor + '" opacity="0.9" />';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tick marks every 3 hours
|
||||
for (var h = 0; h <= 24; h += 3) {
|
||||
var tx = (h / 24) * W;
|
||||
@@ -645,6 +729,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Local time ticks
|
||||
var LOCAL_TICK_Y = TICK_Y + 18;
|
||||
for (var h = 0; h < 24; h += 3) {
|
||||
var localMin = h * 60;
|
||||
var utcOffset = new Date().getTimezoneOffset(); // offset in minutes (negative for east of UTC)
|
||||
var utcMin = (localMin + utcOffset + 1440) % 1440;
|
||||
var tx = (utcMin / 1440) * W;
|
||||
svg += '<text class="sch-timeline-tick-label sch-timeline-local-tick" x="' + (tx + 3).toFixed(1) + '" y="' + (LOCAL_TICK_Y + 10) +
|
||||
'">' + String(h).padStart(2, "0") + 'L</text>';
|
||||
}
|
||||
|
||||
// Current time needle
|
||||
svg += '<g id="sch-timeline-needle-g">' + timelineNeedleSvg() + '</g>';
|
||||
|
||||
@@ -659,6 +754,29 @@
|
||||
if (entry) schOpenEntryForm(entry, i);
|
||||
});
|
||||
});
|
||||
|
||||
// Click-to-add on empty timeline region
|
||||
var svgEl = container.querySelector('svg');
|
||||
if (svgEl) {
|
||||
svgEl.addEventListener('click', function (e) {
|
||||
// Only trigger if clicking on the background bar, not on a segment
|
||||
if (e.target.classList.contains('sch-timeline-seg')) return;
|
||||
var rect = svgEl.getBoundingClientRect();
|
||||
var xPct = (e.clientX - rect.left) / rect.width;
|
||||
var clickMin = Math.floor(xPct * 1440);
|
||||
var startHour = Math.floor(clickMin / 60);
|
||||
var startMin = startHour * 60;
|
||||
var endMin = ((startHour + 1) % 24) * 60;
|
||||
|
||||
// Pre-fill the entry form with the clicked hour
|
||||
schOpenEntryForm(null, null);
|
||||
var startEl = document.getElementById('scheduler-ts-start');
|
||||
var endEl = document.getElementById('scheduler-ts-end');
|
||||
if (startEl) startEl.value = minToHHMM(startMin);
|
||||
if (endEl) endEl.value = minToHHMM(endMin);
|
||||
});
|
||||
svgEl.style.cursor = 'crosshair';
|
||||
}
|
||||
}
|
||||
|
||||
function timelineNeedleSvg() {
|
||||
@@ -675,6 +793,57 @@
|
||||
if (g) g.innerHTML = timelineNeedleSvg();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Inline row editing
|
||||
// -------------------------------------------------------------------------
|
||||
function schInlineEdit(tr, entry, idx) {
|
||||
var bmOptions = bookmarkList.map(function (bm) {
|
||||
var sel = bm.id === entry.bookmark_id ? ' selected' : '';
|
||||
return '<option value="' + escHtml(bm.id) + '"' + sel + '>' + escHtml(bm.name) + '</option>';
|
||||
}).join('');
|
||||
|
||||
tr.innerHTML =
|
||||
'<td class="sch-drag-handle" draggable="true" title="Drag to reorder">\u2807</td>' +
|
||||
'<td><input type="time" class="status-input sch-inline-input" value="' + minToHHMM(entry.start_min) + '" data-field="start" /></td>' +
|
||||
'<td><input type="time" class="status-input sch-inline-input" value="' + minToHHMM(entry.end_min) + '" data-field="end" /></td>' +
|
||||
'<td>' + (entry.center_hz ? formatFreq(entry.center_hz) : '\u2014') + '</td>' +
|
||||
'<td><select class="status-input sch-inline-input" data-field="bookmark">' + bmOptions + '</select></td>' +
|
||||
'<td>' + (Array.isArray(entry.bookmark_ids) && entry.bookmark_ids.length ? entry.bookmark_ids.map(function(id) { return escHtml(bmName(id)); }).join(', ') : '\u2014') + '</td>' +
|
||||
'<td><input type="text" class="status-input sch-inline-input" value="' + escHtml(entry.label || '') + '" data-field="label" /></td>' +
|
||||
'<td><input type="number" class="status-input sch-inline-input" value="' + (entry.interleave_min || '') + '" min="1" max="60" placeholder="\u2014" data-field="interleave" style="width:4rem;" /></td>' +
|
||||
'<td><input type="checkbox" ' + (entry.record ? 'checked' : '') + ' data-field="record" /></td>' +
|
||||
'<td><button class="sch-write sch-inline-save" type="button">Save</button><button class="sch-write sch-inline-cancel" type="button">Cancel</button></td>';
|
||||
|
||||
tr.classList.add('sch-inline-editing');
|
||||
|
||||
tr.querySelector('.sch-inline-save').addEventListener('click', function () {
|
||||
var startEl = tr.querySelector('[data-field="start"]');
|
||||
var endEl = tr.querySelector('[data-field="end"]');
|
||||
var bmEl = tr.querySelector('[data-field="bookmark"]');
|
||||
var labelEl = tr.querySelector('[data-field="label"]');
|
||||
var ilEl = tr.querySelector('[data-field="interleave"]');
|
||||
var recEl = tr.querySelector('[data-field="record"]');
|
||||
|
||||
if (bmEl && !bmEl.value) { alert('Please select a bookmark.'); return; }
|
||||
|
||||
entry.start_min = hhmmToMin(startEl.value);
|
||||
entry.end_min = hhmmToMin(endEl.value);
|
||||
entry.bookmark_id = bmEl.value;
|
||||
entry.label = labelEl.value.trim() || null;
|
||||
var ilVal = parseInt(ilEl.value, 10);
|
||||
entry.interleave_min = (!isNaN(ilVal) && ilVal > 0) ? ilVal : null;
|
||||
entry.record = recEl.checked;
|
||||
|
||||
currentConfig.entries[idx] = entry;
|
||||
renderTimespanEntries();
|
||||
markSchedulerDirty();
|
||||
});
|
||||
|
||||
tr.querySelector('.sch-inline-cancel').addEventListener('click', function () {
|
||||
renderTimespanEntries();
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TimeSpan entries table
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -688,6 +857,10 @@
|
||||
: [];
|
||||
entries.forEach(function (entry, idx) {
|
||||
const tr = document.createElement("tr");
|
||||
if (currentSchedulerStatus && currentSchedulerStatus.last_entry_id &&
|
||||
entry.id && String(entry.id) === String(currentSchedulerStatus.last_entry_id)) {
|
||||
tr.classList.add("sch-active");
|
||||
}
|
||||
const il = entry.interleave_min ? String(entry.interleave_min) + " min" : "—";
|
||||
const allDay = entry.start_min === entry.end_min;
|
||||
const centerCell = entry.center_hz ? formatFreq(entry.center_hz) : "—";
|
||||
@@ -696,8 +869,9 @@
|
||||
? extraIds.map(function (id) { return escHtml(bmName(id)); }).join(", ")
|
||||
: "—";
|
||||
tr.innerHTML =
|
||||
'<td>' + (allDay ? "All day" : minToHHMM(entry.start_min)) + '</td>' +
|
||||
'<td>' + (allDay ? "—" : minToHHMM(entry.end_min)) + '</td>' +
|
||||
'<td class="sch-drag-handle" draggable="true" title="Drag to reorder">\u2807</td>' +
|
||||
'<td>' + (allDay ? "All day" : minToHHMM(entry.start_min) + ' <span class="sch-local-time">(' + minToLocal(entry.start_min) + ')</span>') + '</td>' +
|
||||
'<td>' + (allDay ? "\u2014" : minToHHMM(entry.end_min) + ' <span class="sch-local-time">(' + minToLocal(entry.end_min) + ')</span>') + '</td>' +
|
||||
'<td>' + centerCell + '</td>' +
|
||||
'<td>' + escHtml(bmName(entry.bookmark_id)) + '</td>' +
|
||||
'<td>' + extraCell + '</td>' +
|
||||
@@ -714,7 +888,7 @@
|
||||
btn.addEventListener("click", function () {
|
||||
const i = parseInt(btn.dataset.idx, 10);
|
||||
const entry = currentConfig && currentConfig.entries ? currentConfig.entries[i] : null;
|
||||
if (entry) schOpenEntryForm(entry, i);
|
||||
if (entry) schInlineEdit(btn.closest('tr'), entry, i);
|
||||
});
|
||||
});
|
||||
tbody.querySelectorAll(".sch-remove-btn").forEach(function (btn) {
|
||||
@@ -722,6 +896,50 @@
|
||||
removeEntry(parseInt(btn.dataset.idx, 10));
|
||||
});
|
||||
});
|
||||
|
||||
// Drag-to-reorder
|
||||
(function () {
|
||||
var handles = tbody.querySelectorAll('.sch-drag-handle');
|
||||
var dragIdx = null;
|
||||
|
||||
handles.forEach(function (handle, idx) {
|
||||
var row = handle.parentElement;
|
||||
|
||||
handle.addEventListener('dragstart', function (e) {
|
||||
dragIdx = idx;
|
||||
row.classList.add('sch-dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', String(idx));
|
||||
});
|
||||
|
||||
row.addEventListener('dragover', function (e) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
row.classList.add('sch-drag-over');
|
||||
});
|
||||
|
||||
row.addEventListener('dragleave', function () {
|
||||
row.classList.remove('sch-drag-over');
|
||||
});
|
||||
|
||||
row.addEventListener('drop', function (e) {
|
||||
e.preventDefault();
|
||||
row.classList.remove('sch-drag-over');
|
||||
if (dragIdx === null || dragIdx === idx) return;
|
||||
var entries = currentConfig.entries;
|
||||
var moved = entries.splice(dragIdx, 1)[0];
|
||||
entries.splice(idx, 0, moved);
|
||||
renderTimespanEntries();
|
||||
markSchedulerDirty();
|
||||
});
|
||||
|
||||
handle.addEventListener('dragend', function () {
|
||||
row.classList.remove('sch-dragging');
|
||||
dragIdx = null;
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
renderTimeline();
|
||||
}
|
||||
|
||||
@@ -730,6 +948,15 @@
|
||||
return bm ? bm.name : String(id || "");
|
||||
}
|
||||
|
||||
function minToLocal(min) {
|
||||
// Convert UTC minutes-since-midnight to local time string
|
||||
var now = new Date();
|
||||
var utcMidnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
||||
var utcMs = utcMidnight.getTime() + min * 60000;
|
||||
var local = new Date(utcMs);
|
||||
return String(local.getHours()).padStart(2, "0") + ":" + String(local.getMinutes()).padStart(2, "0");
|
||||
}
|
||||
|
||||
function minToHHMM(min) {
|
||||
const h = Math.floor(min / 60) % 24;
|
||||
const m = min % 60;
|
||||
@@ -741,6 +968,43 @@
|
||||
return parseInt(parts[0] || "0", 10) * 60 + parseInt(parts[1] || "0", 10);
|
||||
}
|
||||
|
||||
function gridToLatLon(grid) {
|
||||
grid = String(grid).toUpperCase().trim();
|
||||
if (grid.length < 4) return null;
|
||||
var lonField = grid.charCodeAt(0) - 65;
|
||||
var latField = grid.charCodeAt(1) - 65;
|
||||
var lonSquare = parseInt(grid.charAt(2), 10);
|
||||
var latSquare = parseInt(grid.charAt(3), 10);
|
||||
if (isNaN(lonSquare) || isNaN(latSquare) || lonField < 0 || lonField > 17 || latField < 0 || latField > 17) return null;
|
||||
var lon = lonField * 20 + lonSquare * 2 - 180;
|
||||
var lat = latField * 10 + latSquare * 1 - 90;
|
||||
if (grid.length >= 6) {
|
||||
var lonSub = grid.charCodeAt(4) - 65;
|
||||
var latSub = grid.charCodeAt(5) - 65;
|
||||
if (lonSub >= 0 && lonSub < 24 && latSub >= 0 && latSub < 24) {
|
||||
lon += lonSub * (2 / 24) + (1 / 24);
|
||||
lat += latSub * (1 / 24) + (0.5 / 24);
|
||||
}
|
||||
} else {
|
||||
lon += 1; // center of square
|
||||
lat += 0.5;
|
||||
}
|
||||
return { lat: lat, lon: lon };
|
||||
}
|
||||
|
||||
function latLonToGrid(lat, lon) {
|
||||
lon = parseFloat(lon) + 180;
|
||||
lat = parseFloat(lat) + 90;
|
||||
if (isNaN(lon) || isNaN(lat)) return "";
|
||||
var lonField = String.fromCharCode(65 + Math.floor(lon / 20));
|
||||
var latField = String.fromCharCode(65 + Math.floor(lat / 10));
|
||||
var lonSquare = Math.floor((lon % 20) / 2);
|
||||
var latSquare = Math.floor(lat % 10);
|
||||
var lonSub = String.fromCharCode(97 + Math.floor(((lon % 2) / 2) * 24));
|
||||
var latSub = String.fromCharCode(97 + Math.floor((lat % 1) * 24));
|
||||
return lonField + latField + lonSquare + latSquare + lonSub + latSub;
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, "&")
|
||||
@@ -795,6 +1059,14 @@
|
||||
markSchedulerDirty();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Bookmark existence check
|
||||
// -------------------------------------------------------------------------
|
||||
function bookmarkExists(id) {
|
||||
if (!id) return true; // null/empty is allowed
|
||||
return bookmarkList.some(function (bm) { return bm.id === id; });
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Save
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -835,6 +1107,47 @@
|
||||
// Satellite overlay — saved regardless of base mode.
|
||||
config.satellites = collectSatelliteConfig();
|
||||
|
||||
// Validate bookmark existence before saving
|
||||
var missingBmErrors = [];
|
||||
if (mode === "grayline" && config.grayline) {
|
||||
var gl = config.grayline;
|
||||
var glFields = [
|
||||
["dawn_bookmark_id", "Grayline dawn"],
|
||||
["day_bookmark_id", "Grayline day"],
|
||||
["dusk_bookmark_id", "Grayline dusk"],
|
||||
["night_bookmark_id", "Grayline night"],
|
||||
];
|
||||
glFields.forEach(function (pair) {
|
||||
if (!bookmarkExists(gl[pair[0]])) missingBmErrors.push(pair[1] + " (bookmark " + gl[pair[0]] + ")");
|
||||
});
|
||||
}
|
||||
if (mode === "time_span" && Array.isArray(config.entries)) {
|
||||
config.entries.forEach(function (entry, idx) {
|
||||
var label = entry.label || "Entry #" + (idx + 1);
|
||||
if (!bookmarkExists(entry.bookmark_id)) {
|
||||
missingBmErrors.push(label + " primary bookmark (" + entry.bookmark_id + ")");
|
||||
}
|
||||
var extras = Array.isArray(entry.bookmark_ids) ? entry.bookmark_ids : [];
|
||||
extras.forEach(function (id) {
|
||||
if (!bookmarkExists(id)) {
|
||||
missingBmErrors.push(label + " extra channel (" + id + ")");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (config.satellites && Array.isArray(config.satellites.entries)) {
|
||||
config.satellites.entries.forEach(function (sat, idx) {
|
||||
var satLabel = sat.name || "Satellite #" + (idx + 1);
|
||||
if (!bookmarkExists(sat.bookmark_id)) {
|
||||
missingBmErrors.push(satLabel + " bookmark (" + sat.bookmark_id + ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
if (missingBmErrors.length > 0) {
|
||||
showSchedulerToast("Missing bookmarks: " + missingBmErrors.join("; "), true);
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById("scheduler-save-btn");
|
||||
if (btn) btn.disabled = true;
|
||||
|
||||
@@ -963,6 +1276,33 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Grid square ↔ lat/lon sync
|
||||
var gridEl = document.getElementById("scheduler-gl-grid");
|
||||
if (gridEl) {
|
||||
gridEl.addEventListener("input", function () {
|
||||
var ll = gridToLatLon(gridEl.value);
|
||||
if (ll) {
|
||||
setInputValue("scheduler-gl-lat", ll.lat.toFixed(3));
|
||||
setInputValue("scheduler-gl-lon", ll.lon.toFixed(3));
|
||||
markSchedulerDirty();
|
||||
}
|
||||
});
|
||||
}
|
||||
var latEl = document.getElementById("scheduler-gl-lat");
|
||||
var lonEl = document.getElementById("scheduler-gl-lon");
|
||||
[latEl, lonEl].forEach(function (el) {
|
||||
if (el) {
|
||||
el.addEventListener("input", function () {
|
||||
var la = parseFloat(document.getElementById("scheduler-gl-lat").value);
|
||||
var lo = parseFloat(document.getElementById("scheduler-gl-lon").value);
|
||||
var gEl = document.getElementById("scheduler-gl-grid");
|
||||
if (gEl && !isNaN(la) && !isNaN(lo)) {
|
||||
gEl.value = latLonToGrid(la, lo);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wireExtraBmAdd();
|
||||
wireSatelliteEvents();
|
||||
}
|
||||
@@ -988,25 +1328,36 @@
|
||||
let pendingExtraBmIds = [];
|
||||
|
||||
function renderExtraBmList() {
|
||||
const container = document.getElementById("scheduler-ts-extra-bm-list");
|
||||
var container = document.getElementById("scheduler-ts-extra-bm-list");
|
||||
if (!container) return;
|
||||
container.innerHTML = "";
|
||||
pendingExtraBmIds.forEach(function (id, idx) {
|
||||
const bm = bookmarkList.find(function (b) { return b.id === id; });
|
||||
const tag = document.createElement("span");
|
||||
tag.className = "sch-extra-bm-tag";
|
||||
tag.textContent = bm ? bm.name : id;
|
||||
const rm = document.createElement("span");
|
||||
rm.className = "sch-extra-bm-rm";
|
||||
rm.textContent = "×";
|
||||
rm.title = "Remove";
|
||||
rm.addEventListener("click", function () {
|
||||
var bm = bookmarkList.find(function (b) { return b.id === id; });
|
||||
var chip = document.createElement("span");
|
||||
chip.className = "sch-extra-bm-chip";
|
||||
var rmBtn = document.createElement("span");
|
||||
rmBtn.className = "sch-extra-bm-chip-rm";
|
||||
rmBtn.textContent = "\u00D7";
|
||||
rmBtn.title = "Remove";
|
||||
rmBtn.addEventListener("click", function () {
|
||||
pendingExtraBmIds.splice(idx, 1);
|
||||
renderExtraBmList();
|
||||
});
|
||||
tag.appendChild(rm);
|
||||
container.appendChild(tag);
|
||||
chip.appendChild(rmBtn);
|
||||
var label = document.createTextNode(" " + (bm ? bm.name : id));
|
||||
chip.appendChild(label);
|
||||
container.appendChild(chip);
|
||||
});
|
||||
|
||||
// Disable already-added bookmarks in dropdown
|
||||
var pick = document.getElementById("scheduler-ts-extra-bm-pick");
|
||||
if (pick) {
|
||||
Array.from(pick.options).forEach(function (opt) {
|
||||
if (opt.value) {
|
||||
opt.disabled = pendingExtraBmIds.includes(opt.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function wireExtraBmAdd() {
|
||||
@@ -1047,13 +1398,53 @@
|
||||
getConfig: function () { return currentConfig; },
|
||||
getStatus: function () { return currentSchedulerStatus; },
|
||||
getBookmarks: function () { return bookmarkList; },
|
||||
markDirty: function () { markSchedulerDirty(); },
|
||||
};
|
||||
if (window.satScheduler) window.satScheduler.wireEvents();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Keyboard shortcuts for scheduler control
|
||||
// -------------------------------------------------------------------------
|
||||
function isInputFocused() {
|
||||
var el = document.activeElement;
|
||||
if (!el) return false;
|
||||
var tag = el.tagName;
|
||||
return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable;
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (isInputFocused()) return;
|
||||
|
||||
if (e.shiftKey && e.key === "R") {
|
||||
e.preventDefault();
|
||||
// Toggle release to scheduler
|
||||
var releaseBtn = document.getElementById("scheduler-release-btn");
|
||||
if (releaseBtn && !releaseBtn.disabled) releaseBtn.click();
|
||||
} else if (e.shiftKey && e.key === "N") {
|
||||
e.preventDefault();
|
||||
schedulerSelectRelativeEntry(1);
|
||||
} else if (e.shiftKey && e.key === "P") {
|
||||
e.preventDefault();
|
||||
schedulerSelectRelativeEntry(-1);
|
||||
}
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Public API
|
||||
// -------------------------------------------------------------------------
|
||||
// Persist details open/closed state
|
||||
(function () {
|
||||
var details = document.querySelector(".sch-ts-details");
|
||||
if (!details) return;
|
||||
var key = "sch-details-open";
|
||||
var saved = localStorage.getItem(key);
|
||||
if (saved !== null) details.open = saved === "1";
|
||||
details.addEventListener("toggle", function () {
|
||||
localStorage.setItem(key, details.open ? "1" : "0");
|
||||
});
|
||||
})();
|
||||
|
||||
window.initScheduler = initScheduler;
|
||||
window.destroyScheduler = destroyScheduler;
|
||||
window.wireSchedulerEvents = wireSchedulerEvents;
|
||||
|
||||
@@ -4573,6 +4573,25 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
.sch-ts-table td:last-child button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.sch-ts-table tr.sch-active {
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
.sch-ts-table tr.sch-active td:first-child {
|
||||
padding-left: calc(0.6rem - 3px);
|
||||
}
|
||||
.sch-local-time {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.sch-timeline-local-tick {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.sch-status-detail {
|
||||
font-size: 0.82rem;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.sch-add-row {
|
||||
align-items: flex-end;
|
||||
}
|
||||
@@ -4778,7 +4797,7 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
.sch-timeline-wrap svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
height: 80px;
|
||||
}
|
||||
.sch-timeline-seg {
|
||||
cursor: pointer;
|
||||
@@ -4812,6 +4831,91 @@ button:focus-visible, input:focus-visible, select:focus-visible {
|
||||
padding: 0.3rem 0;
|
||||
user-select: none;
|
||||
}
|
||||
/* ── Inline Row Editing ──────────────────────────────────────────── */
|
||||
.sch-inline-editing td {
|
||||
padding: 0.25rem 0.4rem;
|
||||
}
|
||||
.sch-inline-input {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.15rem 0.3rem;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.sch-inline-save, .sch-inline-cancel {
|
||||
font-size: 0.75rem !important;
|
||||
padding: 0.15rem 0.4rem !important;
|
||||
}
|
||||
/* ── Drag-to-reorder ─────────────────────────────────────────────── */
|
||||
.sch-drag-handle {
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 1rem;
|
||||
width: 1.5rem;
|
||||
padding: 0.3rem !important;
|
||||
}
|
||||
.sch-drag-handle:active { cursor: grabbing; }
|
||||
.sch-drag-th { width: 1.5rem; }
|
||||
.sch-dragging { opacity: 0.5; }
|
||||
.sch-drag-over { border-top: 2px solid var(--accent); }
|
||||
/* ── Extra Bookmark Chips ────────────────────────────────────────── */
|
||||
.sch-extra-bm-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sch-extra-bm-chip-rm {
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
.sch-extra-bm-chip-rm:hover { opacity: 1; }
|
||||
/* ── Activity Log ────────────────────────────────────────────────── */
|
||||
.sch-activity-log-details {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.sch-activity-log-details summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
padding: 0.3rem 0;
|
||||
}
|
||||
.sch-activity-log {
|
||||
max-height: 12rem;
|
||||
overflow-y: auto;
|
||||
font-size: 0.78rem;
|
||||
padding: 0.3rem 0;
|
||||
}
|
||||
.sch-log-entry {
|
||||
padding: 0.15rem 0;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
}
|
||||
.sch-log-time {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.sch-log-action {
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
.sch-log-bm {
|
||||
color: var(--accent);
|
||||
}
|
||||
.sch-log-label {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
/* ── Inline Entry Editor (replaces modal overlay) ─────────────────── */
|
||||
#sch-entry-form-wrap {
|
||||
position: static;
|
||||
|
||||
@@ -602,6 +602,7 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(crate::server::scheduler::put_scheduler_activate_entry)
|
||||
.service(crate::server::scheduler::get_scheduler_control)
|
||||
.service(crate::server::scheduler::put_scheduler_control)
|
||||
.service(crate::server::scheduler::get_scheduler_log)
|
||||
.service(crate::server::background_decode::get_background_decode)
|
||||
.service(crate::server::background_decode::put_background_decode)
|
||||
.service(crate::server::background_decode::delete_background_decode)
|
||||
|
||||
@@ -529,6 +529,15 @@ pub struct SchedulerStatus {
|
||||
/// Name of the satellite whose pass is currently active (if any).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub active_satellite: Option<String>,
|
||||
/// Frequency in Hz of the active bookmark.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub freq_hz: Option<u64>,
|
||||
/// Mode string of the active bookmark.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<String>,
|
||||
/// Decoders active from the primary and extra bookmarks.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub active_decoders: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -542,6 +551,7 @@ async fn apply_scheduler_target(
|
||||
center_hz: Option<u64>,
|
||||
extra_bm_ids: &[String],
|
||||
satellite_name: Option<&str>,
|
||||
log_map: &SharedActivityLogMap,
|
||||
) -> Result<SchedulerStatus, String> {
|
||||
let bookmark = bookmarks
|
||||
.get_for_rig(remote, bookmark_id)
|
||||
@@ -592,6 +602,15 @@ async fn apply_scheduler_target(
|
||||
|
||||
apply_scheduler_decoders(rig_tx, remote, &bookmark, &extra_bookmarks).await;
|
||||
|
||||
let mut all_decoders: Vec<String> = bookmark.decoders.clone();
|
||||
for ebm in &extra_bookmarks {
|
||||
for d in &ebm.decoders {
|
||||
if !all_decoders.contains(d) {
|
||||
all_decoders.push(d.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status = SchedulerStatus {
|
||||
active: true,
|
||||
last_entry_id: entry_id.map(str::to_string),
|
||||
@@ -606,6 +625,9 @@ async fn apply_scheduler_target(
|
||||
last_center_hz: center_hz,
|
||||
last_bookmark_ids: extra_bm_ids.to_vec(),
|
||||
active_satellite: satellite_name.map(str::to_string),
|
||||
freq_hz: Some(bookmark.freq_hz),
|
||||
mode: Some(bookmark.mode.clone()),
|
||||
active_decoders: all_decoders,
|
||||
};
|
||||
|
||||
{
|
||||
@@ -613,12 +635,82 @@ async fn apply_scheduler_target(
|
||||
map.insert(remote.to_string(), status.clone());
|
||||
}
|
||||
|
||||
{
|
||||
let log = {
|
||||
let mut map = log_map.write().unwrap_or_else(|e| e.into_inner());
|
||||
map.entry(remote.to_string())
|
||||
.or_insert_with(|| Arc::new(ActivityLog::new()))
|
||||
.clone()
|
||||
};
|
||||
log.push(ActivityLogEntry {
|
||||
utc: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64,
|
||||
action: if satellite_name.is_some() {
|
||||
"satellite_aos".to_string()
|
||||
} else {
|
||||
"applied".to_string()
|
||||
},
|
||||
entry_label: entry_id.map(|s| s.to_string()),
|
||||
bookmark_name: Some(bookmark.name.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
/// Shared mutable state for scheduler status (one entry per rig).
|
||||
pub type SchedulerStatusMap = Arc<RwLock<HashMap<String, SchedulerStatus>>>;
|
||||
|
||||
// ============================================================================
|
||||
// Activity log
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ActivityLogEntry {
|
||||
pub utc: i64,
|
||||
pub action: String,
|
||||
pub entry_label: Option<String>,
|
||||
pub bookmark_name: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ActivityLog {
|
||||
entries: std::sync::Mutex<std::collections::VecDeque<ActivityLogEntry>>,
|
||||
}
|
||||
|
||||
impl Default for ActivityLog {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActivityLog {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: std::sync::Mutex::new(std::collections::VecDeque::with_capacity(101)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&self, entry: ActivityLogEntry) {
|
||||
if let Ok(mut entries) = self.entries.lock() {
|
||||
if entries.len() >= 100 {
|
||||
entries.pop_front();
|
||||
}
|
||||
entries.push_back(entry);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> Vec<ActivityLogEntry> {
|
||||
self.entries
|
||||
.lock()
|
||||
.map(|e| e.iter().cloned().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedActivityLogMap = Arc<RwLock<HashMap<String, Arc<ActivityLog>>>>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct AppliedTarget {
|
||||
bookmark_id: String,
|
||||
@@ -692,6 +784,7 @@ impl SchedulerControlManager {
|
||||
|
||||
pub type SharedSchedulerControlManager = Arc<SchedulerControlManager>;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spawn_scheduler_task(
|
||||
context: Arc<FrontendRuntimeContext>,
|
||||
rig_tx: mpsc::Sender<RigRequest>,
|
||||
@@ -700,6 +793,7 @@ pub fn spawn_scheduler_task(
|
||||
status_map: SchedulerStatusMap,
|
||||
control: SharedSchedulerControlManager,
|
||||
recorder_mgr: Option<Arc<super::recorder::RecorderManager>>,
|
||||
log_map: SharedActivityLogMap,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = time::interval(Duration::from_secs(30));
|
||||
@@ -772,6 +866,7 @@ pub fn spawn_scheduler_task(
|
||||
sat_target.center_hz,
|
||||
&sat_target.extra_bm_ids,
|
||||
Some(&sat_target.satellite),
|
||||
&log_map,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -882,6 +977,7 @@ pub fn spawn_scheduler_task(
|
||||
center_hz,
|
||||
&extra_bm_ids,
|
||||
None,
|
||||
&log_map,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1060,6 +1156,7 @@ async fn apply_last_scheduler_cycle(
|
||||
remote: &str,
|
||||
status_map: &SchedulerStatusMap,
|
||||
bookmarks: &BookmarkStoreMap,
|
||||
log_map: &SharedActivityLogMap,
|
||||
) {
|
||||
let status = {
|
||||
let Ok(map) = status_map.read() else {
|
||||
@@ -1092,6 +1189,7 @@ async fn apply_last_scheduler_cycle(
|
||||
status.last_center_hz,
|
||||
&status.last_bookmark_ids,
|
||||
status.active_satellite.as_deref(),
|
||||
log_map,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1275,6 +1373,7 @@ pub async fn put_scheduler_activate_entry(
|
||||
status_map: web::Data<SchedulerStatusMap>,
|
||||
bookmarks: web::Data<Arc<BookmarkStoreMap>>,
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
log_map: web::Data<SharedActivityLogMap>,
|
||||
) -> impl Responder {
|
||||
let rig_id = path.into_inner();
|
||||
let Some(config) = store_map.store_for(&rig_id).get_config() else {
|
||||
@@ -1302,6 +1401,7 @@ pub async fn put_scheduler_activate_entry(
|
||||
entry.center_hz,
|
||||
&entry.bookmark_ids,
|
||||
None,
|
||||
log_map.get_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1338,6 +1438,7 @@ pub async fn put_scheduler_control(
|
||||
rig_tx: web::Data<mpsc::Sender<RigRequest>>,
|
||||
status_map: web::Data<SchedulerStatusMap>,
|
||||
bookmarks: web::Data<Arc<BookmarkStoreMap>>,
|
||||
log_map: web::Data<SharedActivityLogMap>,
|
||||
) -> impl Responder {
|
||||
let body = body.into_inner();
|
||||
let summary = control.set_released(body.session_id, body.released);
|
||||
@@ -1348,6 +1449,7 @@ pub async fn put_scheduler_control(
|
||||
remote,
|
||||
status_map.get_ref(),
|
||||
bookmarks.get_ref().as_ref(),
|
||||
log_map.get_ref(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -1355,6 +1457,22 @@ pub async fn put_scheduler_control(
|
||||
HttpResponse::Ok().json(summary)
|
||||
}
|
||||
|
||||
/// GET /scheduler/{remote}/log
|
||||
#[get("/scheduler/{remote}/log")]
|
||||
pub async fn get_scheduler_log(
|
||||
path: web::Path<String>,
|
||||
log_map: web::Data<SharedActivityLogMap>,
|
||||
) -> impl Responder {
|
||||
let remote = path.into_inner();
|
||||
let entries = {
|
||||
let map = log_map.read().unwrap_or_else(|e| e.into_inner());
|
||||
map.get(&remote)
|
||||
.map(|log| log.entries())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
HttpResponse::Ok().json(entries)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
|
||||
@@ -43,7 +43,9 @@ use trx_frontend::{FrontendRuntimeContext, FrontendSpawner};
|
||||
use auth::{AuthConfig, AuthState, SameSite};
|
||||
use background_decode::{BackgroundDecodeManager, BackgroundDecodeStore};
|
||||
use recorder::{RecorderConfig, RecorderManager};
|
||||
use scheduler::{SchedulerControlManager, SchedulerStatusMap, SchedulerStoreMap};
|
||||
use scheduler::{
|
||||
SchedulerControlManager, SchedulerStatusMap, SchedulerStoreMap, SharedActivityLogMap,
|
||||
};
|
||||
use vchan::ClientChannelManager;
|
||||
|
||||
/// HTTP frontend implementation.
|
||||
@@ -88,6 +90,7 @@ async fn serve(
|
||||
let bookmark_store_map = Arc::new(bookmarks::BookmarkStoreMap::new());
|
||||
let scheduler_status: SchedulerStatusMap = Arc::new(RwLock::new(HashMap::new()));
|
||||
let scheduler_control = Arc::new(SchedulerControlManager::default());
|
||||
let activity_log_map: SharedActivityLogMap = Arc::new(RwLock::new(HashMap::new()));
|
||||
|
||||
let recorder_config = RecorderConfig::default();
|
||||
let recorder_mgr = Arc::new(RecorderManager::new(recorder_config));
|
||||
@@ -100,6 +103,7 @@ async fn serve(
|
||||
scheduler_status.clone(),
|
||||
scheduler_control.clone(),
|
||||
Some(recorder_mgr.clone()),
|
||||
activity_log_map.clone(),
|
||||
);
|
||||
|
||||
let background_decode_path = BackgroundDecodeStore::default_path();
|
||||
@@ -155,6 +159,7 @@ async fn serve(
|
||||
scheduler_store,
|
||||
scheduler_status,
|
||||
scheduler_control,
|
||||
activity_log_map,
|
||||
vchan_mgr,
|
||||
session_rig_mgr,
|
||||
background_decode_mgr,
|
||||
@@ -182,6 +187,7 @@ fn build_server(
|
||||
scheduler_store: Arc<SchedulerStoreMap>,
|
||||
scheduler_status: SchedulerStatusMap,
|
||||
scheduler_control: Arc<SchedulerControlManager>,
|
||||
activity_log_map: SharedActivityLogMap,
|
||||
vchan_mgr: Arc<ClientChannelManager>,
|
||||
session_rig_mgr: Arc<api::SessionRigManager>,
|
||||
background_decode_mgr: Arc<BackgroundDecodeManager>,
|
||||
@@ -198,6 +204,7 @@ fn build_server(
|
||||
let scheduler_store = web::Data::new(scheduler_store);
|
||||
let scheduler_status = web::Data::new(scheduler_status);
|
||||
let scheduler_control = web::Data::new(scheduler_control);
|
||||
let activity_log_map = web::Data::new(activity_log_map);
|
||||
let vchan_mgr = web::Data::new(vchan_mgr);
|
||||
let session_rig_mgr = web::Data::new(session_rig_mgr);
|
||||
let background_decode_mgr = web::Data::new(background_decode_mgr);
|
||||
@@ -255,6 +262,7 @@ fn build_server(
|
||||
.app_data(scheduler_store.clone())
|
||||
.app_data(scheduler_status.clone())
|
||||
.app_data(scheduler_control.clone())
|
||||
.app_data(activity_log_map.clone())
|
||||
.app_data(vchan_mgr.clone())
|
||||
.app_data(session_rig_mgr.clone())
|
||||
.app_data(background_decode_mgr.clone())
|
||||
|
||||
Reference in New Issue
Block a user