Documentation
¶
Overview ¶
Package configstore persists graywolf configuration in a SQLite database via GORM. Pure-Go (no cgo) via glebarez/sqlite.
Index ¶
- Constants
- func IsNotFound(err error) bool
- func IsValidTheme(id string) bool
- func U32Ptr(v uint32) *uint32
- func ValidChannelMode(m string) bool
- func ValidKissInterfaceType(t string) bool
- func ValidKissMode(m string) bool
- type AX25SessionProfile
- type AX25TerminalConfig
- type AX25TranscriptEntry
- type AX25TranscriptSession
- type Action
- type ActionInvocation
- type ActionInvocationFilter
- type ActionListenerAddressee
- type AgwConfig
- type AudioDevice
- type Beacon
- type Channel
- type ChannelModeLookup
- type ConfigStore
- type DigipeaterConfig
- type DigipeaterRule
- type GPSConfig
- type IGateConfig
- type IGateRfFilter
- type KissInterface
- type LogBufferConfig
- type MapsConfig
- type MapsDownload
- type Message
- type MessageCounter
- type MessagePreferences
- type MessagesConfig
- type OTPCredential
- type OrphanChannelRefRows
- type PacketFilter
- type PositionLogConfig
- type PttConfig
- type Referrer
- type Referrers
- type SmartBeaconConfig
- type StationConfig
- type Store
- func (s *Store) AppendAX25TranscriptEntry(ctx context.Context, e *AX25TranscriptEntry) error
- func (s *Store) ChannelExists(ctx context.Context, id uint32) (bool, error)
- func (s *Store) ChannelReferrers(ctx context.Context, channelID uint32) (Referrers, error)
- func (s *Store) Close() error
- func (s *Store) CountOrphanChannelRefs(ctx context.Context) (map[string]int, error)
- func (s *Store) CreateAX25SessionProfile(ctx context.Context, p *AX25SessionProfile) error
- func (s *Store) CreateAX25TranscriptSession(ctx context.Context, sess *AX25TranscriptSession) error
- func (s *Store) CreateAction(ctx context.Context, a *Action) error
- func (s *Store) CreateActionListenerAddressee(ctx context.Context, name string) error
- func (s *Store) CreateAudioDevice(ctx context.Context, d *AudioDevice) error
- func (s *Store) CreateBeacon(ctx context.Context, b *Beacon) error
- func (s *Store) CreateChannel(ctx context.Context, c *Channel) error
- func (s *Store) CreateDigipeaterRule(ctx context.Context, r *DigipeaterRule) error
- func (s *Store) CreateIGateRfFilter(ctx context.Context, f *IGateRfFilter) error
- func (s *Store) CreateKissInterface(ctx context.Context, k *KissInterface) error
- func (s *Store) CreateOTPCredential(ctx context.Context, c *OTPCredential) error
- func (s *Store) CreateTacticalCallsign(ctx context.Context, t *TacticalCallsign) error
- func (s *Store) DB() *gorm.DB
- func (s *Store) DeleteAX25SessionProfile(ctx context.Context, id uint32) error
- func (s *Store) DeleteAX25TranscriptSession(ctx context.Context, id uint32) error
- func (s *Store) DeleteAction(ctx context.Context, id uint) error
- func (s *Store) DeleteActionListenerAddresseeByName(ctx context.Context, name string) error
- func (s *Store) DeleteAllAX25Transcripts(ctx context.Context) error
- func (s *Store) DeleteAllActionInvocations(ctx context.Context) (int, error)
- func (s *Store) DeleteAudioDevice(ctx context.Context, id uint32) error
- func (s *Store) DeleteAudioDeviceChecked(ctx context.Context, id uint32, cascade bool) (deleted []Channel, refs []Channel, err error)
- func (s *Store) DeleteBeacon(ctx context.Context, id uint32) error
- func (s *Store) DeleteChannel(ctx context.Context, id uint32) error
- func (s *Store) DeleteChannelCascade(ctx context.Context, channelID uint32) (int, error)
- func (s *Store) DeleteDigipeaterRule(ctx context.Context, id uint32) error
- func (s *Store) DeleteIGateRfFilter(ctx context.Context, id uint32) error
- func (s *Store) DeleteKissInterface(ctx context.Context, id uint32) error
- func (s *Store) DeleteMapsDownload(ctx context.Context, slug string) error
- func (s *Store) DeleteOTPCredential(ctx context.Context, id uint) error
- func (s *Store) DeletePttConfig(ctx context.Context, channelID uint32) error
- func (s *Store) DeleteTacticalCallsign(ctx context.Context, id uint32) error
- func (s *Store) EndAX25TranscriptSession(ctx context.Context, id uint32, reason string, bytes, frames uint64) error
- func (s *Store) GetAX25SessionProfile(ctx context.Context, id uint32) (*AX25SessionProfile, error)
- func (s *Store) GetAX25TerminalConfig(ctx context.Context) (*AX25TerminalConfig, error)
- func (s *Store) GetAX25TranscriptSession(ctx context.Context, id uint32) (*AX25TranscriptSession, error)
- func (s *Store) GetAction(ctx context.Context, id uint) (*Action, error)
- func (s *Store) GetActionByName(ctx context.Context, name string) (*Action, error)
- func (s *Store) GetAgwConfig(ctx context.Context) (*AgwConfig, error)
- func (s *Store) GetAudioDevice(ctx context.Context, id uint32) (*AudioDevice, error)
- func (s *Store) GetBeacon(ctx context.Context, id uint32) (*Beacon, error)
- func (s *Store) GetChannel(ctx context.Context, id uint32) (*Channel, error)
- func (s *Store) GetDigipeaterConfig(ctx context.Context) (*DigipeaterConfig, error)
- func (s *Store) GetGPSConfig(ctx context.Context) (*GPSConfig, error)
- func (s *Store) GetIGateConfig(ctx context.Context) (*IGateConfig, error)
- func (s *Store) GetKissInterface(ctx context.Context, id uint32) (*KissInterface, error)
- func (s *Store) GetLogBufferConfig(ctx context.Context) (LogBufferConfig, bool, error)
- func (s *Store) GetMapsConfig(ctx context.Context) (MapsConfig, error)
- func (s *Store) GetMapsDownload(ctx context.Context, slug string) (MapsDownload, error)
- func (s *Store) GetMessagePreferences(ctx context.Context) (*MessagePreferences, error)
- func (s *Store) GetMessagesConfig(ctx context.Context) (*MessagesConfig, error)
- func (s *Store) GetOTPCredential(ctx context.Context, id uint) (*OTPCredential, error)
- func (s *Store) GetOTPCredentialByName(ctx context.Context, name string) (*OTPCredential, error)
- func (s *Store) GetPositionLogConfig(ctx context.Context) (*PositionLogConfig, error)
- func (s *Store) GetPttConfigForChannel(ctx context.Context, channelID uint32) (*PttConfig, error)
- func (s *Store) GetSmartBeaconConfig(ctx context.Context) (*SmartBeaconConfig, error)
- func (s *Store) GetStationConfig(ctx context.Context) (StationConfig, error)
- func (s *Store) GetTacticalCallsign(ctx context.Context, id uint32) (*TacticalCallsign, error)
- func (s *Store) GetTacticalCallsignByCallsign(ctx context.Context, callsign string) (*TacticalCallsign, error)
- func (s *Store) GetThemeConfig(ctx context.Context) (ThemeConfig, error)
- func (s *Store) GetTxTiming(ctx context.Context, channel uint32) (*TxTiming, error)
- func (s *Store) GetUnitsConfig(ctx context.Context) (UnitsConfig, error)
- func (s *Store) GetUpdatesConfig(ctx context.Context) (UpdatesConfig, error)
- func (s *Store) InsertActionInvocation(ctx context.Context, row *ActionInvocation) error
- func (s *Store) ListAX25SessionProfiles(ctx context.Context) ([]AX25SessionProfile, error)
- func (s *Store) ListAX25TranscriptEntries(ctx context.Context, sessionID uint32) ([]AX25TranscriptEntry, error)
- func (s *Store) ListAX25TranscriptSessions(ctx context.Context, limit int) ([]AX25TranscriptSession, error)
- func (s *Store) ListActionInvocations(ctx context.Context, f ActionInvocationFilter) ([]ActionInvocation, error)
- func (s *Store) ListActionListenerAddressees(ctx context.Context) ([]ActionListenerAddressee, error)
- func (s *Store) ListActions(ctx context.Context) ([]Action, error)
- func (s *Store) ListAudioDevices(ctx context.Context) ([]AudioDevice, error)
- func (s *Store) ListBeacons(ctx context.Context) ([]Beacon, error)
- func (s *Store) ListChannels(ctx context.Context) ([]Channel, error)
- func (s *Store) ListDigipeaterRules(ctx context.Context) ([]DigipeaterRule, error)
- func (s *Store) ListDigipeaterRulesForChannel(ctx context.Context, channel uint32) ([]DigipeaterRule, error)
- func (s *Store) ListEnabledTacticalCallsigns(ctx context.Context) ([]TacticalCallsign, error)
- func (s *Store) ListIGateRfFilters(ctx context.Context) ([]IGateRfFilter, error)
- func (s *Store) ListIGateRfFiltersForChannel(ctx context.Context, channel uint32) ([]IGateRfFilter, error)
- func (s *Store) ListKissInterfaces(ctx context.Context) ([]KissInterface, error)
- func (s *Store) ListMapsDownloads(ctx context.Context) ([]MapsDownload, error)
- func (s *Store) ListOTPCredentials(ctx context.Context) ([]OTPCredential, error)
- func (s *Store) ListOrphanChannelRefs(ctx context.Context) ([]OrphanChannelRefRows, error)
- func (s *Store) ListPacketFilters(ctx context.Context) ([]PacketFilter, error)
- func (s *Store) ListPttConfigs(ctx context.Context) ([]PttConfig, error)
- func (s *Store) ListTacticalCallsigns(ctx context.Context) ([]TacticalCallsign, error)
- func (s *Store) ListTxTimings(ctx context.Context) ([]TxTiming, error)
- func (s *Store) Migrate() error
- func (s *Store) MigrateMapsDownloadSlugs(ctx context.Context) error
- func (s *Store) ModeForChannel(ctx context.Context, channelID uint32) (string, error)
- func (s *Store) OTPCredentialUsedBy(ctx context.Context) (map[uint][]string, error)
- func (s *Store) PinAX25SessionProfile(ctx context.Context, id uint32, pinned bool) error
- func (s *Store) PruneActionInvocations(ctx context.Context, maxRows int, maxAge time.Duration) (int, error)
- func (s *Store) ResolveStationCallsign(ctx context.Context) (string, error)
- func (s *Store) SQLiteVersion() string
- func (s *Store) SetChannelFX25(ctx context.Context, id uint32, enable bool) error
- func (s *Store) SetChannelIL2P(ctx context.Context, id uint32, enable bool) error
- func (s *Store) TouchAX25SessionProfileLastUsed(ctx context.Context, id uint32, when time.Time) error
- func (s *Store) TouchOTPCredentialUsed(ctx context.Context, id uint, when time.Time) error
- func (s *Store) UpdateAX25SessionProfile(ctx context.Context, p *AX25SessionProfile) error
- func (s *Store) UpdateAction(ctx context.Context, a *Action) error
- func (s *Store) UpdateAudioDevice(ctx context.Context, d *AudioDevice) error
- func (s *Store) UpdateBeacon(ctx context.Context, b *Beacon) error
- func (s *Store) UpdateChannel(ctx context.Context, c *Channel) error
- func (s *Store) UpdateDigipeaterRule(ctx context.Context, r *DigipeaterRule) error
- func (s *Store) UpdateIGateRfFilter(ctx context.Context, f *IGateRfFilter) error
- func (s *Store) UpdateKissInterface(ctx context.Context, k *KissInterface) error
- func (s *Store) UpdateTacticalCallsign(ctx context.Context, t *TacticalCallsign) error
- func (s *Store) UpsertAX25TerminalConfig(ctx context.Context, cfg *AX25TerminalConfig) error
- func (s *Store) UpsertAgwConfig(ctx context.Context, c *AgwConfig) error
- func (s *Store) UpsertDigipeaterConfig(ctx context.Context, c *DigipeaterConfig) error
- func (s *Store) UpsertGPSConfig(ctx context.Context, c *GPSConfig) error
- func (s *Store) UpsertIGateConfig(ctx context.Context, c *IGateConfig) error
- func (s *Store) UpsertLogBufferConfig(ctx context.Context, c LogBufferConfig) error
- func (s *Store) UpsertMapsConfig(ctx context.Context, c MapsConfig) error
- func (s *Store) UpsertMapsDownload(ctx context.Context, d MapsDownload) error
- func (s *Store) UpsertMessagePreferences(ctx context.Context, cfg *MessagePreferences) error
- func (s *Store) UpsertMessagesConfig(ctx context.Context, mc *MessagesConfig) error
- func (s *Store) UpsertPositionLogConfig(ctx context.Context, c *PositionLogConfig) error
- func (s *Store) UpsertPttConfig(ctx context.Context, p *PttConfig) error
- func (s *Store) UpsertRecentAX25SessionProfile(ctx context.Context, p *AX25SessionProfile, capRecents int) error
- func (s *Store) UpsertSmartBeaconConfig(ctx context.Context, cfg *SmartBeaconConfig) error
- func (s *Store) UpsertStationConfig(ctx context.Context, c StationConfig) error
- func (s *Store) UpsertThemeConfig(ctx context.Context, c ThemeConfig) error
- func (s *Store) UpsertTxTiming(ctx context.Context, t *TxTiming) error
- func (s *Store) UpsertUnitsConfig(ctx context.Context, c UnitsConfig) error
- func (s *Store) UpsertUpdatesConfig(ctx context.Context, c UpdatesConfig) error
- type TacticalCallsign
- type ThemeConfig
- type TxTiming
- type UnitsConfig
- type UpdatesConfig
Constants ¶
const ( KissModeModem = "modem" KissModeTnc = "tnc" )
KISS interface mode values. Stored lowercase and matched exactly — see ValidKissMode. The default for newly created rows is KissModeModem so existing behavior is preserved byte-for-byte.
const ( DefaultTncIngressRateHz uint32 = 50 DefaultTncIngressBurst uint32 = 100 )
Defaults for KissInterface.TncIngressRateHz / TncIngressBurst. Kept in sync with the GORM struct-tag defaults on KissInterface. Go callers should reference these constants rather than hard-coding 50/100 so the two sides of the model can't drift.
const ( KissTypeTCP = "tcp" KissTypeTCPClient = "tcp-client" KissTypeSerial = "serial" KissTypeBluetooth = "bluetooth" )
KISS interface transport types. Kept lowercase and matched exactly via ValidKissInterfaceType. "tcp" is the server-listen (inbound) transport — graywolf binds ListenAddr and accepts multiple clients. "tcp-client" is the outbound dial (Phase 4) — graywolf connects to a remote KISS TNC at RemoteHost:RemotePort and maintains a single supervised connection with exponential backoff + jitter.
const ( ChannelModeAPRS = "aprs" // APRS only — connected-mode AX.25 refuses this channel ChannelModePacket = "packet" // Packet only — beacon/digi/igate/messages all suppressed for this channel ChannelModeAPRSPacket = "aprs+packet" // Permissive — both allowed )
Channel.Mode values. Default is ChannelModeAPRS to preserve current behavior on databases that pre-date the column.
const ( ReferrerTypeBeacon = "beacon" ReferrerTypeDigipeaterRuleFrom = "digipeater_rule_from" ReferrerTypeDigipeaterRuleTo = "digipeater_rule_to" ReferrerTypeKissInterface = "kiss_interface" ReferrerTypeIGateConfigRf = "igate_config_rf" ReferrerTypeIGateConfigTx = "igate_config_tx" ReferrerTypeIGateRfFilter = "igate_rf_filter" ReferrerTypeTxTiming = "tx_timing" )
Referrer Type tokens. Exported so the webapi layer can switch on them without re-stringing constants in two places.
Variables ¶
This section is empty.
Functions ¶
func IsNotFound ¶ added in v0.13.0
IsNotFound mirrors gorm.ErrRecordNotFound for callers that want a stable not-found check without importing gorm directly.
func IsValidTheme ¶
IsValidTheme reports whether id is a well-formed theme identifier. It does NOT verify the id corresponds to a shipped theme — that's the frontend's job.
func U32Ptr ¶
U32Ptr returns a pointer to a copy of v. Small helper for call sites (tests, DTO mappers, fixtures) that need to set Channel.InputDeviceID — a *uint32 after the Phase 2 nullable migration — from a literal or a uint32 local. Keeps the common case a one-liner without the "declare local, take address" dance.
func ValidChannelMode ¶ added in v0.12.4
ValidChannelMode reports whether m is an accepted Channel.Mode value. Empty string is rejected; callers wanting the default must substitute ChannelModeAPRS first.
func ValidKissInterfaceType ¶
ValidKissInterfaceType reports whether t is an accepted KissInterface.InterfaceType value. "tcp-client" was added in Phase 4 of the KISS TCP-client + channel-backing plan.
func ValidKissMode ¶
ValidKissMode reports whether m is an accepted KissInterface.Mode value. The match is case-sensitive and the empty string is rejected: callers that want the "absent field" default to land on KissModeModem must substitute it themselves before calling this helper.
Types ¶
type AX25SessionProfile ¶ added in v0.12.4
type AX25SessionProfile struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"not null;default:''" json:"name"`
LocalCall string `gorm:"not null" json:"local_call"`
LocalSSID uint8 `gorm:"column:local_ssid;not null;default:0" json:"local_ssid"`
DestCall string `gorm:"not null" json:"dest_call"`
DestSSID uint8 `gorm:"column:dest_ssid;not null;default:0" json:"dest_ssid"`
// ViaPath is a comma-separated digipeater list ("WIDE2-1,RELAY") so
// the column stays a single text column without a join table.
ViaPath string `gorm:"not null;default:''" json:"via_path"`
Mod128 bool `gorm:"not null;default:false" json:"mod128"`
Paclen uint32 `gorm:"not null;default:0" json:"paclen"`
Maxframe uint32 `gorm:"not null;default:0" json:"maxframe"`
T1MS uint32 `gorm:"not null;default:0" json:"t1_ms"`
T2MS uint32 `gorm:"not null;default:0" json:"t2_ms"`
T3MS uint32 `gorm:"not null;default:0" json:"t3_ms"`
N2 uint32 `gorm:"not null;default:0" json:"n2"`
ChannelID *uint32 `gorm:"" json:"channel_id,omitempty"`
// Pinned distinguishes operator-saved profiles from automatic recents.
// A pinned profile survives the recent-list trim that caps unpinned
// rows at 20.
Pinned bool `gorm:"not null;default:false;index" json:"pinned"`
// LastUsed is updated whenever a profile is bound to a session that
// reached CONNECTED. NULL until the first successful connection.
LastUsed *time.Time `gorm:"index" json:"last_used,omitempty"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
AX25SessionProfile is a saved BBS shortcut. Operators can pin recents and the terminal UI surfaces both pinned and recent profiles in the pre-connect form. See plan §3d.
type AX25TerminalConfig ¶ added in v0.12.4
type AX25TerminalConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
ScrollbackRows uint32 `gorm:"not null;default:1000" json:"scrollback_rows"`
CursorBlink bool `gorm:"not null;default:false" json:"cursor_blink"`
DefaultModulo uint32 `gorm:"not null;default:8" json:"default_modulo"`
DefaultPaclen uint32 `gorm:"not null;default:256" json:"default_paclen"`
// MacrosJSON stores `[{"label": "...", "payload": "<base64>"}]`. Kept
// as a JSON-text column so the schema does not have to evolve as the
// macro shape grows; the REST DTO marshals to a typed array.
MacrosJSON string `gorm:"type:text;not null;default:'[]'" json:"-"`
RawTailFilter string `gorm:"not null;default:''" json:"raw_tail_filter"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
AX25TerminalConfig is a singleton (id=1) row holding terminal-route UI preferences and operator-defined macros. See plan §3c.1 for the fields and how the bridge consumes them.
type AX25TranscriptEntry ¶ added in v0.12.4
type AX25TranscriptEntry struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
SessionID uint32 `gorm:"not null;index" json:"session_id"`
TS time.Time `gorm:"not null;index" json:"ts"`
Direction string `gorm:"not null" json:"direction"` // rx|tx
Kind string `gorm:"not null" json:"kind"` // data|event
Payload []byte `gorm:"" json:"payload,omitempty"`
}
AX25TranscriptEntry is one line in a transcript: a single observed data block or a link-state event timestamp.
type AX25TranscriptSession ¶ added in v0.12.4
type AX25TranscriptSession struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
ChannelID uint32 `gorm:"not null;index" json:"channel_id"`
PeerCall string `gorm:"not null;index" json:"peer_call"`
PeerSSID uint8 `gorm:"column:peer_ssid;not null;default:0" json:"peer_ssid"`
ViaPath string `gorm:"not null;default:''" json:"via_path"`
StartedAt time.Time `gorm:"not null;index" json:"started_at"`
EndedAt *time.Time `gorm:"" json:"ended_at,omitempty"`
EndReason string `gorm:"not null;default:''" json:"end_reason"`
ByteCount uint64 `gorm:"not null;default:0" json:"byte_count"`
FrameCount uint64 `gorm:"not null;default:0" json:"frame_count"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
AX25TranscriptSession is a single recorded link, populated when the operator toggles transcript on for that session. See plan §3e.
type Action ¶ added in v0.13.0
type Action struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"uniqueIndex;size:32;not null"`
Description string
Type string `gorm:"size:16;not null"` // 'command' | 'webhook'
CommandPath string
WorkingDir string
WebhookMethod string `gorm:"size:8"` // 'GET' | 'POST'
WebhookURL string
WebhookHeaders string `gorm:"type:text;default:'{}'"` // JSON map
WebhookBodyTemplate string `gorm:"type:text"`
TimeoutSec int `gorm:"not null;default:10"`
// `default:true` on a gorm bool tag is a footgun: gorm uses it as
// the value to send when the Go field is its zero value, which makes
// a genuine `false` from the wire indistinguishable from "field not
// set". A fresh action created with the toggle off would silently
// persist as enabled. The DDL still carries DEFAULT 1 (see
// migrate_actions.go) for downgrade-safety; the application layer
// always provides an explicit value via the dto.Action wire shape,
// so dropping the gorm-side default here costs nothing.
OTPRequired bool `gorm:"not null"`
OTPCredentialID *uint `gorm:"column:otp_credential_id"` // FK to OTPCredential, nullable; ON DELETE SET NULL
SenderAllowlist string // CSV
ArgSchema string `gorm:"type:text;default:'[]'"` // JSON list
ArgMode string `gorm:"size:16;not null;default:'kv'"`
RateLimitSec int `gorm:"not null;default:5"`
QueueDepth int `gorm:"not null;default:8"`
MaxReplyLines int `gorm:"not null;default:1"`
Enabled bool `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
Action is one operator-defined trigger. Identified by Name (used as the message keyword). Type switches between command and webhook execution; the type-specific fields are nullable.
func (*Action) BeforeSave ¶ added in v0.13.0
BeforeSave normalizes Name to uppercase and trims whitespace before insert or update. The on-air Action grammar treats names as case-insensitive; canonical form on the wire and in the audit log is uppercase. Normalizing on write keeps the unique index on `name` collision-free across mixed-case operator input.
type ActionInvocation ¶ added in v0.13.0
type ActionInvocation struct {
ID uint `gorm:"primaryKey"`
ActionID *uint `gorm:"index"`
ActionNameAt string `gorm:"size:64"`
SenderCall string `gorm:"size:9;index"`
Source string `gorm:"size:4"` // 'rf' | 'is'
OTPCredentialID *uint `gorm:"column:otp_credential_id"`
OTPVerified bool
RawArgsJSON string `gorm:"type:text"`
Status string `gorm:"size:24"`
StatusDetail string
ExitCode *int
HTTPStatus *int
OutputCapture string `gorm:"type:text"`
ReplyText string
Truncated bool
ReplyLineCount int `gorm:"not null;default:1"`
CreatedAt time.Time `gorm:"index"`
}
ActionInvocation is the per-attempt audit row. ActionID is nullable so an invocation that resolved to status=unknown still gets logged. ActionNameAt is denormalized so a row remains readable after the underlying Action is deleted.
type ActionInvocationFilter ¶ added in v0.13.0
type ActionInvocationFilter struct {
ActionID *uint
SenderCall string
Status string
Source string // 'rf' | 'is'
Search string
Limit int
Offset int
}
ActionInvocationFilter narrows a ListActionInvocations call. All fields are optional; the zero filter returns the most recent rows up to the default limit. Search is a case-insensitive substring match applied to action_name_at, sender_call, and status_detail in a single OR predicate so the UI's free-text box can probe multiple columns without the caller having to choose.
type ActionListenerAddressee ¶ added in v0.13.0
type ActionListenerAddressee struct {
ID uint `gorm:"primaryKey"`
Addressee string `gorm:"uniqueIndex;size:9;not null"`
CreatedAt time.Time
}
ActionListenerAddressee extends the Actions trigger surface with an extra APRS addressee (e.g. "GWACT") independent of the station call or tactical aliases. Ships empty.
type AgwConfig ¶
type AgwConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
ListenAddr string `gorm:"not null;default:'0.0.0.0:8000'" json:"listen_addr"`
Callsigns string `gorm:"not null;default:'N0CALL'" json:"callsigns"` // CSV; one per AGW port
Enabled bool `gorm:"not null;default:false" json:"enabled"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
AgwConfig is a singleton (id=1) row describing the AGWPE listener.
type AudioDevice ¶
type AudioDevice struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"not null" json:"name"`
Direction string `gorm:"not null;default:'input'" json:"direction"` // input|output
SourceType string `gorm:"not null" json:"source_type"` // soundcard|flac|stdin|sdr_udp
SourcePath string `json:"device_path"` // cpal name or file path
SampleRate uint32 `gorm:"not null;default:48000" json:"sample_rate"`
Channels uint32 `gorm:"not null;default:1" json:"channels"`
Format string `gorm:"not null;default:'s16le'" json:"format"`
GainDB float32 `gorm:"not null;default:0" json:"gain_db"` // software gain: -60 to +12 dB, 0 = unity
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
AudioDevice describes a single audio input source feeding the modem. SourceType selects how the Rust modem opens the device:
- "soundcard": cpal device by name (DeviceName is cpal name)
- "flac": file playback (DeviceName/SourcePath is file path)
- "stdin": raw s16le on stdin
- "sdr_udp": SDR UDP stream (later phases)
type Beacon ¶
type Beacon struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Type string `gorm:"not null;default:'position'" json:"type"` // position|object|tracker|custom|igate
Channel uint32 `gorm:"not null;default:1" json:"channel"`
Callsign string `gorm:"not null" json:"callsign"`
Destination string `gorm:"not null;default:'APGRWO'" json:"destination"`
Path string `gorm:"not null;default:'WIDE1-1'" json:"path"`
UseGps bool `gorm:"column:use_gps;default:false" json:"use_gps"` // source lat/lon/alt from GPS cache instead of fixed fields
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
AltFt float64 `json:"alt_ft"` // altitude in feet for position reports
Ambiguity uint32 `gorm:"not null;default:0" json:"ambiguity"`
SymbolTable string `gorm:"not null;default:'/'" json:"symbol_table"`
Symbol string `gorm:"not null;default:'-'" json:"symbol"`
Overlay string `json:"overlay"` // alternate symbol table overlay character
Compress bool `gorm:"not null;default:true" json:"compress"` // use 13-byte base-91 compressed position encoding (APRS101 ch 9)
Messaging bool `gorm:"not null;default:false" json:"messaging"` // '=' instead of '!' prefix
Comment string `json:"comment"`
CommentCmd string `json:"comment_cmd"` // shell command whose stdout is appended as comment
CustomInfo string `json:"custom_info"` // raw info field override for Type=="custom"
ObjectName string `json:"object_name"` // for Type=="object"
Power uint32 `gorm:"not null;default:0" json:"power"` // watts for PHG
Height uint32 `gorm:"not null;default:0" json:"height"` // feet HAAT for PHG
Gain uint32 `gorm:"not null;default:0" json:"gain"` // dBi for PHG
Dir uint32 `gorm:"not null;default:0" json:"dir"` // antenna direction 0..8 for PHG
Freq string `json:"freq"` // frequency string for freq info
Tone string `json:"tone"` // CTCSS/DCS tone
FreqOffset string `json:"freq_offset"` // repeater offset
DelaySeconds uint32 `gorm:"not null;default:30" json:"delay_seconds"`
EverySeconds uint32 `gorm:"not null;default:1800" json:"interval"`
SlotSeconds int32 `gorm:"not null;default:-1" json:"slot_seconds"`
SmartBeacon bool `gorm:"not null;default:false" json:"smart_beacon"`
// Deprecated: use the global configstore.SmartBeaconConfig instead.
// This column is no longer read as of 2026-04-18 (the SmartBeacon
// curve is now a global singleton, matching direwolf). The column
// will be dropped in a future migration once all deployments have
// moved to the global config. See
// .context/2026-04-18-smart-beacon-implementation.md.
SbFastSpeed uint32 `gorm:"default:60" json:"sb_fast_speed"`
// Deprecated: use the global configstore.SmartBeaconConfig instead.
// This column is no longer read as of 2026-04-18 (the SmartBeacon
// curve is now a global singleton, matching direwolf). The column
// will be dropped in a future migration once all deployments have
// moved to the global config. See
// .context/2026-04-18-smart-beacon-implementation.md.
SbSlowSpeed uint32 `gorm:"default:5" json:"sb_slow_speed"`
// Deprecated: use the global configstore.SmartBeaconConfig instead.
// This column is no longer read as of 2026-04-18 (the SmartBeacon
// curve is now a global singleton, matching direwolf). The column
// will be dropped in a future migration once all deployments have
// moved to the global config. See
// .context/2026-04-18-smart-beacon-implementation.md.
SbFastRate uint32 `gorm:"default:60" json:"sb_fast_rate"`
// Deprecated: use the global configstore.SmartBeaconConfig instead.
// This column is no longer read as of 2026-04-18 (the SmartBeacon
// curve is now a global singleton, matching direwolf). The column
// will be dropped in a future migration once all deployments have
// moved to the global config. See
// .context/2026-04-18-smart-beacon-implementation.md.
SbSlowRate uint32 `gorm:"default:1800" json:"sb_slow_rate"`
// Deprecated: use the global configstore.SmartBeaconConfig instead.
// This column is no longer read as of 2026-04-18 (the SmartBeacon
// curve is now a global singleton, matching direwolf). The column
// will be dropped in a future migration once all deployments have
// moved to the global config. See
// .context/2026-04-18-smart-beacon-implementation.md.
SbTurnAngle uint32 `gorm:"default:30" json:"sb_turn_angle"`
// Deprecated: use the global configstore.SmartBeaconConfig instead.
// This column is no longer read as of 2026-04-18 (the SmartBeacon
// curve is now a global singleton, matching direwolf). The column
// will be dropped in a future migration once all deployments have
// moved to the global config. See
// .context/2026-04-18-smart-beacon-implementation.md.
SbTurnSlope uint32 `gorm:"default:255" json:"sb_turn_slope"`
// Deprecated: use the global configstore.SmartBeaconConfig instead.
// This column is no longer read as of 2026-04-18 (the SmartBeacon
// curve is now a global singleton, matching direwolf). The column
// will be dropped in a future migration once all deployments have
// moved to the global config. See
// .context/2026-04-18-smart-beacon-implementation.md.
SbMinTurnTime uint32 `gorm:"default:5" json:"sb_min_turn_time"`
SendToAPRSIS bool `gorm:"column:send_to_aprs_is;not null;default:false" json:"send_to_aprs_is"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
Beacon is a scheduled beacon. Type selects the payload builder.
type Channel ¶
type Channel struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"not null" json:"name"`
InputDeviceID *uint32 `gorm:"index" json:"input_device_id"`
InputDevice *AudioDevice `gorm:"foreignKey:InputDeviceID;references:ID;constraint:OnDelete:RESTRICT,OnUpdate:RESTRICT" json:"-"`
InputChannel uint32 `gorm:"not null;default:0" json:"input_channel"` // 0=left/mono, 1=right
OutputDeviceID uint32 `gorm:"not null;default:0;index" json:"output_device_id"` // 0=RX-only; soft FK, see type comment
OutputChannel uint32 `gorm:"not null;default:0" json:"output_channel"`
ModemType string `gorm:"not null;default:'afsk'" json:"modem_type"`
BitRate uint32 `gorm:"not null;default:1200" json:"bit_rate"`
MarkFreq uint32 `gorm:"not null;default:1200" json:"mark_freq"`
SpaceFreq uint32 `gorm:"not null;default:2200" json:"space_freq"`
Profile string `gorm:"not null;default:'A'" json:"profile"`
NumSlicers uint32 `gorm:"not null;default:1" json:"num_slicers"`
FixBits string `gorm:"not null;default:'none'" json:"fix_bits"` // none|single|double
FX25Encode bool `gorm:"not null;default:false" json:"fx25_encode"`
IL2PEncode bool `gorm:"column:il2p_encode;not null;default:false" json:"il2p_encode"`
NumDecoders uint32 `gorm:"not null;default:1" json:"num_decoders"`
DecoderOffset int32 `gorm:"not null;default:0" json:"decoder_offset"`
Mode string `gorm:"not null;default:'aprs'" json:"mode"` // aprs|packet|aprs+packet
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
Channel is a logical radio channel optionally tied to an audio device.
Foreign-key policy:
- InputDeviceID is a *pointer-typed soft FK* to AudioDevice.ID: a nil value means "KISS-only channel — no modem, no audio". When non-nil, the value must reference an existing input-direction device (enforced at the application layer in validateChannel). Phase 2 migrated the column from NOT NULL to NULL to allow channels that are serviced only by a KISS TNC interface. DeleteAudioDeviceChecked still walks both input and output references to refuse / cascade a device delete that would orphan channels.
- OutputDeviceID is a *soft* FK, not enforced by SQLite. The column is a plain uint32 where 0 means "RX-only" (no output device). SQLite FK constraints treat any non-NULL value as a reference, so a stored 0 would fail the constraint, and making the column nullable would ripple through DTOs and protobuf mappings for no gain. The relation is validated at the application layer in validateChannel, and DeleteAudioDeviceChecked walks both input and output references.
When InputDeviceID is nil, ModemType / BitRate / MarkFreq / SpaceFreq / Profile / NumSlicers / FixBits / FX25Encode / IL2PEncode / NumDecoders / DecoderOffset are stored unchanged but effectively unused: the modem subprocess is never told about the channel (see pkg/modembridge/session.go pushConfiguration, which skips nil-input channels). They round-trip through the UI so a future Convert flow can flip a channel back to modem-backed without losing the operator's last known values.
type ChannelModeLookup ¶ added in v0.12.4
type ChannelModeLookup interface {
ModeForChannel(ctx context.Context, channelID uint32) (string, error)
}
ChannelModeLookup is the small read-only surface the TX-gating subsystems (beacon, digipeater, igate, messages, ax25conn) consume to decide whether to permit a transmit on a given channel. The concrete *Store implements it; tests can substitute a fake.
type ConfigStore ¶
type ConfigStore interface {
// Audio devices
CreateAudioDevice(ctx context.Context, d *AudioDevice) error
GetAudioDevice(ctx context.Context, id uint32) (*AudioDevice, error)
ListAudioDevices(ctx context.Context) ([]AudioDevice, error)
UpdateAudioDevice(ctx context.Context, d *AudioDevice) error
DeleteAudioDevice(ctx context.Context, id uint32) error
// Channels
CreateChannel(ctx context.Context, c *Channel) error
GetChannel(ctx context.Context, id uint32) (*Channel, error)
ListChannels(ctx context.Context) ([]Channel, error)
UpdateChannel(ctx context.Context, c *Channel) error
DeleteChannel(ctx context.Context, id uint32) error
SetChannelFX25(ctx context.Context, id uint32, enable bool) error
SetChannelIL2P(ctx context.Context, id uint32, enable bool) error
// PTT
UpsertPttConfig(ctx context.Context, p *PttConfig) error
GetPttConfigForChannel(ctx context.Context, channelID uint32) (*PttConfig, error)
// TX timing
ListTxTimings(ctx context.Context) ([]TxTiming, error)
GetTxTiming(ctx context.Context, channel uint32) (*TxTiming, error)
UpsertTxTiming(ctx context.Context, t *TxTiming) error
// KISS interfaces
ListKissInterfaces(ctx context.Context) ([]KissInterface, error)
GetKissInterface(ctx context.Context, id uint32) (*KissInterface, error)
CreateKissInterface(ctx context.Context, k *KissInterface) error
UpdateKissInterface(ctx context.Context, k *KissInterface) error
DeleteKissInterface(ctx context.Context, id uint32) error
// AGW
GetAgwConfig(ctx context.Context) (*AgwConfig, error)
UpsertAgwConfig(ctx context.Context, c *AgwConfig) error
// Digipeater
GetDigipeaterConfig(ctx context.Context) (*DigipeaterConfig, error)
UpsertDigipeaterConfig(ctx context.Context, c *DigipeaterConfig) error
ListDigipeaterRules(ctx context.Context) ([]DigipeaterRule, error)
ListDigipeaterRulesForChannel(ctx context.Context, channel uint32) ([]DigipeaterRule, error)
CreateDigipeaterRule(ctx context.Context, r *DigipeaterRule) error
UpdateDigipeaterRule(ctx context.Context, r *DigipeaterRule) error
DeleteDigipeaterRule(ctx context.Context, id uint32) error
// iGate
GetIGateConfig(ctx context.Context) (*IGateConfig, error)
UpsertIGateConfig(ctx context.Context, c *IGateConfig) error
ListIGateRfFilters(ctx context.Context) ([]IGateRfFilter, error)
ListIGateRfFiltersForChannel(ctx context.Context, channel uint32) ([]IGateRfFilter, error)
CreateIGateRfFilter(ctx context.Context, f *IGateRfFilter) error
UpdateIGateRfFilter(ctx context.Context, f *IGateRfFilter) error
DeleteIGateRfFilter(ctx context.Context, id uint32) error
// Beacons
ListBeacons(ctx context.Context) ([]Beacon, error)
GetBeacon(ctx context.Context, id uint32) (*Beacon, error)
CreateBeacon(ctx context.Context, b *Beacon) error
UpdateBeacon(ctx context.Context, b *Beacon) error
DeleteBeacon(ctx context.Context, id uint32) error
// GPS
GetGPSConfig(ctx context.Context) (*GPSConfig, error)
UpsertGPSConfig(ctx context.Context, c *GPSConfig) error
// Packet filters
ListPacketFilters(ctx context.Context) ([]PacketFilter, error)
}
ConfigStore defines the persistence contract for graywolf configuration. The concrete *Store satisfies this interface; consumers should depend on ConfigStore to enable testing with fakes.
type DigipeaterConfig ¶
type DigipeaterConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Enabled bool `gorm:"not null;default:false" json:"enabled"`
DedupeWindowSeconds uint32 `gorm:"not null;default:30" json:"dedupe_window_seconds"`
MyCall string `gorm:"not null;default:'N0CALL'" json:"my_call"` // local callsign used for preemptive digi
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
DigipeaterConfig is a singleton (id=1) row with global digipeater settings.
type DigipeaterRule ¶
type DigipeaterRule struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
FromChannel uint32 `gorm:"not null;index" json:"from_channel"`
ToChannel uint32 `gorm:"not null" json:"to_channel"`
Alias string `gorm:"not null" json:"alias"`
AliasType string `gorm:"not null;default:'widen'" json:"alias_type"` // widen|exact|trace
MaxHops uint32 `gorm:"not null;default:2" json:"max_hops"` // maximum N-N accepted (e.g. WIDE2-2)
Action string `gorm:"not null;default:'repeat'" json:"action"`
Priority uint32 `gorm:"not null;default:100" json:"priority"` // lower = evaluated first
Enabled bool `gorm:"not null;default:true" json:"enabled"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
DigipeaterRule is one per-channel digipeater alias/rule. The digi engine walks rules in Priority ascending order looking for a match against an unconsumed path entry.
Action enumeration:
"repeat" — retransmit on ToChannel, consume this alias slot "drop" — match and suppress (filter-only rule)
AliasType enumeration:
"widen" — WIDEn-N style (Alias is the base e.g. "WIDE"; consumes 1 hop, decrements SSID) "exact" — exact callsign match (Alias is full "CALL[-SSID]"); e.g. the local callsign (preemptive) "trace" — TRACEn-N behaves like WIDEn-N but also inserts the local callsign before the alias
type GPSConfig ¶
type GPSConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
SourceType string `gorm:"not null;default:'none'" json:"source"` // none|serial|gpsd
Device string `json:"serial_port"` // serial device path, e.g. /dev/ttyUSB0
BaudRate uint32 `gorm:"not null;default:4800" json:"baud_rate"`
GpsdHost string `gorm:"not null;default:'localhost'" json:"gpsd_host"`
GpsdPort uint32 `gorm:"not null;default:2947" json:"gpsd_port"`
Enabled bool `gorm:"not null;default:false" json:"enabled"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
GPSConfig is a singleton (id=1) row for the GPS receiver.
type IGateConfig ¶
type IGateConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Enabled bool `gorm:"not null;default:false" json:"enabled"`
Server string `gorm:"not null;default:'rotate.aprs2.net'" json:"server"`
Port uint32 `gorm:"not null;default:14580" json:"port"`
ServerFilter string `json:"server_filter"` // APRS-IS server-side filter expression
SimulationMode bool `gorm:"not null;default:false" json:"simulation_mode"`
GateRfToIs bool `gorm:"not null;default:true" json:"gate_rf_to_is"`
GateIsToRf bool `gorm:"not null;default:false" json:"gate_is_to_rf"`
RfChannel uint32 `gorm:"not null;default:0" json:"rf_channel"` // channel used when gating IS->RF; 0 = unset
MaxMsgHops uint32 `gorm:"not null;default:2" json:"max_msg_hops"` // WIDE hops for IS->RF messages
SoftwareName string `gorm:"not null;default:'graywolf'" json:"software_name"` // APRS-IS login banner software name
SoftwareVersion string `gorm:"not null;default:'0.1'" json:"software_version"` // APRS-IS login banner version
// TxChannel governs IS->RF on this iGate. The messages-tx channel
// moved to MessagesConfig.TxChannel; this column stays because
// migration 13 (`messages_config_singleton`) reads it once on first
// run to seed MessagesConfig. Do not remove without first deleting
// that migration -- operators upgrading from older builds will
// silently lose their messages TX channel otherwise.
TxChannel uint32 `gorm:"not null;default:0" json:"tx_channel"` // radio channel for IS->RF submissions; 0 = unset
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
IGateConfig is a singleton (id=1) row for the iGate.
Callsign and Passcode columns remain in the DB for forward-safety on downgrade, but are no longer read/written by application code. See .context/2026-04-21-centralized-station-callsign.md §D4.
type IGateRfFilter ¶
type IGateRfFilter struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Channel uint32 `gorm:"not null;index" json:"channel"`
Type string `gorm:"not null" json:"type"` // callsign|prefix|message_dest|object
Pattern string `gorm:"not null" json:"pattern"`
Action string `gorm:"not null;default:'allow'" json:"action"` // allow|deny
Priority uint32 `gorm:"not null;default:100" json:"priority"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
IGateRfFilter is a per-channel allow/deny rule used to decide which RF-originated packets are forwarded to APRS-IS. Evaluation: lowest Priority first (ascending order); first match determines action.
type KissInterface ¶
type KissInterface struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"not null;uniqueIndex" json:"name"`
InterfaceType string `gorm:"not null;default:'tcp'" json:"type"` // tcp|tcp-client|serial|bluetooth
ListenAddr string `json:"listen_addr"` // host:port for tcp (server-listen)
Device string `json:"serial_device"` // /dev/ttyUSB0 or bluetooth mac
BaudRate uint32 `gorm:"default:9600" json:"baud_rate"`
Channel uint32 `gorm:"not null;default:1" json:"channel"` // default radio channel for this interface
Broadcast bool `gorm:"not null;default:true" json:"broadcast"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
Mode string `gorm:"not null;default:'modem'" json:"mode"` // modem|tnc
TncIngressRateHz uint32 `gorm:"not null;default:50" json:"tnc_ingress_rate_hz"` // token-bucket refill, frames/sec
TncIngressBurst uint32 `gorm:"not null;default:100" json:"tnc_ingress_burst"` // token-bucket size
// InterfaceType == "tcp-client" uses RemoteHost / RemotePort as the
// dial target and ReconnectInitMs / ReconnectMaxMs to size the
// supervisor's backoff schedule. ListenAddr is ignored on tcp-client
// rows. Unused / zero on all other interface types; see Phase 4 in
// .context/2026-04-20-kiss-tcp-client-and-channel-backing.md.
RemoteHost string `gorm:"column:remote_host;not null;default:''" json:"remote_host"`
RemotePort uint16 `gorm:"column:remote_port;not null;default:0" json:"remote_port"`
ReconnectInitMs uint32 `gorm:"column:reconnect_init_ms;not null;default:1000" json:"reconnect_init_ms"`
ReconnectMaxMs uint32 `gorm:"column:reconnect_max_ms;not null;default:300000" json:"reconnect_max_ms"`
// AllowTxFromGovernor: when true (and Mode == KissModeTnc), this
// interface is registered as a KissTnc TX backend and the
// dispatcher fan-outs governor-scheduled frames (beacon / digi /
// iGate IS→RF / KISS / AGW submissions) for this channel to it.
// Default false so existing TNC-mode rows that users configured
// before Phase 3 do NOT silently start transmitting. Phase 4 sets
// the DTO default to true for newly-created tcp-client rows only.
// Modem-mode rows ignore this flag entirely (they TX via Submit,
// they don't receive TX from the governor).
AllowTxFromGovernor bool `gorm:"column:allow_tx_from_governor;not null;default:false" json:"allow_tx_from_governor"`
// NeedsReconfig is set to true when a referential cascade (Phase 5)
// nulls this row's Channel. Phase 3 merely declares the column so
// the shape is stable before the cascade logic lands; no code reads
// it yet.
NeedsReconfig bool `gorm:"column:needs_reconfig;not null;default:false" json:"needs_reconfig"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
KissInterface represents one row in kiss_interfaces. Each Server in pkg/kiss corresponds to one row. InterfaceType is "tcp"|"serial"| "bluetooth"; for serial/bluetooth the Device and BaudRate are used and ListenAddr may be empty.
Mode selects the per-interface routing policy:
- KissModeModem (default): peer is an APRS app; frames it sends are queued for RF transmission.
- KissModeTnc: peer is a hardware TNC supplying off-air RX; frames are fanned out to digi/igate/messages/station cache, not auto-submitted to TX. See .context/2026-04-19-kiss-modem-tnc-mode.md.
TncIngressRateHz and TncIngressBurst configure the per-interface token-bucket ingress cap consumed in TNC mode (wired in Phase 3). The fields are stored and surfaced for every row regardless of mode so the operator's choice survives a mode flip.
type LogBufferConfig ¶
type LogBufferConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
MaxRows int `gorm:"not null;default:0" json:"max_rows"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
LogBufferConfig stores the operator's override for the in-database log ring size. Singleton at id=1. MaxRows == 0 disables persistence entirely (back to console-only logging). When no row exists, the logbuffer package picks an environment-aware default (2000 on Pi / SD-card / ramdisk, 5000 on disk-backed systems).
type MapsConfig ¶
type MapsConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Source string `gorm:"not null;default:'graywolf'" json:"source"`
Callsign string `gorm:"not null;default:''" json:"callsign"`
Token string `gorm:"not null;default:''" json:"-"`
// RegisteredAt is the zero time when no registration has occurred;
// kept as a value type (not *time.Time) so the JSON contract is
// always a string and Token=="" remains the single source of truth
// for whether this device is registered.
RegisteredAt time.Time `json:"registered_at"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
MapsConfig is the singleton row that captures the operator's basemap source choice plus the device-local registration with auth.nw5w.com. Source is one of "osm" (public OSM raster tiles) or "graywolf" (private maps.nw5w.com vector tiles, requires Token). Graywolf is the default; an empty Token means the device hasn't registered yet, and the maplibre frontend falls back to OSM rendering until it does.
type MapsDownload ¶
type MapsDownload struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Slug string `gorm:"not null;uniqueIndex" json:"slug"`
Status string `gorm:"not null;default:'pending'" json:"status"` // pending|downloading|complete|error
BytesTotal int64 `gorm:"not null;default:0" json:"bytes_total"`
BytesDownloaded int64 `gorm:"not null;default:0" json:"bytes_downloaded"`
DownloadedAt time.Time `json:"downloaded_at"`
ErrorMessage string `gorm:"not null;default:''" json:"error_message,omitempty"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
MapsDownload tracks one state's offline PMTiles archive. The file itself lives at <tile-cache-dir>/<slug>.pmtiles; this row is just the metadata. Status transitions: pending -> downloading -> complete | error. A retry restarts at pending.
type Message ¶
type Message struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Direction string `gorm:"not null;index:idx_msg_direction_unread,priority:1" json:"direction"` // "in" | "out"
OurCall string `gorm:"size:9;not null;index:idx_msg_peer,priority:1;index:idx_msg_to_time,priority:1" json:"our_call"`
PeerCall string `gorm:"size:9;not null;index:idx_msg_peer,priority:2;index:idx_msg_peer_time" json:"peer_call"`
FromCall string `gorm:"size:9;not null;index:idx_msg_from_time,priority:1;index:idx_msg_msgid_from,priority:2" json:"from_call"`
ToCall string `gorm:"size:9;not null;index:idx_msg_to_time,priority:2" json:"to_call"`
Text string `gorm:"size:200;not null" json:"text"`
MsgID string `gorm:"size:5;index:idx_msg_msgid_from,priority:1" json:"msg_id"`
CreatedAt time.Time `` /* 198-byte string literal not displayed */
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
ReceivedAt *time.Time `json:"received_at,omitempty"`
SentAt *time.Time `json:"sent_at,omitempty"`
AckedAt *time.Time `json:"acked_at,omitempty"`
AckState string `gorm:"size:16;not null;default:'none'" json:"ack_state"` // none | acked | rejected | broadcast
Source string `gorm:"size:4;not null;default:''" json:"source"` // rf | is (string form of aprs.Direction)
Channel uint32 `gorm:"not null;default:0" json:"channel"`
Path string `gorm:"size:64" json:"path"` // display path, e.g. "W1ABC*,WIDE1-1*"
Via string `gorm:"size:64" json:"via"` // last used digipeater
RawTNC2 string `gorm:"column:raw_tnc2;size:512" json:"raw_tnc2"` // archival raw text
Unread bool `gorm:"not null;default:false;index:idx_msg_direction_unread,priority:2" json:"unread"`
Attempts uint32 `gorm:"not null;default:0" json:"attempts"`
NextRetryAt *time.Time `json:"next_retry_at,omitempty"`
FailureReason string `gorm:"size:128" json:"failure_reason"`
ReplyAckID string `gorm:"size:5" json:"reply_ack_id"` // inbound: APRS11 reply-ack id we observed
IsAck bool `gorm:"not null;default:false" json:"is_ack"`
IsRej bool `gorm:"not null;default:false" json:"is_rej"`
IsBulletin bool `gorm:"not null;default:false" json:"is_bulletin"`
IsNWS bool `gorm:"column:is_nws;not null;default:false" json:"is_nws"`
PreferIS bool `gorm:"column:prefer_is;not null;default:false" json:"prefer_is"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ThreadKind string `gorm:"size:10;not null;default:'dm';index:idx_msg_thread,priority:1" json:"thread_kind"` // dm | tactical
ThreadKey string `gorm:"size:9;not null;default:'';index:idx_msg_thread,priority:2" json:"thread_key"` // peer callsign for dm, tactical label for tactical
ReceivedByCall string `gorm:"size:9" json:"received_by_call"` // tactical outbound: first acker's call
// Kind classifies the message body so the UI can render specialized
// affordances (e.g. an Accept button for tactical invites) without
// having to re-parse the wire text. Defaults to "text"; "invite"
// marks a `!GW1 INVITE <TAC>` DM. The CHECK constraint pins the
// enum at the SQL layer as a backstop against accidental writes of
// other values from SQL shells or future migrations. No index — the
// column is never a query predicate, only a display tag.
Kind string `gorm:"size:10;not null;default:'text';check:kind IN ('text','invite')" json:"kind"`
// InviteTactical is the tactical callsign referenced by an invite
// message. Empty when Kind != "invite". Size 9 mirrors ThreadKey /
// TacticalCallsign.Callsign.
InviteTactical string `gorm:"size:9;not null;default:''" json:"invite_tactical"`
// InviteAcceptedAt records when the local operator accepted this
// invite. Audit-only: UI rendering of "Joined" keys off the live
// TacticalSet cache, not this column, so first-paint is race-free
// on refresh. Nil until accept. No index.
InviteAcceptedAt *time.Time `json:"invite_accepted_at,omitempty"`
}
Message is one persisted APRS text message, DM or tactical, in either direction. Columns cover the full lifecycle: receipt metadata, state transitions (SentAt/AckedAt/AckState/Attempts), retry scheduling (NextRetryAt + FailureReason), ack/reply-ack correlation (MsgID + ReplyAckID), and thread identity ((ThreadKind, ThreadKey) — "dm" uses peer callsign, "tactical" uses the tactical label).
Lifecycle columns set by the repository:
- Insert: CreatedAt, ReceivedAt (inbound), Direction, FromCall, ToCall, OurCall, ThreadKind, ThreadKey, PeerCall, Text, Unread, etc. The repository derives ThreadKey + PeerCall at insert and writes them directly — callers only need to set ThreadKind and the direction-dependent raw callsigns.
- Send pipeline (Phase 3): QueuedAt, SentAt, AckState, Attempts, NextRetryAt, FailureReason.
- Router (Phase 2): AckedAt, AckState, ReceivedByCall (for tactical reply-ack correlation).
ThreadKind is one of: "dm" (1:1) or "tactical" (group broadcast via tactical callsign). See the APRS messages feature plan for the full design.
type MessageCounter ¶
type MessageCounter struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"-"`
NextID uint32 `gorm:"not null;default:1" json:"next_id"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
MessageCounter is a singleton (id=1) holding the next msgid to allocate. NextID rolls 1..999; allocation skips values currently held by outstanding outbound DM rows (see pkg/messages/store.go AllocateMsgID). Separate from MessagePreferences so bumping the counter does not touch the preferences row.
type MessagePreferences ¶
type MessagePreferences struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"-"`
FallbackPolicy string `gorm:"size:16;not null;default:'is_fallback'" json:"fallback_policy"` // rf_only | is_fallback | is_only | both
DefaultPath string `gorm:"size:64;not null;default:'WIDE1-1,WIDE2-1'" json:"default_path"`
RetryMaxAttempts uint32 `gorm:"not null;default:4" json:"retry_max_attempts"`
RetentionDays uint32 `gorm:"not null;default:0" json:"retention_days"` // 0 = forever
// MaxMessageTextOverride raises the default 67-char cap on
// addressee-line direct messages up to 200. 0 (the column default,
// and the value seen on pre-upgrade rows after GORM AutoMigrate
// adds the column) means "use the default 67". Valid non-zero
// values fall in [68, 200]; the webapi DTO validator rejects
// anything outside that range. Applies to addressee-line DMs only:
// bulletins, status beacons, and position/weather frames are
// unaffected.
MaxMessageTextOverride uint32 `gorm:"not null;default:0" json:"max_message_text_override"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
MessagePreferences is a singleton (id=1) holding operator-level messaging preferences. Seeded at migrate-time with defaults if no row exists. See plan Phase 3 for semantics.
type MessagesConfig ¶ added in v0.12.4
type MessagesConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
TxChannel uint32 `gorm:"not null;default:0" json:"tx_channel"` // 0 = auto-resolve at runtime
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
MessagesConfig is a singleton (id=1) row that owns messaging-specific settings. TxChannel moved here from IGateConfig; iGate retains its own TxChannel which now governs IS->RF only.
type OTPCredential ¶ added in v0.13.0
type OTPCredential struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"uniqueIndex;size:64;not null"`
Issuer string `gorm:"size:64"`
Account string `gorm:"size:128"`
Algorithm string `gorm:"size:16;not null;default:'SHA1'"`
Digits int `gorm:"not null;default:6"`
Period int `gorm:"not null;default:30"`
SecretB32 string `gorm:"size:64;not null"`
CreatedAt time.Time
LastUsedAt *time.Time
}
OTPCredential is one TOTP secret. Stored plaintext; UI surfaces the secret only once at create time and never reads it back.
type OrphanChannelRefRows ¶
type OrphanChannelRefRows struct {
// Token mirrors one of the ReferrerType* constants above so callers
// can key their log field consistently with the 409 response body.
Token string
// RowIDs is the set of row primary keys that have a dangling ref.
RowIDs []uint32
// MissingChannelIDs is the deduplicated set of channel ids those
// rows point at but that no longer exist. May have fewer entries
// than RowIDs when several rows share the same missing channel.
MissingChannelIDs []uint32
}
OrphanChannelRefRows describes one table's set of rows whose channel soft-FK column points at a non-existent channel id. Used by the bootstrap audit in pkg/app/wiring.go to emit a per-table WARN line listing the affected row ids plus the distinct missing channel ids, so operators can locate the referrers without clicking through every list page.
type PacketFilter ¶
type PacketFilter struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Channel uint32 `gorm:"not null;index" json:"channel"`
Name string `gorm:"not null" json:"name"`
Expr string `gorm:"not null" json:"expr"`
Action string `gorm:"not null;default:'allow'" json:"action"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
PacketFilter is a reserved stub table for future per-channel packet filters (Phase 5/6).
type PositionLogConfig ¶
type PositionLogConfig struct {
ID uint32 `gorm:"primaryKey" json:"id"`
Enabled bool `gorm:"not null;default:false" json:"enabled"`
DBPath string `gorm:"not null;default:'./graywolf-history.db'" json:"db_path"`
}
PositionLogConfig controls the optional persistent position history database. Disabled by default to protect SD-card-based systems.
type PttConfig ¶
type PttConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
ChannelID uint32 `gorm:"not null;uniqueIndex" json:"channel_id"`
Channel *Channel `gorm:"foreignKey:ChannelID;references:ID;constraint:OnDelete:CASCADE,OnUpdate:CASCADE" json:"-"`
Method string `gorm:"not null;default:'none'" json:"method"` // serial_rts|serial_dtr|gpio|cm108|none
Device string `json:"device_path"`
GpioPin uint32 `json:"gpio_pin"` // CM108-only: 1-indexed HID GPIO pin (default 3)
GpioLine uint32 `gorm:"not null;default:0" json:"gpio_line"` // gpiochip method: 0-indexed line offset
Invert bool `gorm:"not null;default:false" json:"invert"` // reverse polarity for rigs wired backwards
SlotTimeMs uint32 `gorm:"not null;default:10" json:"slot_time_ms"`
Persist uint32 `gorm:"not null;default:63" json:"persist"`
DwaitMs uint32 `gorm:"not null;default:0" json:"dwait_ms"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
PttConfig holds push-to-talk configuration for a channel. ChannelID is a hard FK to Channel.ID with OnDelete:CASCADE: PTT settings have no meaning without the channel they belong to, and the uniqueIndex on ChannelID guarantees one row per channel.
type Referrer ¶
Referrer describes a single row in a dependent table that references a channel via a soft integer FK. Used by ChannelReferrers to surface the impact of a cascade delete to the operator before commit.
Type is a stable token identifying the referent table + role (so two columns of the same table — e.g. digipeater_rule_from vs digipeater_rule_to — don't collapse together). ID is the row's own primary key. Name is a human-legible label chosen from the row (Beacon.Callsign+Type, DigipeaterRule.Alias, KissInterface.Name, etc.); empty for singleton referents (IGateConfig) where the row has no meaningful display name.
type Referrers ¶
type Referrers struct {
Items []Referrer `json:"items"`
}
Referrers is the collected result of a ChannelReferrers scan. Items is always non-nil (possibly empty) so JSON encoders emit `[]` rather than `null` on the wire.
type SmartBeaconConfig ¶
type SmartBeaconConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"-"`
Enabled bool `gorm:"not null" json:"enabled"`
FastSpeedKt uint32 `gorm:"not null" json:"fast_speed"`
FastRateSec uint32 `gorm:"not null" json:"fast_rate"`
SlowSpeedKt uint32 `gorm:"not null" json:"slow_speed"`
SlowRateSec uint32 `gorm:"not null" json:"slow_rate"`
MinTurnDeg uint32 `gorm:"not null" json:"min_turn_angle"`
TurnSlope uint32 `gorm:"not null" json:"turn_slope"`
MinTurnSec uint32 `gorm:"not null" json:"min_turn_time"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
SmartBeaconConfig is a singleton (id=1) row holding the global SmartBeacon curve parameters applied to every beacon with SmartBeacon=true. Mirrors direwolf's single SMARTBEACON directive: the curve is global, not per-beacon. No integer defaults are declared in gorm tags — defaults live in pkg/beacon.DefaultSmartBeacon() (the single source of truth) and are surfaced to callers via the DTO layer when no row exists. GetSmartBeaconConfig returning (nil, nil) signals "no row yet — apply defaults."
type StationConfig ¶
type StationConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Callsign string `gorm:"not null;default:''" json:"callsign"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
StationConfig is a singleton (id=1) row holding the station-wide APRS callsign. This is the single source of truth for the callsign used by the iGate (APRS-IS login + passcode), the digipeater (unless overridden), beacons (unless overridden), and APRS messaging. See .context/2026-04-21-centralized-station-callsign.md.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps a *gorm.DB with typed helpers for graywolf's tables.
func OpenMemory ¶
OpenMemory opens an isolated in-memory database (one per call).
func (*Store) AppendAX25TranscriptEntry ¶ added in v0.12.4
func (s *Store) AppendAX25TranscriptEntry(ctx context.Context, e *AX25TranscriptEntry) error
AppendAX25TranscriptEntry inserts a single transcript entry. The caller stamps the timestamp so a transcript-on toggle that lands a burst of buffered events keeps their original wall-clock order.
func (*Store) ChannelExists ¶
ChannelExists reports whether a channel row with the given ID exists. Returns (false, nil) when the row is absent; (false, err) on any driver / context error. Callers that need the row itself should use GetChannel; ChannelExists is the cheaper probe for write-time reference validation (see dto.ValidateChannelRef).
func (*Store) ChannelReferrers ¶
ChannelReferrers queries every table that references channels.id via a soft integer foreign key and returns a structured list of dependent rows. Used by DELETE /api/channels/{id} to return 409 with the impact list, and by GET /api/channels/{id}/referrers to power the first confirmation dialog in the UI.
Covered tables (see design decision D12 in .context/2026-04-20-kiss-tcp-client-and-channel-backing.md):
- Beacon.Channel
- DigipeaterRule.FromChannel (emitted as "digipeater_rule_from")
- DigipeaterRule.ToChannel (emitted as "digipeater_rule_to" only when ToChannel matches AND FromChannel does NOT — cross-channel rules where only the destination matched; same-channel rules are already covered by the FromChannel branch).
- KissInterface.Channel
- IGateConfig.RfChannel (as "igate_config_rf")
- IGateConfig.TxChannel (as "igate_config_tx")
- IGateRfFilter.Channel
- TxTiming.Channel
PttConfig.ChannelID has a hard FK with OnDelete:CASCADE, so SQLite removes those rows automatically and this scan deliberately omits them.
A channelID of 0 returns an empty list — 0 is reserved for "none" in singletons like IGateConfig.TxChannel and never a real channel row.
func (*Store) CountOrphanChannelRefs ¶
CountOrphanChannelRefs runs a one-shot scan at bootstrap for rows whose channel references don't resolve. Returns a map keyed by a stable table-role token (mirroring ReferrerType*) to the number of orphan rows in that role. Never returns nil — an empty map means "all refs resolve". Tables are scanned with a LEFT JOIN + NULL predicate so the query is O(n) per table rather than O(n*m).
The caller is expected to log a warn line per non-zero entry. No deletion or cleanup happens here; operators decide whether to remediate via the cascade-delete UI.
func (*Store) CreateAX25SessionProfile ¶ added in v0.12.4
func (s *Store) CreateAX25SessionProfile(ctx context.Context, p *AX25SessionProfile) error
CreateAX25SessionProfile inserts a new profile row. ID is set on return.
func (*Store) CreateAX25TranscriptSession ¶ added in v0.12.4
func (s *Store) CreateAX25TranscriptSession(ctx context.Context, sess *AX25TranscriptSession) error
CreateAX25TranscriptSession inserts a new session row. Stamps StartedAt = now if the caller left it zero.
func (*Store) CreateAction ¶ added in v0.13.0
func (*Store) CreateActionListenerAddressee ¶ added in v0.13.0
func (*Store) CreateAudioDevice ¶
func (s *Store) CreateAudioDevice(ctx context.Context, d *AudioDevice) error
func (*Store) CreateDigipeaterRule ¶
func (s *Store) CreateDigipeaterRule(ctx context.Context, r *DigipeaterRule) error
func (*Store) CreateIGateRfFilter ¶
func (s *Store) CreateIGateRfFilter(ctx context.Context, f *IGateRfFilter) error
func (*Store) CreateKissInterface ¶
func (s *Store) CreateKissInterface(ctx context.Context, k *KissInterface) error
func (*Store) CreateOTPCredential ¶ added in v0.13.0
func (s *Store) CreateOTPCredential(ctx context.Context, c *OTPCredential) error
func (*Store) CreateTacticalCallsign ¶
func (s *Store) CreateTacticalCallsign(ctx context.Context, t *TacticalCallsign) error
CreateTacticalCallsign inserts a new tactical entry. Callsign is normalized to uppercase by the TacticalCallsign.BeforeSave hook.
func (*Store) DeleteAX25SessionProfile ¶ added in v0.12.4
DeleteAX25SessionProfile removes the row by id. Idempotent: deleting a missing row returns nil so callers can wire it directly to a DELETE handler without a 404 race.
func (*Store) DeleteAX25TranscriptSession ¶ added in v0.12.4
DeleteAX25TranscriptSession removes a session row plus every entry that references it. Idempotent.
func (*Store) DeleteAction ¶ added in v0.13.0
func (*Store) DeleteActionListenerAddresseeByName ¶ added in v0.13.0
func (*Store) DeleteAllAX25Transcripts ¶ added in v0.12.4
DeleteAllAX25Transcripts wipes every transcript session + entry. Used by the "delete all" button on the transcripts subroute.
func (*Store) DeleteAllActionInvocations ¶ added in v0.13.0
DeleteAllActionInvocations truncates the audit log. Operator-driven only — surfaced via DELETE /api/actions/invocations and the UI's "clear log" button. The retention pruner uses PruneActionInvocations instead so the routine path stays bounded without dropping recent rows.
func (*Store) DeleteAudioDevice ¶
func (*Store) DeleteAudioDeviceChecked ¶
func (s *Store) DeleteAudioDeviceChecked(ctx context.Context, id uint32, cascade bool) (deleted []Channel, refs []Channel, err error)
DeleteAudioDeviceChecked atomically checks for channels referencing the device and either refuses the delete (cascade=false with refs) or cascades through them (cascade=true, or no refs) within a single transaction. There is no window for a concurrent writer to slip in a new referencing channel between the check and the delete, so an operator who declined to cascade can never have a channel silently swept away.
Return shapes:
- refs non-empty, deleted nil: operator refused to cascade; nothing was modified. Caller should surface refs to the user and ask.
- refs nil, deleted: the device is gone; deleted lists the channels that went with it (possibly empty if nothing referenced the device).
func (*Store) DeleteChannelCascade ¶
DeleteChannelCascade atomically removes a channel plus every soft-FK reference per the D12 per-table policy:
- Beacon.Channel == id → delete row
- DigipeaterRule.FromChannel == id → delete row
- DigipeaterRule.ToChannel == id AND FromChannel != id → delete row (cross-channel rules where only the destination matched)
- KissInterface.Channel == id → set Channel=0 AND NeedsReconfig=true (the interface may still be useful on another channel after reconfig; don't delete it)
- IGateConfig.RfChannel == id → set RfChannel=0
- IGateConfig.TxChannel == id → set TxChannel=0
- IGateRfFilter.Channel == id → delete row
- TxTiming.Channel == id → delete row
- Channel itself → delete (fires ON DELETE CASCADE for PttConfig)
All operations run in a single SQLite transaction; either every change lands or none do. Callers are expected to fire a single post-commit bridge / kiss-manager reload so in-memory state reconverges exactly once (not N times, one per affected row).
Returns the count of rows that were touched (for observability in the caller's reload log line) plus any error. A nonexistent channelID returns (0, gorm.ErrRecordNotFound) without changing anything.
func (*Store) DeleteDigipeaterRule ¶
func (*Store) DeleteIGateRfFilter ¶
func (*Store) DeleteKissInterface ¶
func (*Store) DeleteMapsDownload ¶
DeleteMapsDownload removes the row for slug. Idempotent — deleting an absent row is not an error.
func (*Store) DeleteOTPCredential ¶ added in v0.13.0
func (*Store) DeletePttConfig ¶
func (*Store) DeleteTacticalCallsign ¶
DeleteTacticalCallsign removes a tactical entry by id. Historical message rows keyed by the tactical label persist so the thread stays a read-only archive — only the monitor entry is deleted.
func (*Store) EndAX25TranscriptSession ¶ added in v0.12.4
func (s *Store) EndAX25TranscriptSession(ctx context.Context, id uint32, reason string, bytes, frames uint64) error
EndAX25TranscriptSession stamps EndedAt + EndReason and rolls up the final byte/frame counters. Idempotent: running twice is harmless, the latest call wins.
func (*Store) GetAX25SessionProfile ¶ added in v0.12.4
GetAX25SessionProfile loads a profile by id; ErrRecordNotFound when missing so callers can map to 404.
func (*Store) GetAX25TerminalConfig ¶ added in v0.12.4
func (s *Store) GetAX25TerminalConfig(ctx context.Context) (*AX25TerminalConfig, error)
GetAX25TerminalConfig returns the singleton AX25TerminalConfig row, creating one with sane defaults on first read. Migration v14 seeds the row at startup; this FirstOrCreate is the belt-and-braces guard for any code path that opens a fresh database without going through Migrate() (e.g. in-process integration tests).
func (*Store) GetAX25TranscriptSession ¶ added in v0.12.4
func (s *Store) GetAX25TranscriptSession(ctx context.Context, id uint32) (*AX25TranscriptSession, error)
GetAX25TranscriptSession fetches one session by id.
func (*Store) GetActionByName ¶ added in v0.13.0
GetActionByName returns the Action with the given name. Matching is case-insensitive — Action.Name is stored uppercase (see BeforeSave), so an inbound `@@otp#unlock` resolves to the row created as `Unlock`.
func (*Store) GetAudioDevice ¶
func (*Store) GetChannel ¶
func (*Store) GetDigipeaterConfig ¶
func (s *Store) GetDigipeaterConfig(ctx context.Context) (*DigipeaterConfig, error)
func (*Store) GetIGateConfig ¶
func (s *Store) GetIGateConfig(ctx context.Context) (*IGateConfig, error)
func (*Store) GetKissInterface ¶
func (*Store) GetLogBufferConfig ¶
GetLogBufferConfig returns the singleton log-buffer configuration row plus an exists flag. The flag is required because MaxRows == 0 is a valid override meaning "operator disabled persistence" — the caller can't distinguish that from "no row stored, use environment default" by inspecting MaxRows alone. DB errors other than not-found are returned verbatim.
func (*Store) GetMapsConfig ¶
func (s *Store) GetMapsConfig(ctx context.Context) (MapsConfig, error)
GetMapsConfig returns the singleton maps preference. When no row exists (fresh install), returns MapsConfig{Source: "graywolf"} with no error so the UI has a deterministic default without a seed step. Graywolf is the default basemap; the maplibre frontend falls back to OSM rendering automatically when the device hasn't registered yet, so this is safe even before the operator obtains a token. An unknown Source value in the stored row is normalized to graywolf.
func (*Store) GetMapsDownload ¶
GetMapsDownload returns the row for slug, or a zero-value struct (ID==0) if none exists. Callers check ID==0 to detect absence.
func (*Store) GetMessagePreferences ¶
func (s *Store) GetMessagePreferences(ctx context.Context) (*MessagePreferences, error)
GetMessagePreferences returns the singleton preferences row. The row is seeded with defaults by seedMessagePreferences on first migrate, so a nil return indicates a DB error path only (preserved for consistency with the other singleton getters).
func (*Store) GetMessagesConfig ¶ added in v0.12.4
func (s *Store) GetMessagesConfig(ctx context.Context) (*MessagesConfig, error)
GetMessagesConfig returns the singleton row, creating an empty row (TxChannel=0, "auto") on first read. Callers handle TxChannel==0 by resolving against the live channel inventory in pkg/app.
Uses FirstOrCreate so two concurrent callers on a freshly-opened database (e.g. a partial migration) cannot both win the race-to-Create and surface a UNIQUE-constraint error. Migration v13 pre-populates the row on real systems; this is the belt-and-braces guard.
func (*Store) GetOTPCredential ¶ added in v0.13.0
func (*Store) GetOTPCredentialByName ¶ added in v0.13.0
func (*Store) GetPositionLogConfig ¶
func (s *Store) GetPositionLogConfig(ctx context.Context) (*PositionLogConfig, error)
func (*Store) GetPttConfigForChannel ¶
func (*Store) GetSmartBeaconConfig ¶
func (s *Store) GetSmartBeaconConfig(ctx context.Context) (*SmartBeaconConfig, error)
GetSmartBeaconConfig returns the singleton SmartBeacon configuration row. When no row exists, returns (nil, nil) — the caller interprets that as "apply defaults from beacon.DefaultSmartBeacon()". DB errors are returned as non-nil errors. Matches the established singleton contract used by GetDigipeaterConfig, GetIGateConfig, GetGPSConfig.
func (*Store) GetStationConfig ¶
func (s *Store) GetStationConfig(ctx context.Context) (StationConfig, error)
GetStationConfig returns the singleton station configuration row. Returns a zero-value StationConfig (no error) when no row exists — callers can treat an empty Callsign as "unconfigured" without a separate nil-check. DB errors other than not-found are returned verbatim.
func (*Store) GetTacticalCallsign ¶
GetTacticalCallsign returns a single tactical entry by id. Returns (nil, nil) on not-found to match the other singleton helpers.
func (*Store) GetTacticalCallsignByCallsign ¶
func (s *Store) GetTacticalCallsignByCallsign(ctx context.Context, callsign string) (*TacticalCallsign, error)
GetTacticalCallsignByCallsign returns the entry whose Callsign equals the uppercase-normalized argument. Returns (nil, nil) on not-found to match the other singleton getters. Used by the invite accept handler so it can upsert without racing the autoincrement ID.
func (*Store) GetThemeConfig ¶
func (s *Store) GetThemeConfig(ctx context.Context) (ThemeConfig, error)
GetThemeConfig returns the singleton theme preference. Fresh install returns ThemeConfig{ThemeID: "graywolf"} with no error. A row with a malformed id (e.g. hand-edited DB) is normalized to the default on read so the frontend never sees garbage.
func (*Store) GetTxTiming ¶
func (*Store) GetUnitsConfig ¶
func (s *Store) GetUnitsConfig(ctx context.Context) (UnitsConfig, error)
GetUnitsConfig returns the singleton measurement-system preference. When no row exists (fresh install), returns UnitsConfig{System: "imperial"} with no error so the UI has a deterministic default without a seed step. An unknown System value in the stored row is normalized to imperial so the frontend always sees one of the two valid values.
func (*Store) GetUpdatesConfig ¶
func (s *Store) GetUpdatesConfig(ctx context.Context) (UpdatesConfig, error)
GetUpdatesConfig returns the singleton updates-check configuration row. When no row exists (fresh install), returns UpdatesConfig{Enabled: true} with no error — the feature is on by default and callers don't need a separate seed step. DB errors other than not-found are returned verbatim. Mirrors the shape of GetStationConfig but with a different zero-value-on-missing contract: StationConfig's zero value ("unconfigured") is also the safe default, whereas UpdatesConfig's safe default is Enabled=true, which differs from the Go zero value.
func (*Store) InsertActionInvocation ¶ added in v0.13.0
func (s *Store) InsertActionInvocation(ctx context.Context, row *ActionInvocation) error
func (*Store) ListAX25SessionProfiles ¶ added in v0.12.4
func (s *Store) ListAX25SessionProfiles(ctx context.Context) ([]AX25SessionProfile, error)
ListAX25SessionProfiles returns every saved profile ordered with pinned rows first, then recents by LastUsed desc, then by Name. The pre-connect form renders both groups in this order.
func (*Store) ListAX25TranscriptEntries ¶ added in v0.12.4
func (s *Store) ListAX25TranscriptEntries(ctx context.Context, sessionID uint32) ([]AX25TranscriptEntry, error)
ListAX25TranscriptEntries returns every entry for a session, ordered by TS asc (chronological).
func (*Store) ListAX25TranscriptSessions ¶ added in v0.12.4
func (s *Store) ListAX25TranscriptSessions(ctx context.Context, limit int) ([]AX25TranscriptSession, error)
ListAX25TranscriptSessions returns transcript-session rows ordered by StartedAt desc (most recent first). Cap clamps the result; pass 0 for "no cap" but expect callers to set a sane upper bound.
func (*Store) ListActionInvocations ¶ added in v0.13.0
func (s *Store) ListActionInvocations(ctx context.Context, f ActionInvocationFilter) ([]ActionInvocation, error)
func (*Store) ListActionListenerAddressees ¶ added in v0.13.0
func (s *Store) ListActionListenerAddressees(ctx context.Context) ([]ActionListenerAddressee, error)
func (*Store) ListActions ¶ added in v0.13.0
func (*Store) ListAudioDevices ¶
func (s *Store) ListAudioDevices(ctx context.Context) ([]AudioDevice, error)
func (*Store) ListDigipeaterRules ¶
func (s *Store) ListDigipeaterRules(ctx context.Context) ([]DigipeaterRule, error)
func (*Store) ListDigipeaterRulesForChannel ¶
func (*Store) ListEnabledTacticalCallsigns ¶
func (s *Store) ListEnabledTacticalCallsigns(ctx context.Context) ([]TacticalCallsign, error)
ListEnabledTacticalCallsigns returns only the entries with Enabled=true. The router uses this at startup and on preferences reload to rebuild its in-memory matching set.
func (*Store) ListIGateRfFilters ¶
func (s *Store) ListIGateRfFilters(ctx context.Context) ([]IGateRfFilter, error)
func (*Store) ListIGateRfFiltersForChannel ¶
func (*Store) ListKissInterfaces ¶
func (s *Store) ListKissInterfaces(ctx context.Context) ([]KissInterface, error)
func (*Store) ListMapsDownloads ¶
func (s *Store) ListMapsDownloads(ctx context.Context) ([]MapsDownload, error)
ListMapsDownloads returns every download row, ordered by slug for deterministic UI display. Returns an empty slice (not nil) on a fresh install.
func (*Store) ListOTPCredentials ¶ added in v0.13.0
func (s *Store) ListOTPCredentials(ctx context.Context) ([]OTPCredential, error)
func (*Store) ListOrphanChannelRefs ¶
func (s *Store) ListOrphanChannelRefs(ctx context.Context) ([]OrphanChannelRefRows, error)
ListOrphanChannelRefs returns, per referrer table, the set of row ids whose channel soft-FK does not resolve, plus the distinct set of missing channel ids referenced. One query per table, same LEFT-JOIN / NOT-IN pattern as CountOrphanChannelRefs but returning the ids instead of just the count.
An empty slice return means "no orphans anywhere". Per-table probe errors are swallowed (e.g. table missing on a fresh DB before AutoMigrate) so the overall scan never fails startup.
func (*Store) ListPacketFilters ¶
func (s *Store) ListPacketFilters(ctx context.Context) ([]PacketFilter, error)
func (*Store) ListPttConfigs ¶
func (*Store) ListTacticalCallsigns ¶
func (s *Store) ListTacticalCallsigns(ctx context.Context) ([]TacticalCallsign, error)
ListTacticalCallsigns returns every tactical entry (enabled or not), ordered by callsign for stable UI display.
func (*Store) ListTxTimings ¶
func (*Store) Migrate ¶
Migrate brings the schema up to date. Safe to call repeatedly.
Ordering matters: the pre-AutoMigrate pass runs first to fix up legacy columns that AutoMigrate would otherwise stumble over (a column rename, for example, looks like an add+drop to the migrator), then AutoMigrate reconciles the Go model shape with SQLite, then the post-AutoMigrate pass runs data migrations that need the new schema in place. See migrate.go for the migration list and the user_version contract.
func (*Store) MigrateMapsDownloadSlugs ¶ added in v0.12.1
MigrateMapsDownloadSlugs prepends "state/" to any legacy bare-slug row in maps_downloads. Idempotent: rows already containing "/" are left alone. Run once at startup after AutoMigrate.
Collision policy: if a row already exists at the namespaced target (e.g. both "colorado" and "state/colorado" coexist after some prior partial migration or hand edit), the legacy bare row is DELETED and the namespaced row is kept. The unique-index on slug means a naive UPDATE would error and abort startup, so this collision case is handled explicitly. The whole pass runs in a single transaction so a crash mid-migration leaves the table either fully migrated or fully untouched.
func (*Store) ModeForChannel ¶ added in v0.12.4
ModeForChannel returns the Mode column for the given channel id. Returns ChannelModeAPRS and a nil error when the channelID is 0 (no channel selected) or when the row does not exist -- TX subsystems treat both cases as the conservative APRS-only choice. Existing rows always carry a non-empty Mode (validateChannel normalizes empty to ChannelModeAPRS), so the empty-string branch is solely a missing-row guard.
Missing-row hits emit a debug log so operators investigating why a downstream subsystem (e.g. ax25conn refusing to bind) believes a channel is APRS-only can correlate against an actually-deleted channel ID without re-deriving the lookup path.
func (*Store) OTPCredentialUsedBy ¶ added in v0.13.0
OTPCredentialUsedBy returns a map cred-id -> action names that reference it. One scan over the actions table; callers iterate the returned map per credential rather than issuing N queries.
func (*Store) PinAX25SessionProfile ¶ added in v0.12.4
PinAX25SessionProfile flips Pinned to true on the row, promoting it from recents into the permanent list.
func (*Store) PruneActionInvocations ¶ added in v0.13.0
func (s *Store) PruneActionInvocations(ctx context.Context, maxRows int, maxAge time.Duration) (int, error)
PruneActionInvocations enforces the audit-log retention contract: rows older than maxAge are deleted unconditionally; if the post-age row count still exceeds maxRows, the oldest excess rows are deleted too. Either bound on its own keeps the table bounded; running both captures the more aggressive of the two so a quiet operator who hasn't crossed the time bound but somehow accumulated a million rows (e.g. a runaway test fixture) still stays under the count cap. Returns the total number of rows deleted across both passes.
func (*Store) ResolveStationCallsign ¶
ResolveStationCallsign returns the normalized station callsign or a sentinel error. The callsign is read from StationConfig; empty (or whitespace-only) returns callsign.ErrCallsignEmpty, N0CALL (case-insensitive, SSID-agnostic) returns callsign.ErrCallsignN0Call. DB errors are returned as-is. Callers can branch on the sentinel errors via errors.Is.
func (*Store) SQLiteVersion ¶
SQLiteVersion returns the runtime SQLite library version string (e.g. "3.42.0") via `SELECT sqlite_version()`. Called from app startup so ops can see the version in the logs — important for migrations that depend on a minimum SQLite version (the 12-step table rebuild added in migration 8 needs ≥ 3.25 for ALTER TABLE RENAME, which this driver satisfies). Returns the empty string on error; callers log the returned value verbatim.
func (*Store) SetChannelFX25 ¶
SetChannelFX25 sets FX.25 encoding for a channel.
func (*Store) SetChannelIL2P ¶
SetChannelIL2P sets IL2P encoding for a channel.
func (*Store) TouchAX25SessionProfileLastUsed ¶ added in v0.12.4
func (s *Store) TouchAX25SessionProfileLastUsed(ctx context.Context, id uint32, when time.Time) error
TouchAX25SessionProfileLastUsed updates LastUsed on a recent. Used by the OnStateChange(CONNECTED) hook in the WebSocket bridge so the recents list reflects the most recent successful connection.
func (*Store) TouchOTPCredentialUsed ¶ added in v0.13.0
TouchOTPCredentialUsed records the most recent moment a credential successfully verified a TOTP code. Stored UTC; the UI surfaces this so operators can spot dormant credentials.
func (*Store) UpdateAX25SessionProfile ¶ added in v0.12.4
func (s *Store) UpdateAX25SessionProfile(ctx context.Context, p *AX25SessionProfile) error
UpdateAX25SessionProfile replaces all editable columns for the row identified by p.ID. Pinned + LastUsed are managed by their own helpers (PinAX25SessionProfile, TouchAX25SessionProfileLastUsed).
func (*Store) UpdateAction ¶ added in v0.13.0
func (*Store) UpdateAudioDevice ¶
func (s *Store) UpdateAudioDevice(ctx context.Context, d *AudioDevice) error
func (*Store) UpdateDigipeaterRule ¶
func (s *Store) UpdateDigipeaterRule(ctx context.Context, r *DigipeaterRule) error
func (*Store) UpdateIGateRfFilter ¶
func (s *Store) UpdateIGateRfFilter(ctx context.Context, f *IGateRfFilter) error
func (*Store) UpdateKissInterface ¶
func (s *Store) UpdateKissInterface(ctx context.Context, k *KissInterface) error
func (*Store) UpdateTacticalCallsign ¶
func (s *Store) UpdateTacticalCallsign(ctx context.Context, t *TacticalCallsign) error
UpdateTacticalCallsign saves changes to an existing row. Callsign re-normalization happens via BeforeSave.
func (*Store) UpsertAX25TerminalConfig ¶ added in v0.12.4
func (s *Store) UpsertAX25TerminalConfig(ctx context.Context, cfg *AX25TerminalConfig) error
UpsertAX25TerminalConfig writes the singleton (id forced to 1). The REST handler converts the macros DTO array into MacrosJSON before calling.
func (*Store) UpsertAgwConfig ¶
func (*Store) UpsertDigipeaterConfig ¶
func (s *Store) UpsertDigipeaterConfig(ctx context.Context, c *DigipeaterConfig) error
func (*Store) UpsertGPSConfig ¶
func (*Store) UpsertIGateConfig ¶
func (s *Store) UpsertIGateConfig(ctx context.Context, c *IGateConfig) error
func (*Store) UpsertLogBufferConfig ¶
func (s *Store) UpsertLogBufferConfig(ctx context.Context, c LogBufferConfig) error
UpsertLogBufferConfig stores the singleton log-buffer config row. When c.ID == 0 and a row already exists, the existing ID is adopted so Save updates in place. MaxRows is written verbatim — including 0, which the consumer treats as "disable persistence".
We use a map-based UpdateColumns path so MaxRows == 0 is never silently rewritten by GORM's "default" tag handling (same footgun the UpdatesConfig CRUD documents at seed_updates.go:38-45).
Side effect: UpdateColumns suppresses auto-timestamps, so UpdatedAt stays stale. No consumer reads it today; same behavior as seed_updates.go's UpsertUpdatesConfig.
Defensive: when c.ID != 0 but the row does not exist, the UpdateColumns call would silently no-op (RowsAffected=0). We require RowsAffected >= 1 on the update path so that footgun surfaces as an error rather than vanishing.
func (*Store) UpsertMapsConfig ¶
func (s *Store) UpsertMapsConfig(ctx context.Context, c MapsConfig) error
UpsertMapsConfig persists the singleton maps preference. Source must be one of the two recognized values; anything else is rejected so a bad PUT can't corrupt the row. ID is adopted from any existing row to preserve the singleton invariant.
This is a full-replace operation: every mutable column (source, callsign, token, registered_at) is overwritten with the value on c. Callers that intend to update only one field (e.g. just Source) MUST GetMapsConfig first, mutate the returned struct, then pass it here — otherwise empty fields silently un-register the device.
func (*Store) UpsertMapsDownload ¶
func (s *Store) UpsertMapsDownload(ctx context.Context, d MapsDownload) error
UpsertMapsDownload writes the row. Rows are keyed by slug (uniqueIndex on the model); a second call with the same slug updates in place rather than inserting a duplicate. Status must be one of the four documented values; the slug must be non-empty.
Slug format is namespaced: state/<slug>, country/<iso2>, or province/<iso2>/<slug>. Legacy bare-slug rows (e.g. "colorado") from pre-namespaced installs are migrated in place at startup by MigrateMapsDownloadSlugs. The store layer does not enforce the grammar -- the webapi layer validates against the live catalog before any write reaches here.
func (*Store) UpsertMessagePreferences ¶
func (s *Store) UpsertMessagePreferences(ctx context.Context, cfg *MessagePreferences) error
UpsertMessagePreferences stores the singleton row. When cfg.ID == 0 and a row already exists, the existing ID is adopted so Save updates in place. Matches UpsertDigipeaterConfig et al.
func (*Store) UpsertMessagesConfig ¶ added in v0.12.4
func (s *Store) UpsertMessagesConfig(ctx context.Context, mc *MessagesConfig) error
UpsertMessagesConfig writes the singleton row (id forced to 1). TxChannel is validated against ChannelModeLookup at the handler layer; the store accepts any uint32 here.
Uses an INSERT ... ON CONFLICT DO UPDATE clause that touches only tx_channel + updated_at, so a stale CreatedAt on the caller's struct cannot clobber the original row's creation timestamp.
func (*Store) UpsertPositionLogConfig ¶
func (s *Store) UpsertPositionLogConfig(ctx context.Context, c *PositionLogConfig) error
func (*Store) UpsertPttConfig ¶
func (*Store) UpsertRecentAX25SessionProfile ¶ added in v0.12.4
func (s *Store) UpsertRecentAX25SessionProfile(ctx context.Context, p *AX25SessionProfile, capRecents int) error
UpsertRecentAX25SessionProfile creates or updates a recent profile entry keyed by (LocalCall, LocalSSID, DestCall, DestSSID, ViaPath, ChannelID). Used by the bridge so successive connects to the same peer/path don't fan out the recents list.
On insert: stamps LastUsed = now, Pinned = false. On match: only updates LastUsed (the operator's prior settings stay).
After the upsert, trims unpinned recents back down to the cap (20) by deleting the oldest LastUsed rows.
func (*Store) UpsertSmartBeaconConfig ¶
func (s *Store) UpsertSmartBeaconConfig(ctx context.Context, cfg *SmartBeaconConfig) error
UpsertSmartBeaconConfig stores the singleton row. Either inserts or updates: if the caller passes cfg.ID == 0 and a row already exists, the existing ID is adopted so Save updates in place rather than creating a second row. Matches UpsertDigipeaterConfig et al.
func (*Store) UpsertStationConfig ¶
func (s *Store) UpsertStationConfig(ctx context.Context, c StationConfig) error
UpsertStationConfig stores the singleton station config row, normalizing the Callsign (TrimSpace + ToUpper) before persist. When c.ID == 0 and a row already exists, the existing ID is adopted so Save updates in place. Normalization at the store boundary means every caller — including future ones — sees a canonical value without having to remember to uppercase on write.
func (*Store) UpsertThemeConfig ¶
func (s *Store) UpsertThemeConfig(ctx context.Context, c ThemeConfig) error
UpsertThemeConfig stores the singleton theme preference. Rejects malformed ids so a bad PUT can't corrupt the row. Preserves the singleton ID across upserts.
func (*Store) UpsertTxTiming ¶
func (*Store) UpsertUnitsConfig ¶
func (s *Store) UpsertUnitsConfig(ctx context.Context, c UnitsConfig) error
UpsertUnitsConfig stores the singleton measurement-system preference. Values other than "imperial" or "metric" are rejected so a bad PUT can't corrupt the row. When c.ID == 0 and a row already exists, the existing ID is adopted so the singleton invariant is preserved.
func (*Store) UpsertUpdatesConfig ¶
func (s *Store) UpsertUpdatesConfig(ctx context.Context, c UpdatesConfig) error
UpsertUpdatesConfig stores the singleton updates-check config row. When c.ID == 0 and a row already exists, the existing ID is adopted so Save updates in place. Unlike StationConfig there is no value to normalize (Enabled is a bool).
GORM footgun: the column carries `default:true`, so a plain Create with Enabled=false would be silently rewritten to true on insert (GORM treats bool zero-values with a default tag as "unset, use default"). To defeat that we build the insert via a map, which sends every column value verbatim. For updates we do the same with UpdateColumns so Enabled=false is always honored.
type TacticalCallsign ¶
type TacticalCallsign struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Callsign string `gorm:"size:9;not null;uniqueIndex" json:"callsign"` // 1-9 [A-Z0-9-], uppercase
Alias string `gorm:"size:64" json:"alias"` // optional free-text
// Enabled: column does not declare default:true on purpose. The
// handler-level default ("Monitor now" toggle) runs before the
// insert, and a GORM default:true would silently override a caller
// passing false (GORM treats Go-zero values as "use the DB
// default"), which is hostile to the common "create disabled" path.
Enabled bool `gorm:"not null" json:"enabled"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
TacticalCallsign is one monitored tactical addressee label. Operators register these to participate in group threads keyed by the label. Callsign is normalized to uppercase via BeforeSave so any path in/out is safe. See plan "Group chat via tactical callsigns" section.
func (*TacticalCallsign) BeforeSave ¶
func (t *TacticalCallsign) BeforeSave(_ *gorm.DB) error
BeforeSave normalizes Callsign to uppercase and trims whitespace before insert or update. Ensures the router's case-sensitive exact match against the cached set always sees a canonical value regardless of how a handler constructed the row.
type ThemeConfig ¶
type ThemeConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
ThemeID string `gorm:"not null;default:'graywolf'" json:"theme_id"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
ThemeConfig stores the operator's preferred UI color theme. Singleton at id=1, default ThemeID="graywolf". The set of shipped themes lives in graywolf/web/themes/themes.json; ids are validated by regex (^[a-z0-9][a-z0-9-]{0,63}$) — see IsValidTheme in seed_theme.go — rather than by a hardcoded list so new themes don't require backend changes.
type TxTiming ¶
type TxTiming struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Channel uint32 `gorm:"not null;uniqueIndex" json:"channel"`
TxDelayMs uint32 `gorm:"not null;default:300" json:"tx_delay_ms"`
TxTailMs uint32 `gorm:"not null;default:100" json:"tx_tail_ms"`
SlotMs uint32 `gorm:"not null;default:100" json:"slot_ms"`
Persist uint32 `gorm:"not null;default:63" json:"persist"`
FullDup bool `gorm:"not null;default:false" json:"full_dup"`
// Rate limits; 0 = unlimited.
Rate1Min uint32 `gorm:"not null;default:0" json:"rate_1min"`
Rate5Min uint32 `gorm:"not null;default:0" json:"rate_5min"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
TxTiming holds per-channel CSMA parameters. Mirrors txgovernor.ChannelTiming.
type UnitsConfig ¶
type UnitsConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
System string `gorm:"not null;default:'imperial'" json:"system"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
UnitsConfig stores the operator's preferred measurement system for display. Singleton at id=1, default System="imperial". Valid values are "imperial" and "metric"; unknown values fall back to imperial on read (see GetUnitsConfig).
type UpdatesConfig ¶
type UpdatesConfig struct {
ID uint32 `gorm:"primaryKey;autoIncrement" json:"id"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
UpdatesConfig controls the daily GitHub update check. Singleton at id=1, default Enabled=true. Disabling stops the ticker and causes GET /api/updates/status to report status="disabled" regardless of any cached result.
Source Files
¶
- action_invocations.go
- action_listener_addressees.go
- actions.go
- ax25_profiles.go
- ax25_terminal_config.go
- ax25_transcripts.go
- channel_mode_lookup.go
- iface.go
- messages.go
- messages_config.go
- migrate.go
- migrate_actions.go
- migrate_actions_argmode.go
- migrate_actions_max_reply_lines.go
- migrate_actions_uppercase.go
- migrate_ax25.go
- migrate_channel_mode.go
- migrate_downloads.go
- migrate_messages_config.go
- migrate_remote_actions.go
- models.go
- otp_credentials.go
- seed_downloads.go
- seed_logbuffer.go
- seed_maps.go
- seed_station.go
- seed_theme.go
- seed_units.go
- seed_updates.go
- smartbeacon.go
- store.go