Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56c103140c | ||
|
|
b60e8285a4 | ||
|
|
0f1424335e | ||
|
|
506a1e6e4c |
@@ -6510,6 +6510,21 @@ func (a *App) deleteRemoteCopies(ids []int64) {
|
|||||||
// withdrawals stop. Three is enough to tell a one-off from a blocked account.
|
// withdrawals stop. Three is enough to tell a one-off from a blocked account.
|
||||||
const maxClublogRefusals = 3
|
const maxClublogRefusals = 3
|
||||||
|
|
||||||
|
// Club Log's delete endpoint is a REAL-TIME one: it exists for an operator
|
||||||
|
// removing a contact they just logged wrongly, at human pace. Club Log watches
|
||||||
|
// the rate and blocks the IP of anything that batches through it — they wrote
|
||||||
|
// to this station about 167 requests in four minutes, which was one deletion of
|
||||||
|
// a couple of hundred rows, not a pile-up.
|
||||||
|
//
|
||||||
|
// There is no bulk-delete API to move to, so the only honest answer is to go at
|
||||||
|
// the pace the endpoint is meant for and to stop rather than push a large
|
||||||
|
// deletion through it. Whoever needs to remove hundreds of QSOs from Club Log
|
||||||
|
// does it on their site, where the tool for it exists.
|
||||||
|
const (
|
||||||
|
clublogDeletePace = 1200 * time.Millisecond
|
||||||
|
maxClublogDeletes = 25
|
||||||
|
)
|
||||||
|
|
||||||
// clublogWasUploaded reports whether this QSO ever reached Club Log. "M"
|
// clublogWasUploaded reports whether this QSO ever reached Club Log. "M"
|
||||||
// (modified since upload) counts: the copy is there, it is merely out of date.
|
// (modified since upload) counts: the copy is there, it is merely out of date.
|
||||||
func clublogWasUploaded(status string) bool {
|
func clublogWasUploaded(status string) bool {
|
||||||
@@ -6523,6 +6538,7 @@ func clublogWasUploaded(status string) bool {
|
|||||||
func (a *App) withdrawRemoteCopies(rows []qso.QSO, cfg extsvc.ExternalServices, doQRZ, doClublog bool) {
|
func (a *App) withdrawRemoteCopies(rows []qso.QSO, cfg extsvc.ExternalServices, doQRZ, doClublog bool) {
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
clublogRefused := 0
|
clublogRefused := 0
|
||||||
|
clublogSent := 0
|
||||||
for i := range rows {
|
for i := range rows {
|
||||||
q := rows[i]
|
q := rows[i]
|
||||||
id := q.ID
|
id := q.ID
|
||||||
@@ -6553,6 +6569,17 @@ func (a *App) withdrawRemoteCopies(rows []qso.QSO, cfg extsvc.ExternalServices,
|
|||||||
if doClublog && !clublogWasUploaded(q.ClublogUploadStatus) {
|
if doClublog && !clublogWasUploaded(q.ClublogUploadStatus) {
|
||||||
applog.Printf("extsvc: QSO %d (%s) was never uploaded to Club Log — nothing to withdraw", id, q.Callsign)
|
applog.Printf("extsvc: QSO %d (%s) was never uploaded to Club Log — nothing to withdraw", id, q.Callsign)
|
||||||
} else if doClublog {
|
} else if doClublog {
|
||||||
|
if clublogSent >= maxClublogDeletes {
|
||||||
|
applog.Printf("extsvc: %d QSOs already withdrawn from Club Log — stopping there. Their delete endpoint is for one contact at a time; remove the rest on clublog.org, which has a tool for it.", clublogSent)
|
||||||
|
doClublog = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if clublogSent > 0 {
|
||||||
|
// Paced deliberately: see clublogDeletePace. This runs in the
|
||||||
|
// background, so the wait costs the operator nothing.
|
||||||
|
time.Sleep(clublogDeletePace)
|
||||||
|
}
|
||||||
|
clublogSent++
|
||||||
if msg, err := extsvc.DeleteClublog(a.ctx, nil, cfg.Clublog, q.Callsign, q.QSODate, q.Band); err != nil {
|
if msg, err := extsvc.DeleteClublog(a.ctx, nil, cfg.Clublog, q.Callsign, q.QSODate, q.Band); err != nil {
|
||||||
clublogRefused++
|
clublogRefused++
|
||||||
applog.Printf("extsvc: Club Log delete of QSO %d (%s) failed: %v", id, q.Callsign, err)
|
applog.Printf("extsvc: Club Log delete of QSO %d (%s) failed: %v", id, q.Callsign, err)
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"version": "0.26.11",
|
||||||
|
"date": "",
|
||||||
|
"en": [
|
||||||
|
"Withdrawing deleted QSOs from Club Log is now paced and capped at 25 per deletion. Their delete endpoint is a real-time one, meant for an operator removing a contact they just mis-logged; Club Log watches the rate and blocks the IP of anything that batches through it. Past the cap OpsLog stops and says so — there is no bulk-delete API, and hundreds of removals belong on clublog.org, which has a tool for it.",
|
||||||
|
"Auto-call, withdrawn earlier, is now disarmed where it was remembered: a stored 'enabled' is switched off and written back the first time OpsLog reads it. The running guard already stopped this build from calling anyone, but the stored flag survived — and any build without that guard would key the transmitter for a feature with no switch left to turn it off."
|
||||||
|
],
|
||||||
|
"fr": [
|
||||||
|
"Le retrait des QSO supprimés chez Club Log est désormais cadencé et limité à 25 par suppression. Leur point d'entrée de suppression est temps réel, prévu pour un opérateur qui retire un contact qu'il vient de mal enregistrer ; Club Log surveille le rythme et bloque l'IP de ce qui passe des lots par là. Au-delà de la limite, OpsLog s'arrête et le dit — il n'existe pas d'API de suppression en masse, et des centaines de retraits se font sur clublog.org, qui a l'outil pour ça.",
|
||||||
|
"L'appel automatique, retiré précédemment, est maintenant désarmé là où il était mémorisé : un « activé » enregistré est éteint et réécrit dès la première lecture par OpsLog. Le garde-fou à l'exécution empêchait déjà cette version d'appeler qui que ce soit, mais l'indicateur enregistré survivait — et toute version sans ce garde-fou passait à l'émission pour une fonction dont il ne reste aucun interrupteur."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.26.10",
|
"version": "0.26.10",
|
||||||
"date": "",
|
"date": "",
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { writeUiPref } from '@/lib/uiPref';
|
import { writeUiPref } from '@/lib/uiPref';
|
||||||
import { loadAutoCall, autoCallKey, type AutoCallSettings, type AutoCallCriteria } from '@/lib/autocall';
|
|
||||||
import { getDateFormat, setDateFormat, type DateFormat } from '@/lib/dateFormat';
|
import { getDateFormat, setDateFormat, type DateFormat } from '@/lib/dateFormat';
|
||||||
import { useI18n, FlagGB, FlagFR, type Lang } from '@/lib/i18n';
|
import { useI18n, FlagGB, FlagFR, type Lang } from '@/lib/i18n';
|
||||||
import { useTheme, CONCRETE_THEMES, type ThemeChoice } from '@/lib/theme';
|
import { useTheme, CONCRETE_THEMES, type ThemeChoice } from '@/lib/theme';
|
||||||
@@ -1933,7 +1932,6 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
|||||||
// feed up or down — so the write has to go where those live.
|
// feed up or down — so the write has to go where those live.
|
||||||
const [bandOpen, setBandOpen] = useState<any>({ enabled: false, bands: [], available: [] });
|
const [bandOpen, setBandOpen] = useState<any>({ enabled: false, bands: [], available: [] });
|
||||||
// How a worked square is matched, and what counts as still wanted.
|
// How a worked square is matched, and what counts as still wanted.
|
||||||
const [autoCall, setAutoCall] = useState<AutoCallSettings>(loadAutoCall);
|
|
||||||
const [gridScope, setGridScope] = useState<any>({ scope: 'mix_digi', hunt: 'new', scopes: [] });
|
const [gridScope, setGridScope] = useState<any>({ scope: 'mix_digi', hunt: 'new', scopes: [] });
|
||||||
useEffect(() => { GetGridScopeSettings().then((g) => setGridScope(g as any)).catch(() => {}); }, []);
|
useEffect(() => { GetGridScopeSettings().then((g) => setGridScope(g as any)).catch(() => {}); }, []);
|
||||||
const saveGridScope = async (next: any) => {
|
const saveGridScope = async (next: any) => {
|
||||||
|
|||||||
@@ -72,13 +72,26 @@ export function loadAutoCall(): AutoCallSettings {
|
|||||||
const raw = localStorage.getItem(AC_KEY);
|
const raw = localStorage.getItem(AC_KEY);
|
||||||
if (!raw) return { ...defaultAutoCall };
|
if (!raw) return { ...defaultAutoCall };
|
||||||
const v = JSON.parse(raw);
|
const v = JSON.parse(raw);
|
||||||
return {
|
const out: AutoCallSettings = {
|
||||||
...defaultAutoCall,
|
...defaultAutoCall,
|
||||||
...v,
|
...v,
|
||||||
criteria: { ...emptyCriteria, ...(v?.criteria ?? {}) },
|
criteria: { ...emptyCriteria, ...(v?.criteria ?? {}) },
|
||||||
watchCriteria: { ...emptyCriteria, ...(v?.watchCriteria ?? {}) },
|
watchCriteria: { ...emptyCriteria, ...(v?.watchCriteria ?? {}) },
|
||||||
watch: Array.isArray(v?.watch) ? v.watch : [],
|
watch: Array.isArray(v?.watch) ? v.watch : [],
|
||||||
};
|
};
|
||||||
|
// DISARMED ON SIGHT, and written back disabled.
|
||||||
|
//
|
||||||
|
// The runtime guard in App.tsx stops this build from calling anyone, but it
|
||||||
|
// leaves "enabled": true sitting in storage, where any build without the
|
||||||
|
// guard — an older one an operator reinstalls, a machine that upgrades
|
||||||
|
// later — reads it and keys the transmitter for a feature with no switch
|
||||||
|
// left to turn off. A withdrawn feature that keys a radio has to be
|
||||||
|
// disarmed where it is REMEMBERED, not only where it runs.
|
||||||
|
if (out.enabled) {
|
||||||
|
out.enabled = false;
|
||||||
|
try { localStorage.setItem(AC_KEY, JSON.stringify(out)); } catch { /* private mode: the guard still holds */ }
|
||||||
|
}
|
||||||
|
return out;
|
||||||
} catch { return { ...defaultAutoCall }; }
|
} catch { return { ...defaultAutoCall }; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Single source of truth for the app version shown in the UI (header + About).
|
// Single source of truth for the app version shown in the UI (header + About).
|
||||||
// Bump this on a release (the release script updates it alongside telemetry.go).
|
// Bump this on a release (the release script updates it alongside telemetry.go).
|
||||||
export const APP_VERSION = '0.26.10';
|
export const APP_VERSION = '0.26.11';
|
||||||
|
|
||||||
// Author / credits, shown in Help -> About.
|
// Author / credits, shown in Help -> About.
|
||||||
export const APP_AUTHOR = 'F4BPO';
|
export const APP_AUTHOR = 'F4BPO';
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
// appVersion is stamped on every heartbeat (and could feed the About box).
|
// appVersion is stamped on every heartbeat (and could feed the About box).
|
||||||
appVersion = "0.26.10"
|
appVersion = "0.26.11"
|
||||||
|
|
||||||
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
|
||||||
// to https://us.i.posthog.com for a US project.
|
// to https://us.i.posthog.com for a US project.
|
||||||
|
|||||||
Reference in New Issue
Block a user