up
This commit is contained in:
+34
-4
@@ -20,6 +20,7 @@ import {
|
||||
ListClusterServers, ClusterSpotStatuses,
|
||||
GetCATSettings,
|
||||
OperatingDefaultForBand,
|
||||
LogUDPLoggedADIF,
|
||||
} from '../wailsjs/go/main/App';
|
||||
import { EventsOn } from '../wailsjs/runtime/runtime';
|
||||
import type { adif as adifModels, lookup as lookupModels, cat as catModels } from '../wailsjs/go/models';
|
||||
@@ -246,7 +247,7 @@ export default function App() {
|
||||
|
||||
// CAT — receives live rig state via Wails events.
|
||||
const [catState, setCatState] = useState<CATState>({ enabled: false, connected: false } as any);
|
||||
// Mode HamLog shows when the rig reports generic DIG_U/DIG_L. OmniRig
|
||||
// Mode OpsLog shows when the rig reports generic DIG_U/DIG_L. OmniRig
|
||||
// can't tell us if it's FT8 vs FT4 vs RTTY, so the user picks the default
|
||||
// in Preferences > Hardware > CAT interface.
|
||||
const digitalDefaultRef = useRef<string>('FT8');
|
||||
@@ -637,6 +638,35 @@ export default function App() {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// ── UDP integration events ───────────────────────────────────────────
|
||||
// Live updates from external apps (WSJT-X / JTDX / MSHV / DXHunter…).
|
||||
// We push the broadcast DX call into the entry field and auto-log any
|
||||
// ADIF record that arrives.
|
||||
useEffect(() => {
|
||||
const unsubDX = EventsOn('udp:dx_call', (p: any) => {
|
||||
const call = String(p?.call ?? '').trim();
|
||||
if (!call) return;
|
||||
// Don't clobber what the user is currently typing — only update
|
||||
// when the entry field is empty or matches a previous broadcast.
|
||||
onCallsignInput(call);
|
||||
});
|
||||
const unsubRC = EventsOn('udp:remote_call', (call: string) => {
|
||||
if (call) onCallsignInput(String(call).trim());
|
||||
});
|
||||
const unsubLog = EventsOn('udp:logged_qso', async (p: any) => {
|
||||
const text = String(p?.adif ?? '').trim();
|
||||
if (!text) return;
|
||||
try {
|
||||
await LogUDPLoggedADIF(text);
|
||||
await refresh();
|
||||
} catch (e: any) {
|
||||
setError('UDP auto-log: ' + String(e?.message ?? e));
|
||||
}
|
||||
});
|
||||
return () => { unsubDX?.(); unsubRC?.(); unsubLog?.(); };
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Resolve slot status for any spot we haven't seen yet — debounced so we
|
||||
// don't hammer the backend at firehose rate. The mode passed to the
|
||||
// backend is derived from the comment (CW / FT8 / FT4 / SSB...) and the
|
||||
@@ -941,7 +971,7 @@ export default function App() {
|
||||
{ type: 'item', label: ctyRefreshing ? 'Refreshing cty.dat…' : 'Refresh cty.dat', action: 'tools.refreshCty', disabled: ctyRefreshing },
|
||||
]},
|
||||
{ name: 'help', label: 'Help', items: [
|
||||
{ type: 'item', label: 'About HamLog', action: 'help.about', disabled: true },
|
||||
{ type: 'item', label: 'About OpsLog', action: 'help.about', disabled: true },
|
||||
]},
|
||||
], [total, selectedId, ctyRefreshing, exporting]);
|
||||
|
||||
@@ -1006,7 +1036,7 @@ export default function App() {
|
||||
<header className="flex items-center gap-3 px-3 h-8 bg-card border-b border-border shrink-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="size-2 rounded-full bg-gradient-to-br from-primary to-orange-400" />
|
||||
<span className="font-bold text-xs tracking-tight">HamLog</span>
|
||||
<span className="font-bold text-xs tracking-tight">OpsLog</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1.5 font-mono ml-2">
|
||||
<span className="text-sm font-semibold text-primary">{freqMhz ? fmtFreqDots(freqMhz) : '—.———.———'}</span>
|
||||
@@ -1027,7 +1057,7 @@ export default function App() {
|
||||
<header className="grid grid-cols-[auto_auto_1fr_auto_auto] items-center gap-4 px-4 h-12 bg-card/95 backdrop-blur border-b border-border shrink-0 shadow-sm">
|
||||
<div className="flex items-center gap-2 pr-2 border-r border-border/60">
|
||||
<div className="size-2.5 rounded-full bg-gradient-to-br from-primary to-orange-400 shadow-[0_0_0_3px_rgba(234,88,12,0.18)]" />
|
||||
<span className="font-bold text-[15px] tracking-tight">HamLog</span>
|
||||
<span className="font-bold text-[15px] tracking-tight">OpsLog</span>
|
||||
<span className="text-[11px] text-muted-foreground">v0.1</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Checkbox } from '@/components/ui/checkbox';
|
||||
// virtual-scroll — everything we want out of the box for a logbook table.
|
||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||
|
||||
// Custom Quartz theme tuned to match HamLog's warm palette.
|
||||
// Custom Quartz theme tuned to match OpsLog's warm palette.
|
||||
const hamlogTheme = themeQuartz.withParams({
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 12.5,
|
||||
@@ -40,8 +40,6 @@ const hamlogTheme = themeQuartz.withParams({
|
||||
iconSize: 12,
|
||||
});
|
||||
|
||||
const badgeCellClass = 'flex items-center';
|
||||
|
||||
type Props = {
|
||||
rows: QSOForm[];
|
||||
total: number;
|
||||
@@ -73,21 +71,6 @@ function fmtDateOnly(s: any): string {
|
||||
return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())}`;
|
||||
}
|
||||
|
||||
const bandPill = (p: any) => p.value
|
||||
? <span style={{
|
||||
display: 'inline-block', padding: '2px 8px', borderRadius: 6,
|
||||
fontFamily: 'ui-monospace, monospace', fontSize: 11, fontWeight: 600,
|
||||
backgroundColor: '#f0d9a8', color: '#7a4a14', lineHeight: '16px',
|
||||
}}>{p.value}</span>
|
||||
: '';
|
||||
const modePill = (p: any) => p.value
|
||||
? <span style={{
|
||||
display: 'inline-block', padding: '2px 8px', borderRadius: 6,
|
||||
fontFamily: 'ui-monospace, monospace', fontSize: 11, fontWeight: 600,
|
||||
backgroundColor: '#d1fae5', color: '#047857', lineHeight: '16px',
|
||||
}}>{p.value}</span>
|
||||
: '';
|
||||
|
||||
// Full catalog of selectable columns, grouped for the picker. `defaultVisible`
|
||||
// = shown out of the box; anything else stays hidden until the user toggles
|
||||
// it in the Columns dialog.
|
||||
@@ -98,9 +81,9 @@ const COL_CATALOG: ColEntry[] = [
|
||||
{ group: 'QSO', label: 'Date UTC', colId: 'qso_date', headerName: 'Date UTC', field: 'qso_date' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value), sort: 'desc', defaultVisible: true },
|
||||
{ group: 'QSO', label: 'Date Off', colId: 'qso_date_off', headerName: 'Date off', field: 'qso_date_off' as any, width: 150, cellClass: 'font-mono', valueFormatter: (p) => fmtDateUTC(p.value) },
|
||||
{ group: 'QSO', label: 'Callsign', colId: 'callsign', headerName: 'Callsign', field: 'callsign' as any, width: 110, cellClass: 'font-mono font-semibold', cellStyle: { color: '#b8410c' }, defaultVisible: true },
|
||||
{ group: 'QSO', label: 'Band', colId: 'band', headerName: 'Band', field: 'band' as any, width: 75, cellClass: badgeCellClass, cellRenderer: bandPill, defaultVisible: true },
|
||||
{ group: 'QSO', label: 'Band RX', colId: 'band_rx', headerName: 'Band RX', field: 'band_rx' as any, width: 75, cellClass: badgeCellClass, cellRenderer: bandPill },
|
||||
{ group: 'QSO', label: 'Mode', colId: 'mode', headerName: 'Mode', field: 'mode' as any, width: 80, cellClass: badgeCellClass, cellRenderer: modePill, defaultVisible: true },
|
||||
{ group: 'QSO', label: 'Band', colId: 'band', headerName: 'Band', field: 'band' as any, width: 75, cellClass: 'font-mono', defaultVisible: true },
|
||||
{ group: 'QSO', label: 'Band RX', colId: 'band_rx', headerName: 'Band RX', field: 'band_rx' as any, width: 75, cellClass: 'font-mono' },
|
||||
{ group: 'QSO', label: 'Mode', colId: 'mode', headerName: 'Mode', field: 'mode' as any, width: 80, cellClass: 'font-mono', defaultVisible: true },
|
||||
{ group: 'QSO', label: 'Submode', colId: 'submode', headerName: 'Submode', field: 'submode' as any, width: 80, cellClass: 'font-mono' },
|
||||
{ group: 'QSO', label: 'MHz (TX)', colId: 'freq_hz', headerName: 'MHz', field: 'freq_hz' as any, width: 110, type: 'rightAligned', cellClass: 'font-mono', valueFormatter: (p) => fmtMhzDots(p.value as number | undefined), comparator: (a, b) => (a ?? 0) - (b ?? 0), defaultVisible: true },
|
||||
{ group: 'QSO', label: 'MHz (RX)', colId: 'freq_rx_hz', headerName: 'MHz RX', field: 'freq_rx_hz' as any, width: 110, type: 'rightAligned', cellClass: 'font-mono', valueFormatter: (p) => fmtMhzDots(p.value as number | undefined), comparator: (a, b) => (a ?? 0) - (b ?? 0) },
|
||||
@@ -175,8 +158,6 @@ const COL_CATALOG: ColEntry[] = [
|
||||
{ group: 'Propagation', label: 'Ant az', colId: 'ant_az', headerName: 'Az', field: 'ant_az' as any, width: 70, type: 'rightAligned' },
|
||||
{ group: 'Propagation', label: 'Ant el', colId: 'ant_el', headerName: 'El', field: 'ant_el' as any, width: 70, type: 'rightAligned' },
|
||||
{ group: 'Propagation', label: 'Ant path', colId: 'ant_path', headerName: 'Path', field: 'ant_path' as any, width: 70 },
|
||||
{ group: 'Propagation', label: 'Rig', colId: 'rig', headerName: 'Rig', field: 'rig' as any, width: 120 },
|
||||
{ group: 'Propagation', label: 'Antenna', colId: 'ant', headerName: 'Antenna', field: 'ant' as any, width: 140 },
|
||||
|
||||
// ── My station (operator side) ──
|
||||
{ group: 'My station', label: 'Station call', colId: 'station_callsign', headerName: 'Station', field: 'station_callsign' as any, width: 100, cellClass: 'font-mono', defaultVisible: true },
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
ConnectClusterServer, DisconnectClusterServer,
|
||||
ConnectAllClusters, DisconnectAllClusters, GetClusterStatus,
|
||||
GetBackupSettings, SaveBackupSettings, RunBackupNow, PickBackupFolder,
|
||||
GetQSLDefaults, SaveQSLDefaults,
|
||||
ComputeStationInfo,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import type { profile as profileModels } from '../../wailsjs/go/models';
|
||||
import type { LookupSettingsForm, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
|
||||
@@ -35,6 +37,7 @@ import {
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { OperatingPanel } from '@/components/OperatingPanel';
|
||||
import { UDPIntegrationsPanel } from '@/components/UDPIntegrationsPanel';
|
||||
|
||||
type LookupSettings = LookupSettingsForm;
|
||||
type StationSettings = StationSettingsForm;
|
||||
@@ -98,7 +101,7 @@ const MODE_CATALOG: Array<{ name: string; sent: string; rcvd: string }> = [
|
||||
const emptyProfile = (): Profile => ({
|
||||
id: 0,
|
||||
name: '',
|
||||
callsign: '', operator: '',
|
||||
callsign: '', operator: '', owner_callsign: '',
|
||||
my_grid: '', my_country: '',
|
||||
my_state: '', my_cnty: '',
|
||||
my_street: '', my_city: '', my_postal_code: '',
|
||||
@@ -117,6 +120,45 @@ interface Props {
|
||||
onSaved: () => void;
|
||||
}
|
||||
|
||||
// Pretty little card showing what OpsLog will stamp on each QSO based on
|
||||
// the callsign + grid in the Station Information form. Debounces the
|
||||
// backend resolver so we don't fire on every keystroke; refreshes when
|
||||
// inputs change. Empty card when no callsign yet.
|
||||
function StationInfoComputedBadge({ callsign, grid }: { callsign: string; grid: string }) {
|
||||
const [info, setInfo] = useState<{
|
||||
country: string; dxcc: number; cqz: number; ituz: number; lat: number; lon: number;
|
||||
} | null>(null);
|
||||
useEffect(() => {
|
||||
const c = callsign.trim();
|
||||
if (!c) { setInfo(null); return; }
|
||||
const t = window.setTimeout(async () => {
|
||||
try {
|
||||
const i = await ComputeStationInfo(c, grid.trim());
|
||||
setInfo(i as any);
|
||||
} catch { setInfo(null); }
|
||||
}, 200);
|
||||
return () => window.clearTimeout(t);
|
||||
}, [callsign, grid]);
|
||||
if (!info || (!info.country && !info.cqz && !info.ituz)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="rounded-md border border-primary/30 bg-primary/5 p-2.5">
|
||||
<div className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground mb-1.5">
|
||||
Auto-filled on each QSO (MY_*)
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1 text-[11px] font-mono">
|
||||
{info.country && <span><span className="text-muted-foreground">Country:</span> <strong>{info.country}</strong></span>}
|
||||
{info.dxcc > 0 && <span><span className="text-muted-foreground">DXCC#:</span> <strong>{info.dxcc}</strong></span>}
|
||||
{info.cqz > 0 && <span><span className="text-muted-foreground">CQ:</span> <strong>{info.cqz}</strong></span>}
|
||||
{info.ituz > 0 && <span><span className="text-muted-foreground">ITU:</span> <strong>{info.ituz}</strong></span>}
|
||||
{info.lat !== 0 && <span><span className="text-muted-foreground">Lat:</span> <strong>{info.lat.toFixed(4)}</strong></span>}
|
||||
{info.lon !== 0 && <span><span className="text-muted-foreground">Lon:</span> <strong>{info.lon.toFixed(4)}</strong></span>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ====== Tree definition ======
|
||||
Section IDs are stable strings — adding new ones means adding a panel below.
|
||||
`disabled: true` greys them out and shows the "coming soon" placeholder. */
|
||||
@@ -124,6 +166,8 @@ type SectionId =
|
||||
| 'station'
|
||||
| 'profiles'
|
||||
| 'operating'
|
||||
| 'confirmations'
|
||||
| 'udp'
|
||||
| 'lookup'
|
||||
| 'lists-bands'
|
||||
| 'lists-modes'
|
||||
@@ -145,6 +189,7 @@ const TREE: TreeNode[] = [
|
||||
{ kind: 'item', label: 'Station Information', id: 'station' },
|
||||
{ kind: 'item', label: 'Profiles (portable, home, contest)', id: 'profiles' },
|
||||
{ kind: 'item', label: 'Operating conditions (rigs & antennas)', id: 'operating' },
|
||||
{ kind: 'item', label: 'Confirmations (QSL / eQSL / LoTW defaults)', id: 'confirmations' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -155,6 +200,7 @@ const TREE: TreeNode[] = [
|
||||
{ kind: 'item', label: 'Modes & default RST', id: 'lists-modes' },
|
||||
]},
|
||||
{ kind: 'item', label: 'DX Cluster', id: 'cluster' },
|
||||
{ kind: 'item', label: 'UDP integrations (WSJT-X, JTDX, MSHV…)', id: 'udp' },
|
||||
{ kind: 'item', label: 'Database backup', id: 'backup' },
|
||||
{ kind: 'item', label: 'Awards', id: 'awards', disabled: true },
|
||||
],
|
||||
@@ -174,11 +220,13 @@ const SECTION_LABELS: Partial<Record<SectionId, string>> = {
|
||||
station: 'Station Information',
|
||||
profiles: 'Profiles',
|
||||
operating: 'Operating conditions',
|
||||
confirmations: 'Confirmations',
|
||||
lookup: 'Callsign Lookup',
|
||||
'lists-bands': 'Bands',
|
||||
'lists-modes': 'Modes & default RST',
|
||||
cluster: 'DX Cluster',
|
||||
backup: 'Database backup',
|
||||
udp: 'UDP integrations',
|
||||
awards: 'Awards',
|
||||
cat: 'CAT interface',
|
||||
rotator: 'Rotator',
|
||||
@@ -316,6 +364,19 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
const [rotatorTesting, setRotatorTesting] = useState(false);
|
||||
const [rotatorTest, setRotatorTest] = useState<{ ok: boolean; msg: string } | null>(null);
|
||||
|
||||
type QSLDefaults = {
|
||||
qsl_sent: string; qsl_rcvd: string;
|
||||
lotw_sent: string; lotw_rcvd: string;
|
||||
eqsl_sent: string; eqsl_rcvd: string;
|
||||
clublog_status: string; hrdlog_status: string;
|
||||
};
|
||||
const [qslDefaults, setQslDefaults] = useState<QSLDefaults>({
|
||||
qsl_sent: '', qsl_rcvd: '',
|
||||
lotw_sent: '', lotw_rcvd: '',
|
||||
eqsl_sent: '', eqsl_rcvd: '',
|
||||
clublog_status: '', hrdlog_status: '',
|
||||
});
|
||||
|
||||
const [backupCfg, setBackupCfg] = useState<mainModels.BackupSettings>({
|
||||
enabled: false, folder: '', rotation: 5, zip: false,
|
||||
last_backup_at: '', default_folder: '',
|
||||
@@ -389,9 +450,9 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const [l, ls, c, ap, r, b] = await Promise.all([
|
||||
const [l, ls, c, ap, r, b, qd] = await Promise.all([
|
||||
GetLookupSettings(), GetListsSettings(), GetCATSettings(), GetActiveProfile(),
|
||||
GetRotatorSettings(), GetBackupSettings(),
|
||||
GetRotatorSettings(), GetBackupSettings(), GetQSLDefaults(),
|
||||
]);
|
||||
setLookup(l);
|
||||
setActiveProfile(ap as Profile);
|
||||
@@ -401,6 +462,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
setCatCfg(c);
|
||||
setRotator(r);
|
||||
setBackupCfg(b as any);
|
||||
setQslDefaults(qd as any);
|
||||
} catch (e: any) {
|
||||
setErr(String(e?.message ?? e));
|
||||
} finally {
|
||||
@@ -519,6 +581,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
await SaveCATSettings(catCfg as any);
|
||||
await SaveRotatorSettings(rotator as any);
|
||||
await SaveBackupSettings(backupCfg as any);
|
||||
await SaveQSLDefaults(qslDefaults as any);
|
||||
await SetClusterAutoConnect(clusterAutoConnect);
|
||||
|
||||
setMsg('Settings saved.');
|
||||
@@ -577,11 +640,19 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
<div className="space-y-1">
|
||||
<Label>Station callsign</Label>
|
||||
<Input className="font-mono uppercase" value={p.callsign ?? ''} onChange={(e) => updateActive({ callsign: e.target.value })} placeholder="F4XYZ" />
|
||||
<div className="text-[10px] text-muted-foreground">What's transmitted (ADIF STATION_CALLSIGN).</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label>Operator</Label>
|
||||
<Label>Operator callsign</Label>
|
||||
<Input className="font-mono uppercase" value={p.operator ?? ''} onChange={(e) => updateActive({ operator: e.target.value })} placeholder="F4XYZ" />
|
||||
<div className="text-[10px] text-muted-foreground">Who's at the radio (ADIF OPERATOR).</div>
|
||||
</div>
|
||||
<div className="space-y-1 col-span-2">
|
||||
<Label>Owner callsign</Label>
|
||||
<Input className="font-mono uppercase max-w-xs" value={p.owner_callsign ?? ''} onChange={(e) => updateActive({ owner_callsign: e.target.value })} placeholder="(leave blank if same as station)" />
|
||||
<div className="text-[10px] text-muted-foreground">Legal station owner — only differs at club stations or remote setups (ADIF STATION_OWNER).</div>
|
||||
</div>
|
||||
<div className="col-span-2"><StationInfoComputedBadge callsign={p.callsign ?? ''} grid={p.my_grid ?? ''} /></div>
|
||||
<div className="space-y-1">
|
||||
<Label>My grid</Label>
|
||||
<Input className="font-mono uppercase" value={p.my_grid ?? ''} onChange={(e) => updateActive({ my_grid: e.target.value })} placeholder="JN18BU" />
|
||||
@@ -1106,7 +1177,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
<>
|
||||
<SectionHeader
|
||||
title="CAT interface (OmniRig)"
|
||||
hint="Reads the rig's current frequency / band / mode and pushes them into the entry strip in real time. Requires OmniRig (free) installed and configured separately for your rig — HamLog just talks to it."
|
||||
hint="Reads the rig's current frequency / band / mode and pushes them into the entry strip in real time. Requires OmniRig (free) installed and configured separately for your rig — OpsLog just talks to it."
|
||||
/>
|
||||
<div className="space-y-4 max-w-lg">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
@@ -1168,10 +1239,10 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Configure your rig (COM port, baud rate, model) in OmniRig's own settings GUI first.
|
||||
HamLog will read whichever Rig slot you select here. Set <strong>CAT delay</strong>
|
||||
OpsLog will read whichever Rig slot you select here. Set <strong>CAT delay</strong>
|
||||
{' '}above 0 if your rig drops commands sent back-to-back (some older Kenwood/Yaesu).
|
||||
OmniRig only reports generic "DIG" for digital modes — <strong>Default digital mode</strong>
|
||||
{' '}is the specific mode HamLog will surface (and log).
|
||||
{' '}is the specific mode OpsLog will surface (and log).
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
@@ -1196,7 +1267,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
<>
|
||||
<SectionHeader
|
||||
title="Rotator (PstRotator)"
|
||||
hint="HamLog sends UDP commands to PstRotator. Enable PstRotator's UDP listener (Setup → Communication → UDP) before testing."
|
||||
hint="OpsLog sends UDP commands to PstRotator. Enable PstRotator's UDP listener (Setup → Communication → UDP) before testing."
|
||||
/>
|
||||
<div className="space-y-4 max-w-xl">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
@@ -1402,6 +1473,126 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function ConfirmationsPanel() {
|
||||
// ADIF status codes. FULL set for paper QSL/eQSL/Clublog/HRDLog,
|
||||
// SIMPLE Y/N set for LoTW (the only values the LoTW protocol returns).
|
||||
const FULL_OPTIONS = [
|
||||
{ value: '_', label: '— leave blank —' },
|
||||
{ value: 'Y', label: 'Y (yes)' },
|
||||
{ value: 'N', label: 'N (no)' },
|
||||
{ value: 'R', label: 'R (requested)' },
|
||||
{ value: 'Q', label: 'Q (queued)' },
|
||||
{ value: 'I', label: 'I (ignore)' },
|
||||
];
|
||||
// LoTW / Clublog / HRDLog also use ADIF-style status codes — keep
|
||||
// R (requested) available so users can mark "queued for upload"
|
||||
// and filter on it later.
|
||||
|
||||
// Renderer inlined as a constant — declaring this as a function
|
||||
// INSIDE ConfirmationsPanel would re-instantiate the component on
|
||||
// every render, which unmounts and re-mounts the Radix Select
|
||||
// (closing it the moment you click the trigger).
|
||||
const renderSelect = (
|
||||
key: keyof QSLDefaults,
|
||||
options: { value: string; label: string }[],
|
||||
) => (
|
||||
<Select
|
||||
value={qslDefaults[key] || '_'}
|
||||
onValueChange={(v) => setQslDefaults((d) => ({ ...d, [key]: v === '_' ? '' : v }))}
|
||||
>
|
||||
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{options.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title="Confirmations"
|
||||
hint="Default QSL / eQSL / LoTW / upload status applied to every QSO you log — manually or via UDP auto-log from WSJT-X / JTDX / MSHV. Leave a field blank to keep the QSO column empty."
|
||||
/>
|
||||
<div className="space-y-3 max-w-2xl">
|
||||
{/* Paper QSL */}
|
||||
<div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
|
||||
<Label className="text-sm font-medium pb-1.5">Paper QSL</Label>
|
||||
<div>
|
||||
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Sent</Label>
|
||||
{renderSelect('qsl_sent', FULL_OPTIONS)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Rcvd</Label>
|
||||
{renderSelect('qsl_rcvd', FULL_OPTIONS)}
|
||||
</div>
|
||||
</div>
|
||||
{/* eQSL */}
|
||||
<div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
|
||||
<Label className="text-sm font-medium pb-1.5">eQSL</Label>
|
||||
<div>
|
||||
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Sent</Label>
|
||||
{renderSelect('eqsl_sent', FULL_OPTIONS)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Rcvd</Label>
|
||||
{renderSelect('eqsl_rcvd', FULL_OPTIONS)}
|
||||
</div>
|
||||
</div>
|
||||
{/* LoTW */}
|
||||
<div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
|
||||
<Label className="text-sm font-medium pb-1.5">LoTW</Label>
|
||||
<div>
|
||||
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Sent</Label>
|
||||
{renderSelect('lotw_sent', FULL_OPTIONS)}
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Rcvd</Label>
|
||||
{renderSelect('lotw_rcvd', FULL_OPTIONS)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-3 space-y-3">
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
Upload status fields (Clublog / HRDLog) are stamped on every QSO; "N" is typical when you want OpsLog to track which QSOs still need to be uploaded.
|
||||
</div>
|
||||
{/* Clublog */}
|
||||
<div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
|
||||
<Label className="text-sm font-medium pb-1.5">Clublog</Label>
|
||||
<div>
|
||||
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Upload status</Label>
|
||||
{renderSelect('clublog_status', FULL_OPTIONS)}
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
{/* HRDLog */}
|
||||
<div className="grid grid-cols-[150px_1fr_1fr] gap-3 items-end">
|
||||
<Label className="text-sm font-medium pb-1.5">HRDLog</Label>
|
||||
<div>
|
||||
<Label className="text-[10px] text-muted-foreground uppercase tracking-wider mb-1 block">Upload status</Label>
|
||||
{renderSelect('hrdlog_status', FULL_OPTIONS)}
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UDPIntegrationsPanelWrapper() {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader
|
||||
title="UDP integrations"
|
||||
hint="Listen for QSO logs from WSJT-X / JTDX / MSHV, ADIF messages from JTAlert/GridTracker, or simple callsign packets from external tools. Outbound connections forward every QSO you log to a remote listener (Cloudlog UDP, N1MM, …)."
|
||||
/>
|
||||
<UDPIntegrationsPanel onError={(m) => setErr(m)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function OperatingPanelWrapper() {
|
||||
return (
|
||||
<>
|
||||
@@ -1445,7 +1636,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
<>
|
||||
<SectionHeader
|
||||
title="Database backup"
|
||||
hint="HamLog can copy the SQLite database to a folder of your choice when you close it, once per day. Rotation keeps the last N copies and deletes older ones."
|
||||
hint="OpsLog can copy the SQLite database to a folder of your choice when you close it, once per day. Rotation keeps the last N copies and deletes older ones."
|
||||
/>
|
||||
<div className="space-y-4 max-w-xl">
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
@@ -1453,7 +1644,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
checked={!!backupCfg.enabled}
|
||||
onCheckedChange={(c) => setBackupCfg((b) => ({ ...b, enabled: !!c }))}
|
||||
/>
|
||||
<span>Automatic backup when closing HamLog (max once per day)</span>
|
||||
<span>Automatic backup when closing OpsLog (max once per day)</span>
|
||||
</label>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
@@ -1483,7 +1674,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
{backupCfg.folder
|
||||
? <>Effective folder: <span className="font-mono">{effectiveFolder}</span></>
|
||||
: <>If empty, HamLog uses the default: <span className="font-mono">{(backupCfg.default_folder ?? '').replace(/\\/g, '/')}</span></>
|
||||
: <>If empty, OpsLog uses the default: <span className="font-mono">{(backupCfg.default_folder ?? '').replace(/\\/g, '/')}</span></>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1541,10 +1732,12 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
station: StationPanel,
|
||||
profiles: ProfilesPanel,
|
||||
operating: OperatingPanelWrapper,
|
||||
confirmations: ConfirmationsPanel,
|
||||
lookup: LookupPanel,
|
||||
'lists-bands': BandsPanel,
|
||||
'lists-modes': ModesPanel,
|
||||
cluster: ClusterPanel,
|
||||
udp: UDPIntegrationsPanelWrapper,
|
||||
backup: BackupPanel,
|
||||
awards: () => <ComingSoon id="awards" icon={Award} />,
|
||||
cat: CATPanel,
|
||||
@@ -1558,7 +1751,7 @@ export function SettingsModal({ onClose, onSaved, initialSection }: Props) {
|
||||
<DialogContent className="max-w-[1180px] w-full max-h-[90vh] grid grid-rows-[auto_1fr_auto] gap-0 p-0">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Preferences</DialogTitle>
|
||||
<DialogDescription className="sr-only">Configure HamLog modules — station, lookup, hardware…</DialogDescription>
|
||||
<DialogDescription className="sr-only">Configure OpsLog modules — station, lookup, hardware…</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
|
||||
@@ -9,7 +9,7 @@ type Step = {
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
// ShutdownProgress is a full-screen overlay that appears while HamLog is
|
||||
// ShutdownProgress is a full-screen overlay that appears while OpsLog is
|
||||
// running its close-time tasks (backup, future LoTW upload, ...). It
|
||||
// listens for `shutdown:start` / `shutdown:update` / `shutdown:done`
|
||||
// events from the backend and renders a checklist that updates as each
|
||||
@@ -30,7 +30,7 @@ export function ShutdownProgress() {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="bg-card border border-border rounded-lg shadow-xl p-6 min-w-[360px] max-w-[480px]">
|
||||
<div className="text-sm font-semibold mb-3 text-foreground">Closing HamLog…</div>
|
||||
<div className="text-sm font-semibold mb-3 text-foreground">Closing OpsLog…</div>
|
||||
<div className="space-y-2">
|
||||
{steps.length === 0 ? (
|
||||
<div className="text-xs text-muted-foreground italic">Nothing to do, exiting.</div>
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Plus, Trash2, Edit2, RefreshCcw, ArrowDownToLine, ArrowUpFromLine } from 'lucide-react';
|
||||
import {
|
||||
ListUDPIntegrations, SaveUDPIntegration, DeleteUDPIntegration, ReloadUDPIntegrations,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import {
|
||||
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select, SelectTrigger, SelectValue, SelectContent, SelectItem,
|
||||
} from '@/components/ui/select';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
// Local mirror of the Go struct — we duplicate the type rather than depend
|
||||
// on the generated Wails model because the inline `as any` casts are
|
||||
// noisier than just owning the shape here.
|
||||
type UDPConfig = {
|
||||
id: number;
|
||||
direction: 'inbound' | 'outbound';
|
||||
name: string;
|
||||
port: number;
|
||||
service_type: 'wsjt' | 'adif' | 'n1mm' | 'remote_call' | 'db_updated';
|
||||
multicast: boolean;
|
||||
multicast_group: string;
|
||||
destination_ip: string;
|
||||
enabled: boolean;
|
||||
sort_order: number;
|
||||
};
|
||||
|
||||
// Service-type catalog used by the dropdowns; each entry is restricted to
|
||||
// inbound or outbound and carries a hint suggesting reasonable defaults
|
||||
// for the "preset" button.
|
||||
const SERVICE_TYPES: Array<{
|
||||
id: UDPConfig['service_type'];
|
||||
direction: UDPConfig['direction'];
|
||||
label: string;
|
||||
hint: string;
|
||||
defaults: Partial<UDPConfig>;
|
||||
}> = [
|
||||
{
|
||||
id: 'wsjt',
|
||||
direction: 'inbound',
|
||||
label: 'WSJT-X / JTDX / MSHV',
|
||||
hint: 'Auto-logs FT8/FT4/etc. QSOs and fills the entry callsign live.',
|
||||
defaults: { port: 2237, multicast: true, multicast_group: '224.0.0.1' },
|
||||
},
|
||||
{
|
||||
id: 'adif',
|
||||
direction: 'inbound',
|
||||
label: 'ADIF message (JTAlert, GridTracker)',
|
||||
hint: 'Receives a single ADIF record per packet and logs it.',
|
||||
defaults: { port: 2333, multicast: false },
|
||||
},
|
||||
{
|
||||
id: 'n1mm',
|
||||
direction: 'inbound',
|
||||
label: 'N1MM Logger+ (contest XML)',
|
||||
hint: 'Receives contest QSOs as XML messages.',
|
||||
defaults: { port: 12060, multicast: false },
|
||||
},
|
||||
{
|
||||
id: 'remote_call',
|
||||
direction: 'inbound',
|
||||
label: 'Remote callsign (DXHunter, custom)',
|
||||
hint: 'A short text packet containing just a callsign — fills the entry field.',
|
||||
defaults: { port: 12090, multicast: false },
|
||||
},
|
||||
{
|
||||
id: 'db_updated',
|
||||
direction: 'outbound',
|
||||
label: 'DB updated → notify other apps',
|
||||
hint: 'Sends the ADIF of every QSO you log to a remote listener (Cloudlog UDP, N1MM, …).',
|
||||
defaults: { port: 2333, destination_ip: '127.0.0.1' },
|
||||
},
|
||||
];
|
||||
|
||||
type Props = { onError: (msg: string) => void };
|
||||
|
||||
export function UDPIntegrationsPanel({ onError }: Props) {
|
||||
const [items, setItems] = useState<UDPConfig[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editing, setEditing] = useState<UDPConfig | null>(null);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
try {
|
||||
const list = await ListUDPIntegrations();
|
||||
setItems(((list ?? []) as any[]) as UDPConfig[]);
|
||||
} catch (e: any) { onError(String(e?.message ?? e)); }
|
||||
finally { setLoading(false); }
|
||||
}, [onError]);
|
||||
|
||||
useEffect(() => { void reload(); }, [reload]);
|
||||
|
||||
function addNew(direction: UDPConfig['direction']) {
|
||||
const preset = SERVICE_TYPES.find((s) => s.direction === direction)!;
|
||||
setEditing({
|
||||
id: 0,
|
||||
direction,
|
||||
name: '',
|
||||
port: preset.defaults.port ?? 2237,
|
||||
service_type: preset.id,
|
||||
multicast: !!preset.defaults.multicast,
|
||||
multicast_group: preset.defaults.multicast_group ?? '',
|
||||
destination_ip: preset.defaults.destination_ip ?? '',
|
||||
enabled: true,
|
||||
sort_order: items.filter((i) => i.direction === direction).length,
|
||||
});
|
||||
}
|
||||
|
||||
async function save(cfg: UDPConfig) {
|
||||
try {
|
||||
const saved = await SaveUDPIntegration(cfg as any) as UDPConfig;
|
||||
setItems((prev) => {
|
||||
if (cfg.id === 0) return [...prev, saved];
|
||||
return prev.map((x) => x.id === saved.id ? saved : x);
|
||||
});
|
||||
setEditing(null);
|
||||
} catch (e: any) { onError(String(e?.message ?? e)); }
|
||||
}
|
||||
|
||||
async function remove(id: number) {
|
||||
if (!confirm('Delete this UDP connection?')) return;
|
||||
try {
|
||||
await DeleteUDPIntegration(id);
|
||||
setItems((prev) => prev.filter((x) => x.id !== id));
|
||||
} catch (e: any) { onError(String(e?.message ?? e)); }
|
||||
}
|
||||
|
||||
async function toggleEnabled(cfg: UDPConfig) {
|
||||
await save({ ...cfg, enabled: !cfg.enabled });
|
||||
}
|
||||
|
||||
async function reloadServers() {
|
||||
try {
|
||||
const errs = await ReloadUDPIntegrations();
|
||||
if (errs && (errs as string[]).length > 0) {
|
||||
onError((errs as string[]).join(' • '));
|
||||
}
|
||||
} catch (e: any) { onError(String(e?.message ?? e)); }
|
||||
}
|
||||
|
||||
if (loading) return <div className="text-xs text-muted-foreground italic">Loading…</div>;
|
||||
|
||||
const inbound = items.filter((i) => i.direction === 'inbound');
|
||||
const outbound = items.filter((i) => i.direction === 'outbound');
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="text-[11px] text-muted-foreground max-w-2xl leading-relaxed">
|
||||
UDP connections let OpsLog talk to other ham radio software. Inbound
|
||||
connections receive QSOs or callsigns and update the logbook live;
|
||||
outbound connections notify other apps when you log a QSO locally.
|
||||
Enable multicast to share a port with another listener without
|
||||
conflict — required for the typical WSJT-X 2237 setup.
|
||||
</div>
|
||||
|
||||
<Section
|
||||
title="Inbound — OpsLog listens"
|
||||
icon={<ArrowDownToLine className="size-4" />}
|
||||
items={inbound}
|
||||
onAdd={() => addNew('inbound')}
|
||||
onEdit={(c) => setEditing(c)}
|
||||
onDelete={remove}
|
||||
onToggle={toggleEnabled}
|
||||
/>
|
||||
<Section
|
||||
title="Outbound — OpsLog sends"
|
||||
icon={<ArrowUpFromLine className="size-4" />}
|
||||
items={outbound}
|
||||
onAdd={() => addNew('outbound')}
|
||||
onEdit={(c) => setEditing(c)}
|
||||
onDelete={remove}
|
||||
onToggle={toggleEnabled}
|
||||
/>
|
||||
|
||||
<div className="border-t border-border/60 pt-3 flex items-center gap-3">
|
||||
<Button size="sm" variant="outline" onClick={reloadServers}>
|
||||
<RefreshCcw className="size-3.5" /> Reload all
|
||||
</Button>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
Restarts every enabled listener after a manual change.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{editing && (
|
||||
<EditDialog
|
||||
cfg={editing}
|
||||
onCancel={() => setEditing(null)}
|
||||
onSave={save}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Section listing ────────────────────────────────────────────────────
|
||||
|
||||
function Section({
|
||||
title, icon, items, onAdd, onEdit, onDelete, onToggle,
|
||||
}: {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
items: UDPConfig[];
|
||||
onAdd: () => void;
|
||||
onEdit: (c: UDPConfig) => void;
|
||||
onDelete: (id: number) => void;
|
||||
onToggle: (c: UDPConfig) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-md border border-border bg-card">
|
||||
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/30">
|
||||
{icon}
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">{title}</span>
|
||||
<div className="flex-1" />
|
||||
<Button size="sm" variant="outline" className="h-7 text-xs" onClick={onAdd}>
|
||||
<Plus className="size-3" /> Add
|
||||
</Button>
|
||||
</div>
|
||||
{items.length === 0 ? (
|
||||
<div className="px-3 py-3 text-xs text-muted-foreground italic">No connection.</div>
|
||||
) : (
|
||||
<div className="divide-y divide-border/60">
|
||||
{items.map((c) => {
|
||||
const svc = SERVICE_TYPES.find((s) => s.id === c.service_type);
|
||||
return (
|
||||
<div key={c.id} className="flex items-center gap-2 px-3 py-2">
|
||||
<Checkbox checked={c.enabled} onCheckedChange={() => onToggle(c)} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-sm truncate">{c.name || '(unnamed)'}</span>
|
||||
<span className="text-[10px] uppercase tracking-wider text-muted-foreground bg-muted px-1.5 py-0.5 rounded">
|
||||
{svc?.label ?? c.service_type}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground font-mono">
|
||||
{c.multicast
|
||||
? <>multicast <strong>{c.multicast_group || '?'}</strong>:{c.port}</>
|
||||
: c.direction === 'outbound'
|
||||
? <>→ {c.destination_ip || '?'}:{c.port}</>
|
||||
: <>:{c.port}</>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<Button size="icon" variant="ghost" className="size-7" onClick={() => onEdit(c)}>
|
||||
<Edit2 className="size-3.5" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" className="size-7 text-destructive hover:bg-destructive/10" onClick={() => onDelete(c.id)}>
|
||||
<Trash2 className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Edit dialog ────────────────────────────────────────────────────────
|
||||
|
||||
function EditDialog({
|
||||
cfg, onCancel, onSave,
|
||||
}: {
|
||||
cfg: UDPConfig;
|
||||
onCancel: () => void;
|
||||
onSave: (c: UDPConfig) => void;
|
||||
}) {
|
||||
const [draft, setDraft] = useState<UDPConfig>(cfg);
|
||||
// Service-type list filtered to this connection's direction.
|
||||
const services = SERVICE_TYPES.filter((s) => s.direction === draft.direction);
|
||||
const currentService = services.find((s) => s.id === draft.service_type);
|
||||
|
||||
function applyPreset(id: UDPConfig['service_type']) {
|
||||
const preset = SERVICE_TYPES.find((s) => s.id === id);
|
||||
if (!preset) return;
|
||||
setDraft((d) => ({
|
||||
...d,
|
||||
service_type: id,
|
||||
port: preset.defaults.port ?? d.port,
|
||||
multicast: preset.defaults.multicast ?? d.multicast,
|
||||
multicast_group: preset.defaults.multicast_group ?? d.multicast_group,
|
||||
destination_ip: preset.defaults.destination_ip ?? d.destination_ip,
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => { if (!o) onCancel(); }}>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{cfg.id === 0 ? 'New' : 'Edit'} {draft.direction} connection
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{currentService?.hint}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 px-5 py-4">
|
||||
<div className="space-y-1">
|
||||
<Label>Name</Label>
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={draft.direction === 'inbound' ? 'WSJT-X log' : 'Cloudlog notify'}
|
||||
value={draft.name}
|
||||
onChange={(e) => setDraft((d) => ({ ...d, name: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label>Service type</Label>
|
||||
<Select value={draft.service_type} onValueChange={(v) => applyPreset(v as UDPConfig['service_type'])}>
|
||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{services.map((s) => (
|
||||
<SelectItem key={s.id} value={s.id}>{s.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[1fr_auto] gap-2 items-end">
|
||||
<div className="space-y-1">
|
||||
<Label>Port</Label>
|
||||
<Input
|
||||
type="number"
|
||||
min={1} max={65535}
|
||||
className="font-mono"
|
||||
value={draft.port}
|
||||
onChange={(e) => {
|
||||
const n = Number(e.target.value);
|
||||
if (Number.isFinite(n)) setDraft((d) => ({ ...d, port: Math.floor(n) }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer pb-2">
|
||||
<Checkbox
|
||||
checked={draft.multicast}
|
||||
onCheckedChange={(c) => setDraft((d) => ({ ...d, multicast: !!c }))}
|
||||
/>
|
||||
<span>Multicast</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{draft.multicast && (
|
||||
<div className="space-y-1">
|
||||
<Label>Multicast group</Label>
|
||||
<Input
|
||||
className="font-mono"
|
||||
placeholder="224.0.0.1"
|
||||
value={draft.multicast_group}
|
||||
onChange={(e) => setDraft((d) => ({ ...d, multicast_group: e.target.value }))}
|
||||
/>
|
||||
<div className="text-[10px] text-muted-foreground">
|
||||
Use the same group address as the sending app. WSJT-X default is 224.0.0.1.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{draft.direction === 'outbound' && (
|
||||
<div className="space-y-1">
|
||||
<Label>Destination IP</Label>
|
||||
<Input
|
||||
className="font-mono"
|
||||
placeholder="127.0.0.1"
|
||||
value={draft.destination_ip}
|
||||
onChange={(e) => setDraft((d) => ({ ...d, destination_ip: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
||||
<Checkbox
|
||||
checked={draft.enabled}
|
||||
onCheckedChange={(c) => setDraft((d) => ({ ...d, enabled: !!c }))}
|
||||
/>
|
||||
<span>Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={onCancel}>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => onSave(draft)}
|
||||
disabled={!draft.name.trim() || !draft.port}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// silence unused-import for cn — kept for future styling tweaks
|
||||
void cn;
|
||||
Vendored
+19
@@ -7,6 +7,7 @@ import {adif} from '../models';
|
||||
import {cat} from '../models';
|
||||
import {cluster} from '../models';
|
||||
import {operating} from '../models';
|
||||
import {udp} from '../models';
|
||||
import {lookup} from '../models';
|
||||
|
||||
export function ActivateProfile(arg1:number):Promise<void>;
|
||||
@@ -17,6 +18,8 @@ export function ClearLookupCache():Promise<void>;
|
||||
|
||||
export function ClusterSpotStatuses(arg1:Array<main.SpotQuery>):Promise<Array<main.SpotStatus>>;
|
||||
|
||||
export function ComputeStationInfo(arg1:string,arg2:string):Promise<main.StationInfoComputed>;
|
||||
|
||||
export function ConnectAllClusters():Promise<void>;
|
||||
|
||||
export function ConnectClusterServer(arg1:number):Promise<void>;
|
||||
@@ -35,6 +38,8 @@ export function DeleteProfile(arg1:number):Promise<void>;
|
||||
|
||||
export function DeleteQSO(arg1:number):Promise<void>;
|
||||
|
||||
export function DeleteUDPIntegration(arg1:number):Promise<void>;
|
||||
|
||||
export function DisconnectAllClusters():Promise<void>;
|
||||
|
||||
export function DisconnectClusterServer(arg1:number):Promise<void>;
|
||||
@@ -59,8 +64,12 @@ export function GetCtyDatInfo():Promise<main.CtyDatInfo>;
|
||||
|
||||
export function GetListsSettings():Promise<main.ListsSettings>;
|
||||
|
||||
export function GetLogFilePath():Promise<string>;
|
||||
|
||||
export function GetLookupSettings():Promise<main.LookupSettings>;
|
||||
|
||||
export function GetQSLDefaults():Promise<main.QSLDefaults>;
|
||||
|
||||
export function GetQSO(arg1:number):Promise<qso.QSO>;
|
||||
|
||||
export function GetRotatorSettings():Promise<main.RotatorSettings>;
|
||||
@@ -79,6 +88,10 @@ export function ListProfiles():Promise<Array<profile.Profile>>;
|
||||
|
||||
export function ListQSO(arg1:qso.ListFilter):Promise<Array<qso.QSO>>;
|
||||
|
||||
export function ListUDPIntegrations():Promise<Array<udp.Config>>;
|
||||
|
||||
export function LogUDPLoggedADIF(arg1:string):Promise<number>;
|
||||
|
||||
export function LookupCallsign(arg1:string):Promise<lookup.Result>;
|
||||
|
||||
export function OpenADIFFile():Promise<string>;
|
||||
@@ -91,6 +104,8 @@ export function PickBackupFolder():Promise<string>;
|
||||
|
||||
export function RefreshCtyDat():Promise<main.CtyDatInfo>;
|
||||
|
||||
export function ReloadUDPIntegrations():Promise<Array<string>>;
|
||||
|
||||
export function RotatorGoTo(arg1:number,arg2:number):Promise<void>;
|
||||
|
||||
export function RotatorPark():Promise<void>;
|
||||
@@ -117,10 +132,14 @@ export function SaveOperatingStation(arg1:operating.Station):Promise<operating.S
|
||||
|
||||
export function SaveProfile(arg1:profile.Profile):Promise<profile.Profile>;
|
||||
|
||||
export function SaveQSLDefaults(arg1:main.QSLDefaults):Promise<void>;
|
||||
|
||||
export function SaveRotatorSettings(arg1:main.RotatorSettings):Promise<void>;
|
||||
|
||||
export function SaveStationSettings(arg1:main.StationSettings):Promise<void>;
|
||||
|
||||
export function SaveUDPIntegration(arg1:udp.Config):Promise<udp.Config>;
|
||||
|
||||
export function SendClusterCommand(arg1:string):Promise<void>;
|
||||
|
||||
export function SetCATFrequency(arg1:number):Promise<void>;
|
||||
|
||||
@@ -18,6 +18,10 @@ export function ClusterSpotStatuses(arg1) {
|
||||
return window['go']['main']['App']['ClusterSpotStatuses'](arg1);
|
||||
}
|
||||
|
||||
export function ComputeStationInfo(arg1, arg2) {
|
||||
return window['go']['main']['App']['ComputeStationInfo'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ConnectAllClusters() {
|
||||
return window['go']['main']['App']['ConnectAllClusters']();
|
||||
}
|
||||
@@ -54,6 +58,10 @@ export function DeleteQSO(arg1) {
|
||||
return window['go']['main']['App']['DeleteQSO'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteUDPIntegration(arg1) {
|
||||
return window['go']['main']['App']['DeleteUDPIntegration'](arg1);
|
||||
}
|
||||
|
||||
export function DisconnectAllClusters() {
|
||||
return window['go']['main']['App']['DisconnectAllClusters']();
|
||||
}
|
||||
@@ -102,10 +110,18 @@ export function GetListsSettings() {
|
||||
return window['go']['main']['App']['GetListsSettings']();
|
||||
}
|
||||
|
||||
export function GetLogFilePath() {
|
||||
return window['go']['main']['App']['GetLogFilePath']();
|
||||
}
|
||||
|
||||
export function GetLookupSettings() {
|
||||
return window['go']['main']['App']['GetLookupSettings']();
|
||||
}
|
||||
|
||||
export function GetQSLDefaults() {
|
||||
return window['go']['main']['App']['GetQSLDefaults']();
|
||||
}
|
||||
|
||||
export function GetQSO(arg1) {
|
||||
return window['go']['main']['App']['GetQSO'](arg1);
|
||||
}
|
||||
@@ -142,6 +158,14 @@ export function ListQSO(arg1) {
|
||||
return window['go']['main']['App']['ListQSO'](arg1);
|
||||
}
|
||||
|
||||
export function ListUDPIntegrations() {
|
||||
return window['go']['main']['App']['ListUDPIntegrations']();
|
||||
}
|
||||
|
||||
export function LogUDPLoggedADIF(arg1) {
|
||||
return window['go']['main']['App']['LogUDPLoggedADIF'](arg1);
|
||||
}
|
||||
|
||||
export function LookupCallsign(arg1) {
|
||||
return window['go']['main']['App']['LookupCallsign'](arg1);
|
||||
}
|
||||
@@ -166,6 +190,10 @@ export function RefreshCtyDat() {
|
||||
return window['go']['main']['App']['RefreshCtyDat']();
|
||||
}
|
||||
|
||||
export function ReloadUDPIntegrations() {
|
||||
return window['go']['main']['App']['ReloadUDPIntegrations']();
|
||||
}
|
||||
|
||||
export function RotatorGoTo(arg1, arg2) {
|
||||
return window['go']['main']['App']['RotatorGoTo'](arg1, arg2);
|
||||
}
|
||||
@@ -218,6 +246,10 @@ export function SaveProfile(arg1) {
|
||||
return window['go']['main']['App']['SaveProfile'](arg1);
|
||||
}
|
||||
|
||||
export function SaveQSLDefaults(arg1) {
|
||||
return window['go']['main']['App']['SaveQSLDefaults'](arg1);
|
||||
}
|
||||
|
||||
export function SaveRotatorSettings(arg1) {
|
||||
return window['go']['main']['App']['SaveRotatorSettings'](arg1);
|
||||
}
|
||||
@@ -226,6 +258,10 @@ export function SaveStationSettings(arg1) {
|
||||
return window['go']['main']['App']['SaveStationSettings'](arg1);
|
||||
}
|
||||
|
||||
export function SaveUDPIntegration(arg1) {
|
||||
return window['go']['main']['App']['SaveUDPIntegration'](arg1);
|
||||
}
|
||||
|
||||
export function SendClusterCommand(arg1) {
|
||||
return window['go']['main']['App']['SendClusterCommand'](arg1);
|
||||
}
|
||||
|
||||
@@ -394,6 +394,32 @@ export namespace main {
|
||||
}
|
||||
}
|
||||
|
||||
export class QSLDefaults {
|
||||
qsl_sent: string;
|
||||
qsl_rcvd: string;
|
||||
lotw_sent: string;
|
||||
lotw_rcvd: string;
|
||||
eqsl_sent: string;
|
||||
eqsl_rcvd: string;
|
||||
clublog_status: string;
|
||||
hrdlog_status: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new QSLDefaults(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.qsl_sent = source["qsl_sent"];
|
||||
this.qsl_rcvd = source["qsl_rcvd"];
|
||||
this.lotw_sent = source["lotw_sent"];
|
||||
this.lotw_rcvd = source["lotw_rcvd"];
|
||||
this.eqsl_sent = source["eqsl_sent"];
|
||||
this.eqsl_rcvd = source["eqsl_rcvd"];
|
||||
this.clublog_status = source["clublog_status"];
|
||||
this.hrdlog_status = source["hrdlog_status"];
|
||||
}
|
||||
}
|
||||
export class RotatorSettings {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
@@ -468,6 +494,28 @@ export namespace main {
|
||||
this.db_path = source["db_path"];
|
||||
}
|
||||
}
|
||||
export class StationInfoComputed {
|
||||
country: string;
|
||||
dxcc: number;
|
||||
cqz: number;
|
||||
ituz: number;
|
||||
lat: number;
|
||||
lon: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new StationInfoComputed(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.country = source["country"];
|
||||
this.dxcc = source["dxcc"];
|
||||
this.cqz = source["cqz"];
|
||||
this.ituz = source["ituz"];
|
||||
this.lat = source["lat"];
|
||||
this.lon = source["lon"];
|
||||
}
|
||||
}
|
||||
export class StationSettings {
|
||||
callsign: string;
|
||||
operator: string;
|
||||
@@ -618,6 +666,7 @@ export namespace profile {
|
||||
name: string;
|
||||
callsign: string;
|
||||
operator: string;
|
||||
owner_callsign: string;
|
||||
my_grid: string;
|
||||
my_country: string;
|
||||
my_state: string;
|
||||
@@ -647,6 +696,7 @@ export namespace profile {
|
||||
this.name = source["name"];
|
||||
this.callsign = source["callsign"];
|
||||
this.operator = source["operator"];
|
||||
this.owner_callsign = source["owner_callsign"];
|
||||
this.my_grid = source["my_grid"];
|
||||
this.my_country = source["my_country"];
|
||||
this.my_state = source["my_state"];
|
||||
@@ -1078,3 +1128,38 @@ export namespace qso {
|
||||
|
||||
}
|
||||
|
||||
export namespace udp {
|
||||
|
||||
export class Config {
|
||||
id: number;
|
||||
direction: string;
|
||||
name: string;
|
||||
port: number;
|
||||
service_type: string;
|
||||
multicast: boolean;
|
||||
multicast_group: string;
|
||||
destination_ip: string;
|
||||
enabled: boolean;
|
||||
sort_order: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Config(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.direction = source["direction"];
|
||||
this.name = source["name"];
|
||||
this.port = source["port"];
|
||||
this.service_type = source["service_type"];
|
||||
this.multicast = source["multicast"];
|
||||
this.multicast_group = source["multicast_group"];
|
||||
this.destination_ip = source["destination_ip"];
|
||||
this.enabled = source["enabled"];
|
||||
this.sort_order = source["sort_order"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user