Initial codebase: Go + Wails amateur radio logbook

Backend (Go 1.25 / Wails v2):
- QSO storage on SQLite (modernc) with embedded migrations (0001..0005)
- Streaming ADIF import (batch insert) + WorkedBefore per callsign and DXCC
- Callsign lookup with QRZ.com + HamQTH providers (primary/failsafe routing)
  and SQLite-backed TTL cache
- DXCC resolver from cty.dat (auto-download, longest-prefix-match)
- Multi-profile operator identities (home/portable/SOTA/contest) — every
  QSO stamps MY_* from the active profile
- CAT control via OmniRig COM on a single OS-locked goroutine, with
  bidirectional sync (freq/mode/band/split/VFOs) and Rig1/Rig2 hot-swap
- Settings store (key/value), CAT debug log at %APPDATA%/HamLog/cat.log

Frontend (React 18 + TypeScript + Tailwind v4 + shadcn-style):
- Single-row entry strip with CAT-aware band/mode/freq, RST, Start/End
  UTC, per-field locks (band/mode/freq/start/end) for backdated QSOs
- Topbar: live freq (MHz.kHz.Hz dotted), live UTC, band/mode/SPLIT badges,
  CAT pill with rig selector and clickable Azimuth pill (rotor TODO)
- Settings tree: Profiles (Log4OM-style manager), Station Information
  (edits the active profile), unified Callsign Lookup with Test buttons,
  Bands/Modes lists, CAT
- Worked-before matrix (band × mode × class) with new-DXCC highlighting
- ADIF import from menu + Maintenance > Refresh cty.dat

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
2026-05-26 00:16:45 +02:00
co-authored by Claude Opus 4.7
parent 734d296300
commit 7ace2cc602
87 changed files with 15892 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
import { useState } from 'react';
import {
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils';
export type MenuItem =
| { type: 'item'; label: string; action: string; shortcut?: string; disabled?: boolean }
| { type: 'separator' };
export interface Menu {
name: string;
label: string;
items: MenuItem[];
}
interface Props {
menus: Menu[];
onAction: (action: string) => void;
}
export function Menubar({ menus, onAction }: Props) {
// Track which menu is open so hover-to-switch works like a desktop menubar.
const [openMenu, setOpenMenu] = useState<string | null>(null);
return (
<nav className="flex items-stretch h-full">
{menus.map((menu) => (
<DropdownMenu
key={menu.name}
open={openMenu === menu.name}
onOpenChange={(o) => setOpenMenu(o ? menu.name : null)}
>
<DropdownMenuTrigger
onMouseEnter={() => {
// Only switch on hover if a menu is already open.
if (openMenu !== null && openMenu !== menu.name) setOpenMenu(menu.name);
}}
className={cn(
'px-3 text-sm rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring',
openMenu === menu.name && 'bg-muted text-primary',
)}
>
{menu.label}
</DropdownMenuTrigger>
<DropdownMenuContent align="start" sideOffset={4} className="min-w-[240px]">
{menu.items.map((item, i) =>
item.type === 'separator' ? (
<DropdownMenuSeparator key={i} />
) : (
<DropdownMenuItem
key={i}
disabled={item.disabled}
onSelect={() => onAction(item.action)}
>
<span>{item.label}</span>
{item.shortcut && <DropdownMenuShortcut>{item.shortcut}</DropdownMenuShortcut>}
</DropdownMenuItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
))}
</nav>
);
}