feat(bandopen): a blinking badge that lasts as long as the opening does

The only sign of a detection was a toast. It is gone in seconds, and an operator
who was tuning at that moment had no way back to it — for an event that lasts
hours and happens a handful of times a season, that is the wrong shape entirely.

The badge sits at the right-hand end of the status bar, before the clock: an
opening is a state of the WORLD, not of this station, so it belongs with the
time and the logbook rather than among the rig and amplifier chips.

Knowing when to go out was the real work. Nothing announces that an opening
ended, and the detector deliberately says nothing more about a band for 45
minutes after announcing it — right for a message, useless for a badge. So it is
inferred: every qualifying spot on that band pushes a deadline out, and when
they stop arriving the badge fades by itself. Fifteen minutes, comfortably more
than the detector's own twelve-minute window, so a quiet couple of minutes
mid-opening does not blink it off and on again.

Gated on the same distance floor the detector uses. Without that, a band busy
with short-range tropo would hold an Es badge lit indefinitely on spots the
detector itself had refused.
This commit is contained in:
2026-08-11 13:48:26 +02:00
parent 0550ecdac3
commit 96c99f0ae6
7 changed files with 123 additions and 13 deletions
+3 -3
View File
@@ -156,10 +156,10 @@ func (a *App) feedBandOpen(s pskr.Spot) {
Call: s.Call, Band: s.Band, DistKm: s.DistKm, Bearing: s.Bearing, At: s.At, Call: s.Call, Band: s.Band, DistKm: s.DistKm, Bearing: s.Bearing, At: s.At,
}, a.opLat) }, a.opLat)
if op != nil { if op != nil {
a.bandOpen.last = append([]bandopen.Opening{*op}, a.bandOpen.last...) a.rememberOpening(*op)
if len(a.bandOpen.last) > maxRememberedOpenings {
a.bandOpen.last = a.bandOpen.last[:maxRememberedOpenings]
} }
if s.DistKm >= bandopen.DefaultConfig().MinKm {
a.markBandAlive(s.Band, s.At)
} }
a.bandOpen.mu.Unlock() a.bandOpen.mu.Unlock()
if op != nil { if op != nil {
+71 -3
View File
@@ -11,6 +11,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
"time"
"hamlog/internal/applog" "hamlog/internal/applog"
"hamlog/internal/bandopen" "hamlog/internal/bandopen"
@@ -23,8 +24,23 @@ type bandOpenState struct {
mu sync.Mutex mu sync.Mutex
det *bandopen.Detector det *bandopen.Detector
last []bandopen.Opening // most recent first, for the UI last []bandopen.Opening // most recent first, for the UI
// live holds the announced openings that are still going, keyed by band, and
// aliveUntil says when each stops counting as current.
//
// The detector announces an opening ONCE and then goes quiet for 45 minutes,
// which is right for a message but useless for a badge that has to stay lit
// while the band is open and go out when it closes. Nothing tells us an
// opening ended, so it is inferred: every qualifying spot on that band pushes
// the deadline out, and when they stop arriving the badge fades by itself.
live map[string]bandopen.Opening
aliveUntil map[string]time.Time
} }
// openingIdle is how long a band may go without a qualifying spot before its
// badge goes out. Longer than the detector's own 12-minute window, so a quiet
// couple of minutes mid-opening does not blink the badge off and on again.
const openingIdle = 15 * time.Minute
const maxRememberedOpenings = 20 const maxRememberedOpenings = 20
// detectBandOpening feeds one spot to the detector and announces a hit. // detectBandOpening feeds one spot to the detector and announces a hit.
@@ -43,10 +59,12 @@ func (a *App) detectBandOpening(s cluster.Spot) {
Bearing: s.ShortPath, At: s.ReceivedAt, Bearing: s.ShortPath, At: s.ReceivedAt,
}, a.opLat) }, a.opLat)
if op != nil { if op != nil {
a.bandOpen.last = append([]bandopen.Opening{*op}, a.bandOpen.last...) a.rememberOpening(*op)
if len(a.bandOpen.last) > maxRememberedOpenings {
a.bandOpen.last = a.bandOpen.last[:maxRememberedOpenings]
} }
// Keeps a lit badge lit. Gated on the same floor the detector uses, or a
// band busy with short-range tropo would hold an Es badge on for ever.
if s.DistanceKm >= bandopen.DefaultConfig().MinKm {
a.markBandAlive(s.Band, s.ReceivedAt)
} }
a.bandOpen.mu.Unlock() a.bandOpen.mu.Unlock()
if op != nil { if op != nil {
@@ -54,6 +72,56 @@ func (a *App) detectBandOpening(s cluster.Spot) {
} }
} }
// markBandAlive pushes a band's badge deadline out. Called for every spot the
// detector accepted, from either feed. Cheap on purpose: this runs on the MQTT
// goroutine at thousands a minute when 6 m is open.
//
// Caller holds bandOpen.mu.
func (a *App) markBandAlive(band string, at time.Time) {
if _, lit := a.bandOpen.live[strings.ToLower(band)]; !lit {
return // nothing announced for this band, nothing to keep alive
}
if a.bandOpen.aliveUntil == nil {
a.bandOpen.aliveUntil = map[string]time.Time{}
}
a.bandOpen.aliveUntil[strings.ToLower(band)] = at.Add(openingIdle)
}
// rememberOpening files a detection and lights its badge. Caller holds the mutex.
func (a *App) rememberOpening(op bandopen.Opening) {
a.bandOpen.last = append([]bandopen.Opening{op}, a.bandOpen.last...)
if len(a.bandOpen.last) > maxRememberedOpenings {
a.bandOpen.last = a.bandOpen.last[:maxRememberedOpenings]
}
if a.bandOpen.live == nil {
a.bandOpen.live = map[string]bandopen.Opening{}
a.bandOpen.aliveUntil = map[string]time.Time{}
}
b := strings.ToLower(op.Band)
a.bandOpen.live[b] = op
a.bandOpen.aliveUntil[b] = op.At.Add(openingIdle)
}
// GetLiveOpenings returns the openings still under way, for the status-bar
// badge. Expired ones are dropped as they are noticed — there is no janitor for
// something that holds at most five entries.
func (a *App) GetLiveOpenings() []bandopen.Opening {
now := time.Now()
a.bandOpen.mu.Lock()
defer a.bandOpen.mu.Unlock()
out := make([]bandopen.Opening, 0, len(a.bandOpen.live))
for b, op := range a.bandOpen.live {
if until, ok := a.bandOpen.aliveUntil[b]; !ok || now.After(until) {
delete(a.bandOpen.live, b)
delete(a.bandOpen.aliveUntil, b)
applog.Printf("bandopen: %s opening has gone quiet", strings.ToUpper(b))
continue
}
out = append(out, op)
}
return out
}
// announceOpening logs and pushes one detection. Shared by both feeds — the // announceOpening logs and pushes one detection. Shared by both feeds — the
// cluster path here and the PSK Reporter path in bandopen_sources.go — so an // cluster path here and the PSK Reporter path in bandopen_sources.go — so an
// opening reads the same however it was noticed. // opening reads the same however it was noticed.
+2 -2
View File
@@ -9,7 +9,7 @@
"Web publishing now offers every field a QSO carries, awards included — 123 instead of 23 — from a searchable dropdown, with the chosen columns listed above it in publication order. Choose carefully: the page is public and the list includes addresses and e-mail.", "Web publishing now offers every field a QSO carries, awards included — 123 instead of 23 — from a searchable dropdown, with the chosen columns listed above it in publication order. Choose carefully: the page is public and the list includes addresses and e-mail.",
"Band-opening detection no longer ignores long paths. It capped them at 2400 km on the assumption that anything further was not a single hop; multi-hop sporadic E is ordinary on 6 m, so the openings most worth hearing about were the ones being discarded. Direction still decides — a second hop leaves the sector the first one entered.", "Band-opening detection no longer ignores long paths. It capped them at 2400 km on the assumption that anything further was not a single hop; multi-hop sporadic E is ordinary on 6 m, so the openings most worth hearing about were the ones being discarded. Direction still decides — a second hop leaves the sector the first one entered.",
"Band-opening detection now has the data it needs. Switching on \"Watch for band openings\" (Settings DX Cluster) subscribes to the PSK Reporter feed — every station on the air reporting what it decodes, rather than the handful of VHF spots a cluster carries — and adds the two RBN nodes if they are missing. Pick the bands to watch: 12, 10, 6, 4 and 2 m. An MQTT chip in the status bar, beside the rig and amplifier, shows the feed is alive.", "Band-opening detection now has the data it needs. Switching on \"Watch for band openings\" (Settings DX Cluster) subscribes to the PSK Reporter feed — every station on the air reporting what it decodes, rather than the handful of VHF spots a cluster carries — and adds the two RBN nodes if they are missing. Pick the bands to watch: 12, 10, 6, 4 and 2 m. An MQTT chip in the status bar, beside the rig and amplifier, shows the feed is alive.",
"Opening detection now covers 12 and 10 m, not just 6, 4 and 2 m. The feed was already subscribed to them while the detector still threw them away, so a busy 10 m could never be announced.", "Opening detection now covers 12 and 10 m, not just 6, 4 and 2 m. The feed was already subscribed to them while the detector still threw them away, so a busy 10 m could never be announced. An opening under way now shows as a blinking badge in the status bar, beside the clock, and stays there until the band goes quiet — the toast that announced it was gone in seconds, while an opening lasts hours.",
"Test connection now actually tests. Club Log checked that the three fields were not empty and reported success without contacting anyone, so a wrong password looked exactly like a right one; it now signs in for real, through the read-only endpoint so a test can never add a record. LoTW reports its two credentials separately: TQSL signs uploads and never uses the website password, so a wrong one used to break nothing until the day confirmations were downloaded." "Test connection now actually tests. Club Log checked that the three fields were not empty and reported success without contacting anyone, so a wrong password looked exactly like a right one; it now signs in for real, through the read-only endpoint so a test can never add a record. LoTW reports its two credentials separately: TQSL signs uploads and never uses the website password, so a wrong one used to break nothing until the day confirmations were downloaded."
], ],
"fr": [ "fr": [
@@ -19,7 +19,7 @@
"La publication web propose désormais tous les champs d un QSO, awards compris — 123 au lieu de 23 — depuis une liste déroulante cherchable, les colonnes choisies étant listées au-dessus dans l ordre de publication. À choisir avec soin : la page est publique et la liste contient adresses et e-mails.", "La publication web propose désormais tous les champs d un QSO, awards compris — 123 au lieu de 23 — depuis une liste déroulante cherchable, les colonnes choisies étant listées au-dessus dans l ordre de publication. À choisir avec soin : la page est publique et la liste contient adresses et e-mails.",
"La détection d ouverture n ignore plus les longues distances. Elle plafonnait à 2400 km en supposant qu au-delà ce n était plus un saut simple ; l Es à sauts multiples est ordinaire sur 6 m, donc les ouvertures les plus intéressantes étaient précisément celles qu on jetait. C est toujours la direction qui tranche — un second saut repart dans le secteur où le premier est arrivé.", "La détection d ouverture n ignore plus les longues distances. Elle plafonnait à 2400 km en supposant qu au-delà ce n était plus un saut simple ; l Es à sauts multiples est ordinaire sur 6 m, donc les ouvertures les plus intéressantes étaient précisément celles qu on jetait. C est toujours la direction qui tranche — un second saut repart dans le secteur où le premier est arrivé.",
"La détection d ouverture dispose enfin des données qu il lui faut. Activer « Surveiller les ouvertures de bande » (Paramètres Cluster DX) souscrit au flux PSK Reporter — toutes les stations en l air qui rapportent ce qu elles décodent, au lieu des quelques spots VHF que porte un cluster — et ajoute les deux nœuds RBN s ils manquent. Les bandes surveillées se choisissent : 12, 10, 6, 4 et 2 m. Une pastille MQTT dans la barre d état, à côté du rig et de l ampli, montre que le flux est vivant.", "La détection d ouverture dispose enfin des données qu il lui faut. Activer « Surveiller les ouvertures de bande » (Paramètres Cluster DX) souscrit au flux PSK Reporter — toutes les stations en l air qui rapportent ce qu elles décodent, au lieu des quelques spots VHF que porte un cluster — et ajoute les deux nœuds RBN s ils manquent. Les bandes surveillées se choisissent : 12, 10, 6, 4 et 2 m. Une pastille MQTT dans la barre d état, à côté du rig et de l ampli, montre que le flux est vivant.",
"La détection d ouverture couvre désormais le 12 et le 10 m, plus seulement le 6, 4 et 2 m. Le flux y était déjà abonné alors que le détecteur les jetait encore, donc un 10 m très actif ne pouvait jamais être annoncé.", "La détection d ouverture couvre désormais le 12 et le 10 m, plus seulement le 6, 4 et 2 m. Le flux y était déjà abonné alors que le détecteur les jetait encore, donc un 10 m très actif ne pouvait jamais être annoncé. Une ouverture en cours s affiche désormais en pastille clignotante dans la barre d état, à côté de l heure, et y reste jusqu à ce que la bande se taise — le toast qui l annonçait disparaissait en quelques secondes, alors qu une ouverture dure des heures.",
"Le bouton Tester la connexion teste vraiment. Club Log vérifiait que les trois champs n étaient pas vides et annonçait la réussite sans contacter personne : un mauvais mot de passe ressemblait exactement à un bon. Il s authentifie maintenant pour de vrai, via le point d accès en lecture seule pour qu un test ne puisse jamais ajouter un enregistrement. LoTW annonce ses deux identifiants séparément : TQSL signe les envois et n utilise jamais le mot de passe du site, donc un mauvais ne cassait rien jusqu au jour du téléchargement des confirmations." "Le bouton Tester la connexion teste vraiment. Club Log vérifiait que les trois champs n étaient pas vides et annonçait la réussite sans contacter personne : un mauvais mot de passe ressemblait exactement à un bon. Il s authentifie maintenant pour de vrai, via le point d accès en lecture seule pour qu un test ne puisse jamais ajouter un enregistrement. LoTW annonce ses deux identifiants séparément : TQSL signe les envois et n utilise jamais le mot de passe du site, donc un mauvais ne cassait rien jusqu au jour du téléchargement des confirmations."
] ]
}, },
+37 -1
View File
@@ -51,7 +51,7 @@ import {
ReportLiveActivity, LiveLastQSOAgeSec, ReportLiveActivity, LiveLastQSOAgeSec,
GetAmpStatuses, AmpOperate, GetAmpStatuses, AmpOperate,
GetFlexState, FlexAmpOperate, GetFlexState, FlexAmpOperate,
GetPSKReporterStatus, GetPSKReporterStatus, GetLiveOpenings,
} from '../wailsjs/go/main/App'; } from '../wailsjs/go/main/App';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { applyAwardRefs } from '@/lib/awardRefs'; import { applyAwardRefs } from '@/lib/awardRefs';
@@ -1710,6 +1710,17 @@ export default function App() {
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
// Re-read the "beam on map" toggle when Preferences closes (it's edited there). // Re-read the "beam on map" toggle when Preferences closes (it's edited there).
useEffect(() => { if (!showSettings) setShowBeamOnMap(localStorage.getItem('opslog.showBeamOnMap') !== '0'); }, [showSettings]); useEffect(() => { if (!showSettings) setShowBeamOnMap(localStorage.getItem('opslog.showBeamOnMap') !== '0'); }, [showSettings]);
// Openings under way, for the blinking status-bar badge. Polled rather than
// event-driven: the badge also has to go OUT when a band goes quiet, and
// nothing emits an event for something that stopped happening.
const [liveOpenings, setLiveOpenings] = useState<any[]>([]);
useEffect(() => {
const load = () => { GetLiveOpenings().then((v: any) => setLiveOpenings(v ?? [])).catch(() => {}); };
load();
const t = window.setInterval(load, 20000);
return () => window.clearInterval(t);
}, []);
// PSK Reporter feed, for the status-bar chip. Polled slowly: the chip only // PSK Reporter feed, for the status-bar chip. Polled slowly: the chip only
// says up or down, and the count behind it is a tooltip. // says up or down, and the count behind it is a tooltip.
const [pskr, setPskr] = useState<any>(null); const [pskr, setPskr] = useState<any>(null);
@@ -6942,6 +6953,31 @@ export default function App() {
{/* UTC clock moved out of the header, where it competed with the {/* UTC clock moved out of the header, where it competed with the
frequency for the eye. It belongs with the other passive frequency for the eye. It belongs with the other passive
indicators. */} indicators. */}
{/* Openings under way. Right-hand end, before the clock, because it is
a state of the world rather than a state of this station the
same side as the time and the logbook.
It BLINKS and it stays. The toast that announces an opening is
gone in seconds, and an operator who was tuning at that moment
had no way back to it; a band opening lasts hours and deserves
something that lasts with it. It goes out by itself when the
spots stop arriving. */}
{liveOpenings.map((o: any) => (
<button
key={o.band}
type="button"
onClick={() => { setActiveTab('bandmap'); }}
title={t('bo.liveTip', {
band: String(o.band).toUpperCase(), n: o.calls, km: o.median_km,
sector: o.sector ?? '', season: o.in_season ? '' : ' — ' + t('bmp.openUnusual'),
})}
className="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 text-[11px] font-bold uppercase tracking-wider shrink-0
border-warning-border bg-warning-muted text-warning-muted-foreground hover:bg-warning-muted/70 animate-pulse"
>
<Radio className="size-3" />
{String(o.band).toUpperCase()} {t('bo.open')}
</button>
))}
<span className="inline-flex items-center gap-1 font-mono text-[11px] text-muted-foreground shrink-0" title="UTC"> <span className="inline-flex items-center gap-1 font-mono text-[11px] text-muted-foreground shrink-0" title="UTC">
<Clock className="size-3" /> <Clock className="size-3" />
{utcNow}<span className="text-[9px]">Z</span> {utcNow}<span className="text-[9px]">Z</span>
+2 -2
View File
@@ -270,7 +270,7 @@ const en: Dict = {
'clu.muteWorkedHint': '(they stay in the list, just quiet — leaves the colour for what is left to do)', 'clu.muteWorkedHint': '(they stay in the list, just quiet — leaves the colour for what is left to do)',
'clu.slotHighlight': 'Colour the stations not worked on this band and mode', 'clu.slotHighlight': 'Colour the stations not worked on this band and mode',
'clu.slotHighlightHint': '(by callsign, whatever the entity status says)', 'clu.slotHighlightHint': '(by callsign, whatever the entity status says)',
'bo.enable': 'Watch for band openings', 'bo.enableHint': '(10, 12, 6, 4 and 2 m. Switching this on adds the two RBN nodes and subscribes to the PSK Reporter feed — the detection needs far more ears than a cluster can give it.)', 'bo.feedUp': 'PSK Reporter feed up — {n} decodes seen', 'bo.feedDown': 'PSK Reporter feed down — needs your station grid, and a moment to connect', 'clu.workedSameSlot': 'Already worked only on the same slot', 'bo.open': 'open', 'bo.liveTip': '{band} is open — {n} stations, ~{km} km, {sector}{season}. Click for the band map.', 'bo.enable': 'Watch for band openings', 'bo.enableHint': '(10, 12, 6, 4 and 2 m. Switching this on adds the two RBN nodes and subscribes to the PSK Reporter feed — the detection needs far more ears than a cluster can give it.)', 'bo.feedUp': 'PSK Reporter feed up — {n} decodes seen', 'bo.feedDown': 'PSK Reporter feed down — needs your station grid, and a moment to connect', 'clu.workedSameSlot': 'Already worked only on the same slot',
'clu.workedSameSlotHint': '— a spot shows "worked" only if you worked that call on the SAME band and mode, not just anywhere. Combines with digital-mode grouping (Settings → General): with it on, a call worked on 20m FT8 also counts as worked for a 20m FT4 spot; with it off, FT8 and FT4 are separate slots.', 'clu.workedSameSlotHint': '— a spot shows "worked" only if you worked that call on the SAME band and mode, not just anywhere. Combines with digital-mode grouping (Settings → General): with it on, a call worked on 20m FT8 also counts as worked for a 20m FT4 spot; with it off, FT8 and FT4 are separate slots.',
// Backup panel // Backup panel
'bk.hintMysql': 'On close (once/day) OpsLog snapshots the local SQLite (config) AND exports the shared MySQL log to ADIF — opslog-log-<date>.adi — so your contacts are protected even though they live on the server. Rotation keeps the last N of each.', 'bk.hintMysql': 'On close (once/day) OpsLog snapshots the local SQLite (config) AND exports the shared MySQL log to ADIF — opslog-log-<date>.adi — so your contacts are protected even though they live on the server. Rotation keeps the last N of each.',
@@ -690,7 +690,7 @@ const fr: Dict = {
'clu.muteWorkedHint': '(elles restent dans la liste, simplement discrètes — la couleur reste pour ce qui est à faire)', 'clu.muteWorkedHint': '(elles restent dans la liste, simplement discrètes — la couleur reste pour ce qui est à faire)',
'clu.slotHighlight': 'Colorer les stations non contactées sur cette bande et ce mode', 'clu.slotHighlight': 'Colorer les stations non contactées sur cette bande et ce mode',
'clu.slotHighlightHint': "(par indicatif, quel que soit le statut de l'entité)", 'clu.slotHighlightHint': "(par indicatif, quel que soit le statut de l'entité)",
'bo.enable': 'Surveiller les ouvertures de bande', 'bo.enableHint': "(10, 12, 6, 4 et 2 m. Activer ajoute les deux nœuds RBN et souscrit au flux PSK Reporter — la détection a besoin de bien plus d oreilles qu un cluster ne peut en fournir.)", 'bo.feedUp': 'Flux PSK Reporter actif — {n} décodages vus', 'bo.feedDown': 'Flux PSK Reporter inactif — il faut ton locator, et un instant pour se connecter', 'clu.workedSameSlot': 'Déjà contacté seulement sur le même slot', 'bo.open': 'ouvert', 'bo.liveTip': '{band} est ouvert — {n} stations, ~{km} km, {sector}{season}. Cliquer pour le bandmap.', 'bo.enable': 'Surveiller les ouvertures de bande', 'bo.enableHint': "(10, 12, 6, 4 et 2 m. Activer ajoute les deux nœuds RBN et souscrit au flux PSK Reporter — la détection a besoin de bien plus d oreilles qu un cluster ne peut en fournir.)", 'bo.feedUp': 'Flux PSK Reporter actif — {n} décodages vus', 'bo.feedDown': 'Flux PSK Reporter inactif — il faut ton locator, et un instant pour se connecter', 'clu.workedSameSlot': 'Déjà contacté seulement sur le même slot',
'clu.workedSameSlotHint': '— un spot n\'affiche « contacté » que si vous avez contacté cet indicatif sur la MÊME bande et le MÊME mode, pas juste n\'importe où. Se combine avec le groupage des modes numériques (Réglages → Général) : activé, un indicatif contacté en 20m FT8 compte aussi comme contacté pour un spot 20m FT4 ; désactivé, FT8 et FT4 sont des slots distincts.', 'clu.workedSameSlotHint': '— un spot n\'affiche « contacté » que si vous avez contacté cet indicatif sur la MÊME bande et le MÊME mode, pas juste n\'importe où. Se combine avec le groupage des modes numériques (Réglages → Général) : activé, un indicatif contacté en 20m FT8 compte aussi comme contacté pour un spot 20m FT4 ; désactivé, FT8 et FT4 sont des slots distincts.',
'bk.hintMysql': "À la fermeture (1×/jour) OpsLog sauvegarde le SQLite local (config) ET exporte le log MySQL partagé en ADIF — opslog-log-<date>.adi — pour protéger tes contacts même s'ils sont sur le serveur. La rotation garde les N derniers de chaque.", 'bk.hintMysql': "À la fermeture (1×/jour) OpsLog sauvegarde le SQLite local (config) ET exporte le log MySQL partagé en ADIF — opslog-log-<date>.adi — pour protéger tes contacts même s'ils sont sur le serveur. La rotation garde les N derniers de chaque.",
'bk.hint': "OpsLog peut copier la base SQLite dans un dossier de ton choix à la fermeture, une fois par jour. La rotation garde les N dernières copies et supprime les plus anciennes.", 'bk.hint': "OpsLog peut copier la base SQLite dans un dossier de ton choix à la fermeture, une fois par jour. La rotation garde les N dernières copies et supprime les plus anciennes.",
+2
View File
@@ -442,6 +442,8 @@ export function GetIcomState():Promise<cat.IcomTXState>;
export function GetListsSettings():Promise<main.ListsSettings>; export function GetListsSettings():Promise<main.ListsSettings>;
export function GetLiveOpenings():Promise<Array<bandopen.Opening>>;
export function GetLiveStations():Promise<Array<main.LiveStation>>; export function GetLiveStations():Promise<Array<main.LiveStation>>;
export function GetLoTWUsersStatus():Promise<main.LoTWUsersStatus>; export function GetLoTWUsersStatus():Promise<main.LoTWUsersStatus>;
+4
View File
@@ -826,6 +826,10 @@ export function GetListsSettings() {
return window['go']['main']['App']['GetListsSettings'](); return window['go']['main']['App']['GetListsSettings']();
} }
export function GetLiveOpenings() {
return window['go']['main']['App']['GetLiveOpenings']();
}
export function GetLiveStations() { export function GetLiveStations() {
return window['go']['main']['App']['GetLiveStations'](); return window['go']['main']['App']['GetLiveStations']();
} }