5 Commits

Author SHA1 Message Date
dxd
1acbb569dd Merge pull request 'feat(wavelog): link to the Wavelog instance next to the API key field' (#3) from feat/wavelog-instance-link into main
Reviewed-on: #3
2026-10-22 21:46:20 +02:00
ARCG Admin (Claude)
9862f751a2 feat(wavelog): link to the Wavelog instance next to the API key field
Show an "Open Wavelog" link beside the Wavelog API Key heading in the
OpenWebRX panel. The URL comes from the runtime WAVELOG_BASE_URL (exposed
via GET /v1/me and the OpenWebRX session response), so the private hostname
is never hardcoded in this public repo. The link is hidden when no base URL
is configured.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-22 21:21:27 +02:00
dxd
3387ded386 Merge pull request 'fix(wavelog): push live tuned frequency, not just band center' (#2) from fix/wavelog-cat-live-frequency into main
Reviewed-on: #2
2026-10-22 21:17:36 +02:00
ARCG Admin (Claude)
061045999b fix(wavelog): push live tuned frequency, not just band center
The CAT push only fired on band select (band center). Fine-tuning inside
OpenWebRX did not update Wavelog. Hook the per-operator live-state updates
(/v1/openwebrx/plugin/live-state) so the actual tuned frequency/mode is
pushed, trailing-debounced (WAVELOG_MIN_INTERVAL_MS, default 3000 ms) so
tuning does not flood the Wavelog API. Band-select push kept as immediate
feedback on band change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-22 21:16:17 +02:00
dxd
4ced7418cd Merge pull request 'feat(wavelog): push station frequency/mode to Wavelog per operator' (#1) from feat/wavelog-cat-frequency into main
Reviewed-on: #1
Reviewed-by: dxd <oe6dxd@arcg.at>
2026-10-22 20:53:24 +02:00
7 changed files with 108 additions and 13 deletions

View File

@@ -93,3 +93,5 @@ WAVELOG_BASE_URL=
WAVELOG_RADIO_NAME=RMS Remote WAVELOG_RADIO_NAME=RMS Remote
# HTTP timeout for the Wavelog API call (ms). # HTTP timeout for the Wavelog API call (ms).
WAVELOG_TIMEOUT_MS=8000 WAVELOG_TIMEOUT_MS=8000
# Min interval between live-tuning pushes per operator (ms, trailing-debounced).
WAVELOG_MIN_INTERVAL_MS=3000

View File

@@ -9,9 +9,13 @@
- `baseUrl` / `WAVELOG_BASE_URL` – URL der Wavelog-Instanz (ohne Slash am Ende). Leer = deaktiviert. - `baseUrl` / `WAVELOG_BASE_URL` – URL der Wavelog-Instanz (ohne Slash am Ende). Leer = deaktiviert.
- `radioName` / `WAVELOG_RADIO_NAME` – Anzeigename in Wavelog (Default `RMS Remote`). - `radioName` / `WAVELOG_RADIO_NAME` – Anzeigename in Wavelog (Default `RMS Remote`).
- `timeoutMs` / `WAVELOG_TIMEOUT_MS` – HTTP-Timeout (Default 8000). - `timeoutMs` / `WAVELOG_TIMEOUT_MS` – HTTP-Timeout (Default 8000).
- Ausloeser: Der Core ruft die Capability beim Bandwechsel (`/v1/openwebrx/bands/select`) auf und - Ausloeser:
uebergibt `centerFreqHz` + `startMod` sowie den Wavelog-Key des aktiven Operators. Best-effort: - **Bandwechsel** (`/v1/openwebrx/bands/select`): sofortiger Push der Band-Mittenfrequenz
Fehler brechen den Bandwechsel nie ab. (`centerFreqHz` + `startMod`).
- **Live-Tuning in OpenWebRX** (`/v1/openwebrx/plugin/live-state`): Push der tatsaechlich getunten
Frequenz/Mode des aktiven Operators, **gedrosselt** (Trailing-Debounce, `WAVELOG_MIN_INTERVAL_MS`,
Default 3000 ms) damit Feintunen die API nicht flutet.
- Beides best-effort: Fehler brechen den jeweiligen Flow nie ab.
- Key-Ablage: pro Benutzer in `users.json` (`wavelogApiKey`), setzbar via `PUT /v1/me/wavelog-key` - Key-Ablage: pro Benutzer in `users.json` (`wavelogApiKey`), setzbar via `PUT /v1/me/wavelog-key`
und sichtbar im OpenWebRX-Panel. **Kein** Key im Repo – der Key ist Laufzeit-/Benutzerdatum. und sichtbar im OpenWebRX-Panel. **Kein** Key im Repo – der Key ist Laufzeit-/Benutzerdatum.
- Mode-Mapping: `usb|lsb|ssb → SSB`, `cw|cwr → CW`, `fm|nfm|wfm → FM`, `am|sam → AM`, sonst Rohwert - Mode-Mapping: `usb|lsb|ssb → SSB`, `cw|cwr → CW`, `fm|nfm|wfm → FM`, `am|sam → AM`, sonst Rohwert

View File

@@ -138,6 +138,7 @@ const els = {
wavelogKeyInput: document.getElementById("wavelogKeyInput"), wavelogKeyInput: document.getElementById("wavelogKeyInput"),
wavelogKeySaveBtn: document.getElementById("wavelogKeySaveBtn"), wavelogKeySaveBtn: document.getElementById("wavelogKeySaveBtn"),
wavelogKeyMessage: document.getElementById("wavelogKeyMessage"), wavelogKeyMessage: document.getElementById("wavelogKeyMessage"),
wavelogInstanceLink: document.getElementById("wavelogInstanceLink"),
openwebrxTxStatePill: document.getElementById("openwebrxTxStatePill"), openwebrxTxStatePill: document.getElementById("openwebrxTxStatePill"),
rotorCurrent: document.getElementById("rotorCurrent"), rotorCurrent: document.getElementById("rotorCurrent"),
rotorCompass: document.getElementById("rotorCompass"), rotorCompass: document.getElementById("rotorCompass"),
@@ -1505,6 +1506,7 @@ async function refreshCurrentUser() {
const result = await api("/v1/me", { authRequired: true }); const result = await api("/v1/me", { authRequired: true });
state.user = result.user; state.user = result.user;
state.wavelogApiKey = typeof result.wavelogApiKey === "string" ? result.wavelogApiKey : ""; state.wavelogApiKey = typeof result.wavelogApiKey === "string" ? result.wavelogApiKey : "";
state.wavelogBaseUrl = typeof result.wavelogBaseUrl === "string" ? result.wavelogBaseUrl : "";
applyWavelogKeyToInput(); applyWavelogKeyToInput();
renderUserSettingsAuthMethods(); renderUserSettingsAuthMethods();
await ensureLanguageFromUserPreference(); await ensureLanguageFromUserPreference();
@@ -1698,6 +1700,16 @@ function applyWavelogKeyToInput() {
if (els.wavelogKeyInput) { if (els.wavelogKeyInput) {
els.wavelogKeyInput.value = String(state.wavelogApiKey || ""); els.wavelogKeyInput.value = String(state.wavelogApiKey || "");
} }
if (els.wavelogInstanceLink) {
const url = String(state.wavelogBaseUrl || "").trim();
if (url) {
els.wavelogInstanceLink.href = url;
els.wavelogInstanceLink.hidden = false;
} else {
els.wavelogInstanceLink.removeAttribute("href");
els.wavelogInstanceLink.hidden = true;
}
}
} }
async function saveWavelogKey() { async function saveWavelogKey() {
@@ -1723,6 +1735,9 @@ async function openOpenWebRxSession() {
const session = result && result.session ? result.session : null; const session = result && result.session ? result.session : null;
if (result && typeof result.wavelogApiKey === "string") { if (result && typeof result.wavelogApiKey === "string") {
state.wavelogApiKey = result.wavelogApiKey; state.wavelogApiKey = result.wavelogApiKey;
if (typeof result.wavelogBaseUrl === "string") {
state.wavelogBaseUrl = result.wavelogBaseUrl;
}
applyWavelogKeyToInput(); applyWavelogKeyToInput();
} }
const sessionTicket = session && session.ticket ? String(session.ticket) : ""; const sessionTicket = session && session.ticket ? String(session.ticket) : "";
@@ -1752,6 +1767,9 @@ async function openOpenWebRxExternal() {
const session = result && result.session ? result.session : null; const session = result && result.session ? result.session : null;
if (result && typeof result.wavelogApiKey === "string") { if (result && typeof result.wavelogApiKey === "string") {
state.wavelogApiKey = result.wavelogApiKey; state.wavelogApiKey = result.wavelogApiKey;
if (typeof result.wavelogBaseUrl === "string") {
state.wavelogBaseUrl = result.wavelogBaseUrl;
}
applyWavelogKeyToInput(); applyWavelogKeyToInput();
} }
const sessionTicket = session && session.ticket ? String(session.ticket) : ""; const sessionTicket = session && session.ticket ? String(session.ticket) : "";

View File

@@ -56,6 +56,7 @@
"Key speichern": "Key speichern", "Key speichern": "Key speichern",
"Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.", "Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.",
"Wavelog API Key gespeichert.": "Wavelog API Key gespeichert.", "Wavelog API Key gespeichert.": "Wavelog API Key gespeichert.",
"Wavelog öffnen": "Wavelog öffnen",
"Reservierungen": "Reservierungen", "Reservierungen": "Reservierungen",
"Naechsten Slot reservieren": "Naechsten Slot reservieren", "Naechsten Slot reservieren": "Naechsten Slot reservieren",
"SWR Uebersicht": "SWR Uebersicht", "SWR Uebersicht": "SWR Uebersicht",

View File

@@ -56,6 +56,7 @@
"Key speichern": "Save key", "Key speichern": "Save key",
"Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "The station's frequency and mode are sent to your Wavelog logbook on band change.", "Frequenz und Mode der Station werden beim Bandwechsel an dein Wavelog-Logbuch gesendet.": "The station's frequency and mode are sent to your Wavelog logbook on band change.",
"Wavelog API Key gespeichert.": "Wavelog API key saved.", "Wavelog API Key gespeichert.": "Wavelog API key saved.",
"Wavelog öffnen": "Open Wavelog",
"Reservierungen": "Reservations", "Reservierungen": "Reservations",
"Naechsten Slot reservieren": "Reserve next slot", "Naechsten Slot reservieren": "Reserve next slot",
"SWR Uebersicht": "SWR Overview", "SWR Uebersicht": "SWR Overview",

View File

@@ -167,7 +167,10 @@
<p class="muted">Session Key (Ticket): <code id="openwebrxSessionTicket">-</code></p> <p class="muted">Session Key (Ticket): <code id="openwebrxSessionTicket">-</code></p>
</div> </div>
<div id="wavelogKeyBox" class="stack"> <div id="wavelogKeyBox" class="stack">
<label for="wavelogKeyInput">Wavelog API Key (Rig-Key)</label> <div class="section-head">
<label for="wavelogKeyInput">Wavelog API Key (Rig-Key)</label>
<a id="wavelogInstanceLink" target="_blank" rel="noopener noreferrer" hidden>Wavelog öffnen</a>
</div>
<input id="wavelogKeyInput" type="text" autocomplete="off" spellcheck="false" placeholder="Wavelog API Key" /> <input id="wavelogKeyInput" type="text" autocomplete="off" spellcheck="false" placeholder="Wavelog API Key" />
<div class="actions"> <div class="actions">
<button id="wavelogKeySaveBtn" type="button" class="ghost-btn">Key speichern</button> <button id="wavelogKeySaveBtn" type="button" class="ghost-btn">Key speichern</button>

View File

@@ -44,6 +44,7 @@ const config = {
actionRateLimit: Number(process.env.ACTION_RATE_LIMIT || 30), actionRateLimit: Number(process.env.ACTION_RATE_LIMIT || 30),
openWebRxPttLiveFreqToleranceHz: Number(process.env.OPENWEBRX_PTT_LIVE_FREQ_TOLERANCE_HZ || 50), openWebRxPttLiveFreqToleranceHz: Number(process.env.OPENWEBRX_PTT_LIVE_FREQ_TOLERANCE_HZ || 50),
openWebRxLiveStateTtlMs: Number(process.env.OPENWEBRX_LIVE_STATE_TTL_MS || 10000), openWebRxLiveStateTtlMs: Number(process.env.OPENWEBRX_LIVE_STATE_TTL_MS || 10000),
wavelogMinIntervalMs: Number(process.env.WAVELOG_MIN_INTERVAL_MS || 3000),
openWebRxPttBlockedBandConfigIdsRaw: String(process.env.OPENWEBRX_PTT_BLOCKED_BAND_CONFIG_IDS || "27,6,24"), openWebRxPttBlockedBandConfigIdsRaw: String(process.env.OPENWEBRX_PTT_BLOCKED_BAND_CONFIG_IDS || "27,6,24"),
openWebRxTxPollMs: Number(process.env.OPENWEBRX_TX_POLL_MS || 3000), openWebRxTxPollMs: Number(process.env.OPENWEBRX_TX_POLL_MS || 3000),
txAudioEnabled: String(process.env.TX_AUDIO_ENABLED || "true").trim().toLowerCase(), txAudioEnabled: String(process.env.TX_AUDIO_ENABLED || "true").trim().toLowerCase(),
@@ -196,6 +197,7 @@ const runtime = {
lastEnsureSdrAtMs: 0 lastEnsureSdrAtMs: 0
}, },
openWebRxLiveStateByUserId: {}, openWebRxLiveStateByUserId: {},
wavelogCatByUserId: {},
rotor: { rotor: {
azimuth: null, azimuth: null,
rawAzimuth: null, rawAzimuth: null,
@@ -358,7 +360,7 @@ async function routeRequest(req, res) {
if (method === "GET" && url.pathname === "/v1/me") { if (method === "GET" && url.pathname === "/v1/me") {
const auth = requireAuth(req, res); const auth = requireAuth(req, res);
if (!auth) return; if (!auth) return;
return sendJson(res, 200, { user: sanitizeUser(auth.user), wavelogApiKey: auth.user.wavelogApiKey || "", capabilities: userCapabilities(auth.user) }); return sendJson(res, 200, { user: sanitizeUser(auth.user), wavelogApiKey: auth.user.wavelogApiKey || "", wavelogBaseUrl: wavelogBaseUrlPublic(), capabilities: userCapabilities(auth.user) });
} }
if (method === "GET" && url.pathname === "/v1/auth/session/diag") { if (method === "GET" && url.pathname === "/v1/auth/session/diag") {
@@ -3639,7 +3641,8 @@ async function handleOpenWebRxSessionIssue(res, user) {
return sendJson(res, 200, { return sendJson(res, 200, {
ok: true, ok: true,
session: result, session: result,
wavelogApiKey: user.wavelogApiKey || "" wavelogApiKey: user.wavelogApiKey || "",
wavelogBaseUrl: wavelogBaseUrlPublic()
}); });
} }
@@ -3717,9 +3720,15 @@ async function handleOpenWebRxBandSelect(res, user, body) {
} }
} }
// Push current frequency/mode to the active operator's Wavelog logbook (CAT API). // Public Wavelog base URL (for the UI link). Comes from runtime env only, so the
// Best-effort: never throws into the band-select flow. // private hostname is never baked into this (public) repo.
async function maybePublishWavelogCat(user, bandResult) { function wavelogBaseUrlPublic() {
return String(process.env.WAVELOG_BASE_URL || "").trim().replace(/\/+$/, "");
}
// Push a frequency/mode to the active operator's Wavelog logbook (CAT API).
// Best-effort: never throws into the caller's flow.
async function sendWavelogCat(user, frequencyHz, mode) {
try { try {
if (!user || !user.wavelogApiKey) { if (!user || !user.wavelogApiKey) {
return; return;
@@ -3730,13 +3739,13 @@ async function maybePublishWavelogCat(user, bandResult) {
if (!runtime.pluginState.providers["logging.cat.publish"]) { if (!runtime.pluginState.providers["logging.cat.publish"]) {
return; return;
} }
const frequencyHz = Number(bandResult && bandResult.centerFreqHz); const freq = Number(frequencyHz);
if (!Number.isFinite(frequencyHz) || frequencyHz <= 0) { if (!Number.isFinite(freq) || freq <= 0) {
return; return;
} }
const outcome = await executeCapability("logging.cat.publish", "publish", { const outcome = await executeCapability("logging.cat.publish", "publish", {
frequencyHz, frequencyHz: freq,
mode: bandResult && bandResult.startMod ? bandResult.startMod : null, mode: mode || null,
apiKey: user.wavelogApiKey apiKey: user.wavelogApiKey
}, { user, skipTxSafety: true }); }, { user, skipTxSafety: true });
if (!outcome || outcome.ok !== true) { if (!outcome || outcome.ok !== true) {
@@ -3753,6 +3762,62 @@ async function maybePublishWavelogCat(user, bandResult) {
} }
} }
// Band select: push the band center immediately (best-effort).
async function maybePublishWavelogCat(user, bandResult) {
await sendWavelogCat(user, bandResult && bandResult.centerFreqHz, bandResult && bandResult.startMod ? bandResult.startMod : null);
}
// Live tuning in OpenWebRX: push the actual tuned frequency/mode, throttled so
// fine-tuning doesn't flood the Wavelog API. Trailing-edge: the latest value is
// always sent once the throttle window elapses.
function scheduleWavelogCatLive(user, frequencyHz, mode) {
try {
if (!user || !user.wavelogApiKey || !runtime.pluginState.enabled["rms.wavelog.cat"]) {
return;
}
const freq = Number(frequencyHz);
if (!Number.isFinite(freq) || freq <= 0) {
return;
}
const key = String(user.id || "");
if (!key) {
return;
}
const minIntervalMs = Number.isFinite(config.wavelogMinIntervalMs) ? Math.max(500, config.wavelogMinIntervalMs) : 3000;
const rec = runtime.wavelogCatByUserId[key] || { lastFreq: null, lastMode: null, lastPushMs: 0, timer: null };
runtime.wavelogCatByUserId[key] = rec;
rec.pendingFreq = freq;
rec.pendingMode = mode || null;
rec.pendingUser = user;
if (rec.lastFreq === freq && rec.lastMode === (mode || null)) {
return;
}
const fire = () => {
rec.timer = null;
rec.lastFreq = rec.pendingFreq;
rec.lastMode = rec.pendingMode;
rec.lastPushMs = Date.now();
sendWavelogCat(rec.pendingUser, rec.pendingFreq, rec.pendingMode);
};
const elapsed = Date.now() - (rec.lastPushMs || 0);
if (!rec.timer && elapsed >= minIntervalMs) {
fire();
} else if (!rec.timer) {
rec.timer = setTimeout(fire, Math.max(250, minIntervalMs - elapsed));
if (rec.timer && typeof rec.timer.unref === "function") {
rec.timer.unref();
}
}
// If a timer is already pending, pendingFreq/Mode were updated above so the
// trailing push sends the latest tuned frequency.
} catch (error) {
console.warn("wavelog.cat live schedule error", {
userId: user && user.id,
error: String(error && error.message ? error.message : error)
});
}
}
async function handleOpenWebRxTxEnable(res, user) { async function handleOpenWebRxTxEnable(res, user) {
if (!runtime.station.isInUse || runtime.station.activeByUserId !== user.id) { if (!runtime.station.isInUse || runtime.station.activeByUserId !== user.id) {
return sendError(res, 403, "openwebrx.not_owner", "OpenWebRX TX nur fuer aktiven Stationsbenutzer"); return sendError(res, 403, "openwebrx.not_owner", "OpenWebRX TX nur fuer aktiven Stationsbenutzer");
@@ -5033,6 +5098,7 @@ async function handleOpenWebRxPluginLiveState(req, res, url, body) {
updatedAtMs: Date.now(), updatedAtMs: Date.now(),
source: "plugin" source: "plugin"
}); });
scheduleWavelogCatLive(user, parsed.frequencyHz, parsed.mode);
return sendJson(res, 200, { return sendJson(res, 200, {
ok: true, ok: true,
liveState: { liveState: {