dto

package
v0.13.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 9, 2026 License: GPL-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package dto: actions resource shapes.

The Actions REST API uses one wire struct per resource for both reads and writes — the operator-facing form binds against the same shape it gets back from GET. ID and derived fields (LastInvokedAt/LastInvokedBy) are populated only on reads and ignored on writes.

Package dto defines the request and response shapes accepted and returned by pkg/webapi. DTOs decouple the HTTP contract from configstore storage models so the DB schema can evolve without changing the API surface, and vice versa.

Each mutable resource exposes:

  • A request struct used for both POST (create) and PUT (update). It implements Validator.
  • ToModel / ToUpdate that map into a configstore.* value.
  • A response struct + FromModel conversion that masks any internal fields (timestamps, denormalized state).

Read-only resources continue to return storage models directly; add DTOs only if the shape needs to diverge.

Package dto wire shapes for the outbound Actions feature.

Index

Constants

View Source
const (
	DefaultAgwListenAddr = "0.0.0.0:8000"
	DefaultAgwCallsigns  = "N0CALL"
)

First-run defaults seeded into the response DTO when the source model field is the Go zero value. These mirror the gorm column defaults on configstore.AgwConfig so GET /api/agw on a fresh install returns a populated, UI-ready config. See IGate defaults for the "zero means unset" caveat.

View Source
const (
	TxReasonNoInputDevice  = "no input device configured"
	TxReasonNoOutputDevice = "no output device configured"
)

TX-capability reason strings. Exported so tests and the validator callers can assert against them without re-stringing the literal.

View Source
const (
	ChannelBackingSummaryModem   = "modem"
	ChannelBackingSummaryKissTnc = "kiss-tnc"
	ChannelBackingSummaryUnbound = "unbound"
)

Backing summary values.

View Source
const (
	ChannelBackingHealthLive    = "live"
	ChannelBackingHealthDown    = "down"
	ChannelBackingHealthUnbound = "unbound"
)

Backing health values.

View Source
const (
	DefaultIGateServer          = "rotate.aprs2.net"
	DefaultIGatePort            = 14580
	DefaultIGateMaxMsgHops      = 2
	DefaultIGateSoftwareName    = "graywolf"
	DefaultIGateSoftwareVersion = "0.1"
)

First-run defaults seeded into the response DTO when the source model field is the Go zero value. These mirror the gorm column defaults on configstore.IGateConfig so GET /api/igate/config on a fresh install (empty store) returns a populated, UI-ready config. Users who explicitly save these fields as zero will see them overwritten on the next GET; this is acceptable for singleton config where "no row yet" and "saved as zero" are not meaningfully distinguishable anyway.

rf_channel and tx_channel are deliberately NOT defaulted here. Both are soft-FKs onto configstore.Channel.ID and there is no guarantee any specific ID exists (channels can be created/deleted, leaving non-contiguous IDs). Defaulting to "1" caused a write-time validation failure when the operator saved an unrelated change: the response echoed rf_channel=1, the UI round-tripped it back, and the handler's non-idempotent ValidateChannelRef path 400'd with "rf_channel: channel 1 does not exist". Passing 0 (the documented "unset" sentinel) lets the UI's defaultCh fallback pick the lowest live channel for tx_channel and lets idempotent-skip absorb rf_channel.

View Source
const (
	MessageStatusQueued      = "queued"       // outbound, not yet submitted
	MessageStatusTxSubmitted = "tx_submitted" // outbound, submitted but not yet TxHook-confirmed
	MessageStatusSentRF      = "sent_rf"      // outbound DM, RF sent, awaiting ack
	MessageStatusSentIS      = "sent_is"      // outbound DM, IS sent, awaiting ack
	MessageStatusAwaitingAck = "awaiting_ack" // outbound DM, sent but not yet acked
	MessageStatusAcked       = "acked"        // outbound DM, acked
	MessageStatusRejected    = "rejected"     // outbound DM, REJ received
	MessageStatusTimeout     = "timeout"      // outbound DM, retry budget exhausted
	MessageStatusBroadcast   = "sent"         // tactical outbound broadcast — terminal
	MessageStatusFailed      = "failed"       // terminal failure (non-retryable)
	MessageStatusReceived    = "received"     // inbound
)

Status wire values. Derived from the underlying configstore.Message columns — see MessageResponse.Status below for the derivation table.

View Source
const MaxMessageText = 67

MaxMessageText is the APRS101 per-message body cap applied to addressee-line direct messages (":ADDRESSEE9:text{id}"). Bulletins, status beacons, positions, and weather frames have their own length conventions and are not affected by this constant. REST uses this value as early-reject feedback so the web UI sees a 400 instead of a silent truncation; the authoritative gate lives on the sender path (pkg/messages.Sender) and consults MessagePreferences for whether an operator-set override relaxes it up to MaxMessageTextUnsafe.

View Source
const MaxMessageTextUnsafe = 200

MaxMessageTextUnsafe is the hard upper ceiling when an operator opts in to long messages via MessagePreferences.MaxMessageTextOverride. 200 bytes leaves safe headroom under the AX.25 info-field limit (~256 bytes) for the addressee framing (":ADDRESSEE9:") and the msgid tail ("{NNN"). Applies to addressee-line direct messages only; bulletins/status/position frames are unaffected.

Variables

This section is empty.

Functions

func DeriveMessageStatus

func DeriveMessageStatus(m configstore.Message) string

DeriveMessageStatus maps the row's persisted column tuple to the wire-visible status enum. The mapping is deterministic — same inputs always produce the same output — and is the single source of truth for the Svelte UI's status pill.

Direction = "in" → "received" Direction = "out" && ThreadKind = "tactical":

AckState == "broadcast"           → "sent"   (per plan: tactical terminal maps to "sent")
SentAt == nil && Attempts == 0    → "queued"
SentAt == nil && Attempts > 0     → "tx_submitted"
default                            → "sent"

Direction = "out" && ThreadKind = "dm":

AckState == "acked"                                          → "acked"
AckState == "rejected" && FailureReason != ""                → "timeout" or "failed"
AckState == "rejected"                                       → "rejected"
SentAt == nil && Attempts == 0                               → "queued"
SentAt == nil && Attempts > 0                                → "tx_submitted"
SentAt != nil && Source == "is"                              → "sent_is"  (if not yet acked)
SentAt != nil                                                → "sent_rf"

func NormalizeCallsign

func NormalizeCallsign(in string) (string, error)

NormalizeCallsign uppercases, strips -SSID, and validates the format. Returns the cleaned callsign or an error matching the server's "must include at least one digit" rule. Used both by the client-side pre-flight (in JS, mirrored) and the backend handler.

func SmartBeaconConfigToModel

func SmartBeaconConfigToModel(r SmartBeaconConfigRequest) configstore.SmartBeaconConfig

SmartBeaconConfigToModel maps a validated request into a storage model. Caller is responsible for stamping ID on update via the upsert path (configstore adopts the existing singleton id when cfg.ID == 0).

func ValidateAddressee

func ValidateAddressee(to string) error

ValidateAddressee returns nil iff to is a syntactically valid APRS addressee. Does NOT check against the tactical set or the loopback guard — handlers do those checks after this one.

func ValidateChannelRef

func ValidateChannelRef(ctx context.Context, lookup ChannelLookup, fieldName string, channelID uint32) error

ValidateChannelRef rejects a write whose channelID points at a non-existent channel. The zero value is treated as "none" — several soft-FK columns (IGateConfig.TxChannel, a TxTiming row's channel after a cascade nulls it, etc.) use 0 as the sentinel for "unset" and must pass through this helper unchanged. A non-zero value that fails to resolve returns a human-legible error intended to land verbatim in a 400 response body.

Callers wire this into the request lifecycle AFTER the DTO's own Validate() method has passed — keeping Validate() pure (no I/O) and letting the handler thread the store into the cross-table check. This mirrors the pattern Phase 3 established for the mutual-exclusivity rule, which lives at the configstore layer and runs once per create/update regardless of which DTO triggered it.

Note: this helper does not hold a DB lock across the lookup and the subsequent store write, so a racing delete between the check and the write could still land an orphan. The store-layer ChannelReferrers + post-delete reload notify path catches that class of race at the next reload; DTO validation is a friendly-error gate, not a hard invariant.

func ValidateMessageText

func ValidateMessageText(text string) error

ValidateMessageText rejects empty or patently over-long bodies. The upper bound here is MaxMessageTextUnsafe (the hard AX.25 headroom ceiling), not the default 67-char cap — the authoritative per-operator cap lives on pkg/messages.Sender and consults MessagePreferences, so a long-mode user with override=200 can compose up to that without the DTO rejecting them first. In default mode the sender-path gate still rejects anything over 67 chars; the DTO's role is just to short-circuit blatantly oversized bodies before they hit the sender.

Types

type AX25SessionProfile added in v0.12.4

type AX25SessionProfile struct {
	ID        uint32     `json:"id"`
	Name      string     `json:"name"`
	LocalCall string     `json:"local_call"`
	LocalSSID uint8      `json:"local_ssid"`
	DestCall  string     `json:"dest_call"`
	DestSSID  uint8      `json:"dest_ssid"`
	ViaPath   string     `json:"via_path"`
	Mod128    bool       `json:"mod128"`
	Paclen    uint32     `json:"paclen"`
	Maxframe  uint32     `json:"maxframe"`
	T1MS      uint32     `json:"t1_ms"`
	T2MS      uint32     `json:"t2_ms"`
	T3MS      uint32     `json:"t3_ms"`
	N2        uint32     `json:"n2"`
	ChannelID *uint32    `json:"channel_id,omitempty"`
	Pinned    bool       `json:"pinned"`
	LastUsed  *time.Time `json:"last_used,omitempty"`
}

AX25SessionProfile is the on-wire shape of GET / POST / PUT /api/ax25/profiles. Pinned + LastUsed are read-only on POST/PUT; promote with POST /api/ax25/profiles/{id}/pin and update LastUsed via the WebSocket bridge's CONNECTED hook.

type AX25SessionProfilePin added in v0.12.4

type AX25SessionProfilePin struct {
	Pinned bool `json:"pinned"`
}

AX25SessionProfilePin is the on-wire body for POST /api/ax25/profiles/{id}/pin.

type AX25TerminalConfig added in v0.12.4

type AX25TerminalConfig struct {
	ScrollbackRows uint32              `json:"scrollback_rows"`
	CursorBlink    bool                `json:"cursor_blink"`
	DefaultModulo  uint32              `json:"default_modulo"`
	DefaultPaclen  uint32              `json:"default_paclen"`
	Macros         []AX25TerminalMacro `json:"macros"`
	RawTailFilter  string              `json:"raw_tail_filter"`
}

AX25TerminalConfig is the GET response shape for /api/ax25/terminal-config. Macros is exposed as a typed array; the store persists it as a JSON-text column.

type AX25TerminalConfigPatch added in v0.12.4

type AX25TerminalConfigPatch struct {
	ScrollbackRows *uint32             `json:"scrollback_rows,omitempty"`
	CursorBlink    *bool               `json:"cursor_blink,omitempty"`
	DefaultModulo  *uint32             `json:"default_modulo,omitempty"`
	DefaultPaclen  *uint32             `json:"default_paclen,omitempty"`
	Macros         []AX25TerminalMacro `json:"macros,omitempty"`
	RawTailFilter  *string             `json:"raw_tail_filter,omitempty"`
}

AX25TerminalConfigPatch is the PUT body for /api/ax25/terminal-config. Every field is a pointer so the handler can distinguish "field absent from request" (preserve existing column) from "field present with zero value" (validation error). The Macros slice uses nil-vs-empty to mean the same: nil = absent, []{} = explicit clear.

type AX25TerminalMacro added in v0.12.4

type AX25TerminalMacro struct {
	Label   string `json:"label"`
	Payload string `json:"payload"`
}

AX25TerminalMacro is one toolbar macro stored under MacrosJSON. Label is the human-visible button text; Payload is base64-encoded raw bytes the operator wants the macro to send (so the macro can carry terminal control codes, not just printable text).

type AX25TranscriptDetail added in v0.12.4

type AX25TranscriptDetail struct {
	Session AX25TranscriptSession `json:"session"`
	Entries []AX25TranscriptEntry `json:"entries"`
}

AX25TranscriptDetail bundles a session row with its full entry list, served by GET /api/ax25/transcripts/{id}.

type AX25TranscriptEntry added in v0.12.4

type AX25TranscriptEntry struct {
	ID        uint64    `json:"id"`
	TS        time.Time `json:"ts"`
	Direction string    `json:"direction"` // rx|tx
	Kind      string    `json:"kind"`      // data|event
	Payload   []byte    `json:"payload"`
}

AX25TranscriptEntry is one persisted line in a transcript.

type AX25TranscriptSession added in v0.12.4

type AX25TranscriptSession struct {
	ID         uint32     `json:"id"`
	ChannelID  uint32     `json:"channel_id"`
	PeerCall   string     `json:"peer_call"`
	PeerSSID   uint8      `json:"peer_ssid"`
	ViaPath    string     `json:"via_path"`
	StartedAt  time.Time  `json:"started_at"`
	EndedAt    *time.Time `json:"ended_at,omitempty"`
	EndReason  string     `json:"end_reason"`
	ByteCount  uint64     `json:"byte_count"`
	FrameCount uint64     `json:"frame_count"`
}

AX25TranscriptSession is the on-wire shape for the transcripts list. Body bytes are NOT included here -- fetch via /api/ax25/transcripts/{id}.

type AcceptInviteRequest

type AcceptInviteRequest struct {
	// Callsign is the tactical label to subscribe to. Required. Must
	// match the APRS tactical syntax (1-9 of [A-Z0-9-]) after uppercase
	// normalization.
	Callsign string `json:"callsign" binding:"required"`
	// SourceMessageID, when non-zero, identifies the inbound invite
	// message that triggered the accept. Used only for audit — the
	// handler sets InviteAcceptedAt on that row if it resolves to a
	// valid invite for the same tactical. Zero = accept without audit.
	SourceMessageID uint `json:"source_message_id"`
}

AcceptInviteRequest is the body accepted by the accept-invite endpoint (Phase 2 wires POST /api/tacticals/subscribe or similar). Acceptance is tactical-keyed, not message-keyed: the message ID is optional and exists only to let the handler stamp InviteAcceptedAt on the originating row for audit.

type AcceptInviteResponse

type AcceptInviteResponse struct {
	// Tactical is the post-accept state of the subscription. Always
	// populated with Enabled=true (accept is the "turn it on" verb).
	Tactical TacticalCallsignResponse `json:"tactical"`
	// AlreadyMember is true when the operator was already subscribed
	// and enabled before this request. Lets the UI suppress the
	// "Joined TAC" toast and emit "Already a member" instead.
	AlreadyMember bool `json:"already_member"`
}

AcceptInviteResponse is the body returned by a successful accept. Never returns 409 — "already a member" is a normal success with AlreadyMember=true so the client can render a distinct toast without error-handling ceremony.

type Action added in v0.13.0

type Action struct {
	ID                  uint              `json:"id"`
	Name                string            `json:"name"`
	Description         string            `json:"description"`
	Type                string            `json:"type"` // "command" | "webhook"
	CommandPath         string            `json:"command_path,omitempty"`
	WorkingDir          string            `json:"working_dir,omitempty"`
	WebhookMethod       string            `json:"webhook_method,omitempty"`
	WebhookURL          string            `json:"webhook_url,omitempty"`
	WebhookHeaders      map[string]string `json:"webhook_headers,omitempty"`
	WebhookBodyTemplate string            `json:"webhook_body_template,omitempty"`
	TimeoutSec          int               `json:"timeout_sec"`
	OTPRequired         bool              `json:"otp_required"`
	OTPCredentialID     *uint             `json:"otp_credential_id,omitempty"`
	SenderAllowlist     string            `json:"sender_allowlist"`
	ArgSchema           []ArgSpec         `json:"arg_schema"`
	ArgMode             string            `json:"arg_mode"` // "kv" (default) | "freeform"
	RateLimitSec        int               `json:"rate_limit_sec"`
	QueueDepth          int               `json:"queue_depth"`
	MaxReplyLines       int               `json:"max_reply_lines"`
	Enabled             bool              `json:"enabled"`
	LastInvokedAt       *string           `json:"last_invoked_at,omitempty"`
	LastInvokedBy       string            `json:"last_invoked_by,omitempty"`
}

Action is the wire shape for an Action definition.

type ActionInvocation added in v0.13.0

type ActionInvocation struct {
	ID             uint              `json:"id"`
	ActionID       *uint             `json:"action_id,omitempty"`
	ActionName     string            `json:"action_name"`
	SenderCall     string            `json:"sender_call"`
	Source         string            `json:"source"`
	OTPCredID      *uint             `json:"otp_credential_id,omitempty"`
	OTPVerified    bool              `json:"otp_verified"`
	Args           map[string]string `json:"args"`
	Status         string            `json:"status"`
	StatusDetail   string            `json:"status_detail,omitempty"`
	ExitCode       *int              `json:"exit_code,omitempty"`
	HTTPStatus     *int              `json:"http_status,omitempty"`
	OutputCapture  string            `json:"output_capture,omitempty"`
	ReplyText      string            `json:"reply_text"`
	ReplyLineCount int               `json:"reply_line_count"`
	Truncated      bool              `json:"truncated"`
	CreatedAt      string            `json:"created_at"`
}

ActionInvocation is one audit row.

type ActionListenerAddressee added in v0.13.0

type ActionListenerAddressee struct {
	ID        uint   `json:"id"`
	Addressee string `json:"addressee"`
	CreatedAt string `json:"created_at"`
}

ActionListenerAddressee is one extra APRS addressee that triggers the Actions classifier (independent of the station call and tactical aliases).

type AgwRequest

type AgwRequest struct {
	ListenAddr string `json:"listen_addr"`
	Callsigns  string `json:"callsigns"`
	Enabled    bool   `json:"enabled"`
}

AgwRequest is the body accepted by PUT /api/agw (singleton).

func (AgwRequest) ToModel

func (r AgwRequest) ToModel() configstore.AgwConfig

func (AgwRequest) Validate

func (r AgwRequest) Validate() error

type AgwResponse

type AgwResponse struct {
	ID uint32 `json:"id"`
	AgwRequest
}

AgwResponse is the body returned by GET/PUT for the singleton.

func AgwFromModel

func AgwFromModel(m configstore.AgwConfig) AgwResponse

type ArgSpec added in v0.13.0

type ArgSpec struct {
	Key      string `json:"key"`
	Regex    string `json:"regex,omitempty"`
	MaxLen   int    `json:"max_len,omitempty"`
	Required bool   `json:"required,omitempty"`
}

ArgSpec is one entry in an Action's arg_schema.

type AudioDeviceDeleteConflict

type AudioDeviceDeleteConflict struct {
	Error    string            `json:"error"`
	Channels []ChannelResponse `json:"channels"`
}

AudioDeviceDeleteConflict is the body returned by DELETE /api/audio-devices/{id} with a 409 when the device is referenced by one or more channels and the caller did not request cascade deletion. The channels slice lists the referencing channels so the UI can surface them in the confirm dialog.

Wire shape matches the pre-typed `map[string]any{"error": ..., "channels": ...}` literal previously emitted by the handler — byte-identical when the slice fields are populated the same way.

type AudioDeviceDeleteResponse

type AudioDeviceDeleteResponse struct {
	Deleted []ChannelResponse `json:"deleted"`
}

AudioDeviceDeleteResponse is the body returned by DELETE /api/audio-devices/{id} on success. Deleted lists the channels that were removed alongside the device when cascade was requested; empty when the device had no referencing channels.

type AudioDeviceLevelsResponse

type AudioDeviceLevelsResponse map[uint32]*modembridge.DeviceLevel

AudioDeviceLevelsResponse is the body returned by GET /api/audio-devices/levels — a map from device id to the latest cached peak/rms/clipping measurement. Swag cannot render a keyed map[uint32]*T directly in a Swagger 2.0 definition, so the response is documented as {object} and the TypeScript client represents it as Record<string, modembridge.DeviceLevel> (JSON object keys are always strings on the wire).

type AudioDeviceRequest

type AudioDeviceRequest struct {
	Name       string  `json:"name"`
	Direction  string  `json:"direction"`
	SourceType string  `json:"source_type"`
	DevicePath string  `json:"device_path"`
	SampleRate uint32  `json:"sample_rate"`
	Channels   uint32  `json:"channels"`
	Format     string  `json:"format"`
	GainDB     float32 `json:"gain_db"`
}

AudioDeviceRequest is the body accepted by POST /api/audio-devices and PUT /api/audio-devices/{id}.

func (AudioDeviceRequest) ToModel

func (AudioDeviceRequest) ToUpdate

func (AudioDeviceRequest) Validate

func (r AudioDeviceRequest) Validate() error

Validate ensures required fields are set and gain is in range.

type AudioDeviceResponse

type AudioDeviceResponse struct {
	ID uint32 `json:"id"`
	AudioDeviceRequest
}

AudioDeviceResponse is the body returned by GET/POST/PUT for a device.

func AudioDevicesFromModels

func AudioDevicesFromModels(ms []configstore.AudioDevice) []AudioDeviceResponse

type AudioDeviceSetGainRequest

type AudioDeviceSetGainRequest struct {
	GainDB float32 `json:"gain_db"`
}

AudioDeviceSetGainRequest is the body for PUT /api/audio-devices/{id}/gain.

func (AudioDeviceSetGainRequest) Validate

func (r AudioDeviceSetGainRequest) Validate() error

Validate enforces the same gain window as AudioDeviceRequest so the live-update path can't install a value the create/update path would reject.

type BeaconRequest

type BeaconRequest struct {
	Type          string  `json:"type"`
	Channel       uint32  `json:"channel"`
	Callsign      *string `json:"callsign"`
	Destination   string  `json:"destination"`
	Path          string  `json:"path"`
	UseGps        bool    `json:"use_gps"`
	Latitude      float64 `json:"latitude"`
	Longitude     float64 `json:"longitude"`
	AltFt         float64 `json:"alt_ft"`
	Ambiguity     uint32  `json:"ambiguity"`
	SymbolTable   string  `json:"symbol_table"`
	Symbol        string  `json:"symbol"`
	Overlay       string  `json:"overlay"`
	Compress      bool    `json:"compress"`
	Messaging     bool    `json:"messaging"`
	Comment       string  `json:"comment"`
	CommentCmd    string  `json:"comment_cmd"`
	CustomInfo    string  `json:"custom_info"`
	ObjectName    string  `json:"object_name"`
	Power         uint32  `json:"power"`
	Height        uint32  `json:"height"`
	Gain          uint32  `json:"gain"`
	Dir           uint32  `json:"dir"`
	Freq          string  `json:"freq"`
	Tone          string  `json:"tone"`
	FreqOffset    string  `json:"freq_offset"`
	DelaySeconds  uint32  `json:"delay_seconds"`
	EverySeconds  uint32  `json:"interval"`
	SlotSeconds   int32   `json:"slot_seconds"`
	SmartBeacon   bool    `json:"smart_beacon"`
	SbFastSpeed   uint32  `json:"sb_fast_speed"`
	SbSlowSpeed   uint32  `json:"sb_slow_speed"`
	SbFastRate    uint32  `json:"sb_fast_rate"`
	SbSlowRate    uint32  `json:"sb_slow_rate"`
	SbTurnAngle   uint32  `json:"sb_turn_angle"`
	SbTurnSlope   uint32  `json:"sb_turn_slope"`
	SbMinTurnTime uint32  `json:"sb_min_turn_time"`
	SendToAPRSIS  bool    `json:"send_to_aprs_is"`
	Enabled       bool    `json:"enabled"`
}

BeaconRequest is the body accepted by POST /api/beacons and PUT /api/beacons/{id}.

Callsign is a per-beacon callsign override (see centralized station-callsign plan, D2/D3). The request DTO uses *string so the three meaningful states are expressible independently:

  • nil → field omitted; on PUT, leave the stored value unchanged. On POST, treated the same as "".
  • "" → inherit from StationConfig at transmit time.
  • non-empty → explicit override (e.g. a vanity or tactical call).

The response DTO carries Callsign as plain string — an empty value in the response means "inherits from station callsign".

func (BeaconRequest) ApplyToUpdate

func (r BeaconRequest) ApplyToUpdate(id uint32, existing configstore.Beacon) configstore.Beacon

ApplyToUpdate merges the request onto an existing stored beacon, honouring pointer-nil = "leave unchanged" on the Callsign override field. All other fields are overwritten with the request value (replace-style PUT).

func (BeaconRequest) ToModel

func (r BeaconRequest) ToModel() configstore.Beacon

func (BeaconRequest) ToUpdate

func (r BeaconRequest) ToUpdate(id uint32) configstore.Beacon

func (BeaconRequest) Validate

func (r BeaconRequest) Validate() error

Validate rejects configurations that would cause the scheduler to skip transmission at send time. Position/igate beacons must either source coordinates from the GPS cache or carry non-zero fixed coordinates. The Callsign override field is no longer validated here — empty / nil mean "inherit from StationConfig", which is now the canonical source of truth.

type BeaconResponse

type BeaconResponse struct {
	ID            uint32  `json:"id"`
	Type          string  `json:"type"`
	Channel       uint32  `json:"channel"`
	Callsign      string  `json:"callsign"`
	Destination   string  `json:"destination"`
	Path          string  `json:"path"`
	UseGps        bool    `json:"use_gps"`
	Latitude      float64 `json:"latitude"`
	Longitude     float64 `json:"longitude"`
	AltFt         float64 `json:"alt_ft"`
	Ambiguity     uint32  `json:"ambiguity"`
	SymbolTable   string  `json:"symbol_table"`
	Symbol        string  `json:"symbol"`
	Overlay       string  `json:"overlay"`
	Compress      bool    `json:"compress"`
	Messaging     bool    `json:"messaging"`
	Comment       string  `json:"comment"`
	CommentCmd    string  `json:"comment_cmd"`
	CustomInfo    string  `json:"custom_info"`
	ObjectName    string  `json:"object_name"`
	Power         uint32  `json:"power"`
	Height        uint32  `json:"height"`
	Gain          uint32  `json:"gain"`
	Dir           uint32  `json:"dir"`
	Freq          string  `json:"freq"`
	Tone          string  `json:"tone"`
	FreqOffset    string  `json:"freq_offset"`
	DelaySeconds  uint32  `json:"delay_seconds"`
	EverySeconds  uint32  `json:"interval"`
	SlotSeconds   int32   `json:"slot_seconds"`
	SmartBeacon   bool    `json:"smart_beacon"`
	SbFastSpeed   uint32  `json:"sb_fast_speed"`
	SbSlowSpeed   uint32  `json:"sb_slow_speed"`
	SbFastRate    uint32  `json:"sb_fast_rate"`
	SbSlowRate    uint32  `json:"sb_slow_rate"`
	SbTurnAngle   uint32  `json:"sb_turn_angle"`
	SbTurnSlope   uint32  `json:"sb_turn_slope"`
	SbMinTurnTime uint32  `json:"sb_min_turn_time"`
	SendToAPRSIS  bool    `json:"send_to_aprs_is"`
	Enabled       bool    `json:"enabled"`
}

BeaconResponse is the body returned by GET/POST/PUT for a beacon. Callsign is the stored value — empty means "inherit from station callsign" at transmit time.

func BeaconFromModel

func BeaconFromModel(m configstore.Beacon) BeaconResponse

func BeaconsFromModels

func BeaconsFromModels(ms []configstore.Beacon) []BeaconResponse

type BeaconSendResponse

type BeaconSendResponse struct {
	Status string `json:"status"` // "sent"
}

BeaconSendResponse is the body returned by POST /api/beacons/{id}/send when a one-shot transmission has been handed to the beacon scheduler.

type Catalog added in v0.12.1

type Catalog struct {
	SchemaVersion int               `json:"schemaVersion"`
	GeneratedAt   string            `json:"generatedAt"`
	Countries     []CatalogCountry  `json:"countries"`
	Provinces     []CatalogProvince `json:"provinces"`
	States        []CatalogState    `json:"states"`
}

Catalog is the response shape for GET /api/maps/catalog. It mirrors the Worker's /manifest.json output 1:1 so the UI can reuse the arrays without translation. SchemaVersion is pinned to 1; bumps require a coordinated UI update.

type CatalogCountry added in v0.12.1

type CatalogCountry struct {
	ISO2      string      `json:"iso2"`
	Name      string      `json:"name"`
	SizeBytes int64       `json:"sizeBytes"`
	SHA256    string      `json:"sha256"`
	BBox      *[4]float64 `json:"bbox,omitempty"`
}

type CatalogProvince added in v0.12.1

type CatalogProvince struct {
	ISO2      string      `json:"iso2"`
	Slug      string      `json:"slug"`
	Name      string      `json:"name"`
	Code      string      `json:"code,omitempty"`
	SizeBytes int64       `json:"sizeBytes"`
	SHA256    string      `json:"sha256"`
	BBox      *[4]float64 `json:"bbox,omitempty"`
}

type CatalogState added in v0.12.1

type CatalogState struct {
	Slug      string      `json:"slug"`
	Name      string      `json:"name"`
	Code      string      `json:"code,omitempty"`
	SizeBytes int64       `json:"sizeBytes"`
	SHA256    string      `json:"sha256"`
	BBox      *[4]float64 `json:"bbox,omitempty"`
}

type ChannelBacking

type ChannelBacking struct {
	Modem   ChannelModemBacking   `json:"modem"`
	KissTnc []ChannelKissTncEntry `json:"kiss_tnc"`
	Summary string                `json:"summary"`
	Health  string                `json:"health"`
	Tx      TxCapability          `json:"tx"`
}

ChannelBacking describes the runtime backing — modem and/or KISS-TNC interfaces — attached to a channel. Computed at request time from store + kissMgr.Status() + modembridge.SessionStatus().

Summary is one of "modem", "kiss-tnc", or "unbound". Dual-backend is forbidden at the config layer (D3) so this is always a single value.

Health is one of "live" (≥1 backend instance is up), "down" (backends exist but all are down), or "unbound" (no backend configured).

Tx is the channel's TX-capability signal used by the beacon / iGate / digipeater picker predicate and by the server-side referrer validator. It is derived from the same underlying fields as Summary + Health but answers a different question: "can a frame submitted on this channel actually be transmitted?" See TxCapability's contract for the single-branch decision rule and the Reason invariant.

type ChannelKissTncEntry

type ChannelKissTncEntry struct {
	InterfaceID         uint32 `json:"interface_id"`
	InterfaceName       string `json:"interface_name"`
	AllowTxFromGovernor bool   `json:"allow_tx_from_governor"`
	State               string `json:"state"`
	LastError           string `json:"last_error,omitempty"`
	RetryAtUnixMs       int64  `json:"retry_at_unix_ms,omitempty"`
}

ChannelKissTncEntry is one TNC-mode KISS interface attached to the channel. AllowTxFromGovernor reflects KissInterface.AllowTxFromGovernor — Phase 3's opt-in flag gating governor-originated TX fan-out to this interface.

State / LastError / RetryAtUnixMs are best-effort today: Phase 1 only supports server-listen interfaces, which expose a state of "listening" (or "stopped" when not running) with no error or retry timestamp. Phase 4 fills these in for tcp-client interfaces. Fields are pre-declared now so the JSON contract doesn't shift between phases.

type ChannelLookup

type ChannelLookup interface {
	ChannelExists(ctx context.Context, id uint32) (bool, error)
}

ChannelLookup is the narrow read surface DTO validators need to reject writes that reference a non-existent channel. Implemented by *configstore.Store via its ChannelExists method. Kept as an interface so tests can stub it without pulling the full configstore into scope, and so the DTO package doesn't need to import configstore just to typecheck the signature (DTOs already depend on configstore for model types; the interface is a convenience, not an isolation layer).

type ChannelModemBacking

type ChannelModemBacking struct {
	Active bool   `json:"active"`
	Reason string `json:"reason,omitempty"`
}

ChannelModemBacking reports whether an audio modem currently serves this channel. Active is true when the channel has a bound input audio device and the modem subprocess is running. Reason is populated when Active is false; empty otherwise.

type ChannelRequest

type ChannelRequest struct {
	Name           string  `json:"name"`
	Mode           string  `json:"mode"`
	InputDeviceID  *uint32 `json:"input_device_id"`
	InputChannel   uint32  `json:"input_channel"`
	OutputDeviceID uint32  `json:"output_device_id"`
	OutputChannel  uint32  `json:"output_channel"`
	ModemType      string  `json:"modem_type"`
	BitRate        uint32  `json:"bit_rate"`
	MarkFreq       uint32  `json:"mark_freq"`
	SpaceFreq      uint32  `json:"space_freq"`
	Profile        string  `json:"profile"`
	NumSlicers     uint32  `json:"num_slicers"`
	FixBits        string  `json:"fix_bits"`
	FX25Encode     bool    `json:"fx25_encode"`
	IL2PEncode     bool    `json:"il2p_encode"`
	NumDecoders    uint32  `json:"num_decoders"`
	DecoderOffset  int32   `json:"decoder_offset"`
}

ChannelRequest is the body accepted by POST /api/channels and PUT /api/channels/{id}.

InputDeviceID is a pointer (*uint32) to model the KISS-only channel case introduced in Phase 2: a null value means the channel is not audio-backed and will be serviced exclusively by a KISS-TNC interface. When null, ModemType / BitRate / etc. are accepted but unused by the modem subprocess (see modembridge/session.go pushConfiguration). When non-null, the device must exist and have direction=input; configstore enforces that at write time.

func (ChannelRequest) ToModel

func (r ChannelRequest) ToModel() configstore.Channel

ToModel maps a create request to a storage model.

func (ChannelRequest) ToUpdate

func (r ChannelRequest) ToUpdate(id uint32) configstore.Channel

ToUpdate maps an update request to a storage model, preserving id.

func (ChannelRequest) Validate

func (r ChannelRequest) Validate() error

Validate ensures required fields are set. Deep validation (device existence, channel range) still runs inside configstore.

InputDeviceID follows the Phase 2 nullable contract:

  • nil → KISS-only channel; OutputDeviceID must be 0 (no TX audio without RX audio).
  • non-nil → modem-backed channel; device existence + direction is validated by configstore.validateChannel.

type ChannelResponse

type ChannelResponse struct {
	ID      uint32          `json:"id"`
	Backing *ChannelBacking `json:"backing,omitempty"`
	ChannelRequest
}

ChannelResponse is the body returned by GET/POST/PUT for a channel.

Backing is a computed, read-only object that tells the UI where a frame submitted on this channel will actually go (see design decision D7 in .context/2026-04-20-kiss-tcp-client-and-channel-backing.md). Empty in POST/PUT round-trips because the store model carries no backing state of its own; populated only by list/get handlers that have access to the running modembridge + kiss manager. Omitted from JSON when zero so create/update response bodies don't carry stale "unbound" placeholders.

func ChannelFromModel

func ChannelFromModel(m configstore.Channel) ChannelResponse

ChannelFromModel converts a storage model into a response DTO. The Backing field is left nil — list/get handlers populate it after the base mapping using the live kiss manager and modem bridge status.

InputDeviceID is copied as-is (both sides are *uint32). A nil pointer round-trips as JSON null, which the TS client surfaces as `input_device_id: null` — the segmented type picker on the Channels page treats that as "KISS-TNC only".

func ChannelsFromModels

func ChannelsFromModels(ms []configstore.Channel) []ChannelResponse

ChannelsFromModels maps a slice for list responses.

type ConversationSummary

type ConversationSummary struct {
	ThreadKind       string    `json:"thread_kind"`
	ThreadKey        string    `json:"thread_key"`
	Alias            string    `json:"alias,omitempty"`
	LastAt           time.Time `json:"last_at"`
	LastSnippet      string    `json:"last_snippet"`
	LastSenderCall   string    `json:"last_sender_call"`
	UnreadCount      int       `json:"unread_count"`
	TotalCount       int       `json:"total_count"`
	ParticipantCount int       `json:"participant_count,omitempty"`
	Archived         bool      `json:"archived"`
}

ConversationSummary is the wire format for one thread in the master pane's rollup. Tactical threads populate ParticipantCount; DM rows omit it.

func ConversationSummaryFromModel

func ConversationSummaryFromModel(s messages.ConversationSummary, alias string) ConversationSummary

ConversationSummaryFromModel renders one store summary into its DTO. alias is looked up by the caller from the tactical map.

type DigipeaterConfigRequest

type DigipeaterConfigRequest struct {
	Enabled             bool    `json:"enabled"`
	DedupeWindowSeconds uint32  `json:"dedupe_window_seconds"`
	MyCall              *string `json:"my_call"`
}

DigipeaterConfigRequest is the body accepted by PUT /api/digipeater.

MyCall is a per-station callsign override (see centralized station-callsign plan, D2/D3). The request DTO uses *string so the three meaningful states are expressible independently:

  • nil → field omitted; leave the stored value unchanged.
  • "" → inherit from StationConfig at transmit time.
  • non-empty → explicit override (e.g. "MTNTOP-1").

The response DTO carries MyCall as plain string — an empty value in the response means "inherits from station callsign". The override is stored verbatim (no case normalization here; the operator's casing is the source of truth for what they typed).

func (DigipeaterConfigRequest) ApplyToModel

ApplyToModel merges the request fields onto an existing stored DigipeaterConfig. Fields whose request representation is a pointer only overwrite the target when the pointer is non-nil, preserving "field omitted = leave unchanged" semantics on this PUT endpoint. Other fields are written unconditionally (consistent with the replace-style PUT pattern used elsewhere in webapi).

func (DigipeaterConfigRequest) Validate

func (r DigipeaterConfigRequest) Validate() error

type DigipeaterConfigResponse

type DigipeaterConfigResponse struct {
	ID                  uint32 `json:"id"`
	Enabled             bool   `json:"enabled"`
	DedupeWindowSeconds uint32 `json:"dedupe_window_seconds"`
	MyCall              string `json:"my_call"`
}

type DigipeaterRuleRequest

type DigipeaterRuleRequest struct {
	FromChannel uint32 `json:"from_channel"`
	ToChannel   uint32 `json:"to_channel"`
	Alias       string `json:"alias"`
	AliasType   string `json:"alias_type"`
	MaxHops     uint32 `json:"max_hops"`
	Action      string `json:"action"`
	Priority    uint32 `json:"priority"`
	Enabled     bool   `json:"enabled"`
}

DigipeaterRuleRequest is the body accepted by POST /api/digipeater/rules and PUT /api/digipeater/rules/{id}.

func (DigipeaterRuleRequest) ToModel

func (DigipeaterRuleRequest) ToUpdate

func (DigipeaterRuleRequest) Validate

func (r DigipeaterRuleRequest) Validate() error

type DigipeaterRuleResponse

type DigipeaterRuleResponse struct {
	ID uint32 `json:"id"`
	DigipeaterRuleRequest
}

func DigipeaterRulesFromModels

func DigipeaterRulesFromModels(ms []configstore.DigipeaterRule) []DigipeaterRuleResponse

type DownloadStatus

type DownloadStatus struct {
	Slug            string    `json:"slug"`
	State           string    `json:"state"`
	BytesTotal      int64     `json:"bytes_total"`
	BytesDownloaded int64     `json:"bytes_downloaded"`
	DownloadedAt    time.Time `json:"downloaded_at"`
	ErrorMessage    string    `json:"error_message,omitempty"`
}

DownloadStatus is the response shape for /api/maps/downloads endpoints. State is one of: "absent" | "pending" | "downloading" | "complete" | "error".

DownloadedAt has no `omitempty` — Go's encoding/json silently treats `omitempty` on a struct value as a no-op (only the empty interface, nil pointer, etc. trigger omission), so the field always serializes. A zero-value timestamp on the wire signals "not complete yet"; clients must use State, not the timestamp, to decide whether the download finished.

type GPSRequest

type GPSRequest struct {
	SourceType string `json:"source"`
	Device     string `json:"serial_port"`
	BaudRate   uint32 `json:"baud_rate"`
	GpsdHost   string `json:"gpsd_host"`
	GpsdPort   uint32 `json:"gpsd_port"`
}

GPSRequest is the body accepted by PUT /api/gps (singleton). Enabled is derived from SourceType on the handler side so the UI doesn't need a separate toggle.

func (GPSRequest) ToModel

func (r GPSRequest) ToModel() configstore.GPSConfig

func (GPSRequest) Validate

func (r GPSRequest) Validate() error

type GPSResponse

type GPSResponse struct {
	ID uint32 `json:"id"`
	GPSRequest
	Enabled bool `json:"enabled"`
}

GPSResponse is the body returned by GET/PUT for the singleton.

func GPSFromModel

func GPSFromModel(m configstore.GPSConfig) GPSResponse

type HealthResponse

type HealthResponse struct {
	StartedAt string `json:"started_at"` // process start time, RFC3339
	Status    string `json:"status"`     // "ok"
	Time      string `json:"time"`       // current UTC time, RFC3339
}

HealthResponse is the body returned by GET /api/health. Used by orchestration (systemd, docker healthcheck) and the web UI header.

Field order matches the alphabetical key order produced by the prior map[string]any encoding so the emitted JSON is byte-identical.

type IGateConfigRequest

type IGateConfigRequest struct {
	Enabled         bool   `json:"enabled"`
	Server          string `json:"server"`
	Port            uint32 `json:"port"`
	ServerFilter    string `json:"server_filter"`
	SimulationMode  bool   `json:"simulation_mode"`
	GateRfToIs      bool   `json:"gate_rf_to_is"`
	GateIsToRf      bool   `json:"gate_is_to_rf"`
	RfChannel       uint32 `json:"rf_channel"`
	MaxMsgHops      uint32 `json:"max_msg_hops"`
	SoftwareName    string `json:"software_name"`
	SoftwareVersion string `json:"software_version"`
	TxChannel       uint32 `json:"tx_channel"`
}

IGateConfigRequest is the body accepted by PUT /api/igate/config.

Per the centralized station-callsign plan (D3, D4), the iGate login callsign is StationConfig.Callsign and the APRS-IS passcode is a computed implementation detail. Neither is part of this DTO; the corresponding DB columns are retained for downgrade-safety only.

func (IGateConfigRequest) ToModel

func (IGateConfigRequest) Validate

func (r IGateConfigRequest) Validate() error

Validate enforces syntax rules on fields the handler can't re-check cheaply. Today that means rejecting `|` in ServerFilter: APRS-IS filter expressions are space-separated OR'd clauses (see https://www.aprs-is.net/javAPRSFilter.aspx) — a pipe is not a valid separator. Some T2 servers silently drop the whole filter when they see one, which turns into "the iGate is receiving every packet on the network" without any on-box symptom. Reject at save time so the UI can surface the error before we ever log in.

type IGateConfigResponse

type IGateConfigResponse struct {
	ID uint32 `json:"id"`
	IGateConfigRequest
}

type IGateRfFilterRequest

type IGateRfFilterRequest struct {
	Channel  uint32 `json:"channel"`
	Type     string `json:"type"`
	Pattern  string `json:"pattern"`
	Action   string `json:"action"`
	Priority uint32 `json:"priority"`
	Enabled  bool   `json:"enabled"`
}

IGateRfFilterRequest is the body accepted by POST /api/igate/filters and PUT /api/igate/filters/{id}.

func (IGateRfFilterRequest) ToModel

func (IGateRfFilterRequest) ToUpdate

func (IGateRfFilterRequest) Validate

func (r IGateRfFilterRequest) Validate() error

type IGateRfFilterResponse

type IGateRfFilterResponse struct {
	ID uint32 `json:"id"`
	IGateRfFilterRequest
}

func IGateRfFiltersFromModels

func IGateRfFiltersFromModels(ms []configstore.IGateRfFilter) []IGateRfFilterResponse

type KissRequest

type KissRequest struct {
	Type             string `json:"type"`
	TcpPort          int    `json:"tcp_port"`
	SerialDevice     string `json:"serial_device"`
	BaudRate         uint32 `json:"baud_rate"`
	Channel          uint32 `json:"channel"`
	Mode             string `json:"mode"`
	TncIngressRateHz uint32 `json:"tnc_ingress_rate_hz"`
	TncIngressBurst  uint32 `json:"tnc_ingress_burst"`
	// AllowTxFromGovernor opts this TNC-mode interface in to receive
	// frames from the TX governor (beacon / digipeater / iGate /
	// KISS / AGW submissions). Only meaningful when Mode == "tnc";
	// the validator rejects true with any other mode. Default false
	// on migrated rows so existing TNC servers do not silently start
	// transmitting; Phase 4 sets the DTO default to true for newly
	// created tcp-client rows.
	AllowTxFromGovernor bool `json:"allow_tx_from_governor"`
	// Tcp-client fields (Phase 4): RemoteHost:RemotePort is the dial
	// target; ReconnectInitMs / ReconnectMaxMs size the supervisor's
	// exponential-backoff reconnect schedule. Unused / zero for
	// Type != "tcp-client".
	RemoteHost      string `json:"remote_host"`
	RemotePort      uint16 `json:"remote_port"`
	ReconnectInitMs uint32 `json:"reconnect_init_ms"`
	ReconnectMaxMs  uint32 `json:"reconnect_max_ms"`
}

KissRequest is the body accepted by POST /api/kiss and PUT /api/kiss/{id}. The frontend uses tcp_port (int) rather than listen_addr (host:port string); the store converts between them.

Mode defaults to "modem" when the client omits the field. The two TncIngress* fields default to the KissInterface struct tags (50/100) via the store-layer normalizer when sent as zero; the handler still rejects obviously-wrong non-zero values up front so the error lands at the API boundary instead of the SQLite boundary.

func (KissRequest) ToModel

func (KissRequest) ToUpdate

func (KissRequest) Validate

func (r KissRequest) Validate() error

type KissResponse

type KissResponse struct {
	ID                  uint32 `json:"id"`
	Type                string `json:"type"`
	TcpPort             int    `json:"tcp_port"`
	SerialDevice        string `json:"serial_device"`
	BaudRate            uint32 `json:"baud_rate"`
	Channel             uint32 `json:"channel"`
	Mode                string `json:"mode"`
	TncIngressRateHz    uint32 `json:"tnc_ingress_rate_hz"`
	TncIngressBurst     uint32 `json:"tnc_ingress_burst"`
	AllowTxFromGovernor bool   `json:"allow_tx_from_governor"`
	NeedsReconfig       bool   `json:"needs_reconfig"`
	// Tcp-client fields (Phase 4). Zero-valued for non-tcp-client rows.
	RemoteHost      string `json:"remote_host"`
	RemotePort      uint16 `json:"remote_port"`
	ReconnectInitMs uint32 `json:"reconnect_init_ms"`
	ReconnectMaxMs  uint32 `json:"reconnect_max_ms"`
	// Per-interface runtime status (Phase 4). Surfaced verbatim from
	// kiss.Manager.Status(); zero-valued when the row is not running
	// or when the manager has nothing to report. Omitted from the
	// wire when the interface is not tcp-client (State == "" +
	// omitempty).
	State          string `json:"state,omitempty"`
	LastError      string `json:"last_error,omitempty"`
	RetryAtUnixMs  int64  `json:"retry_at_unix_ms,omitempty"`
	PeerAddr       string `json:"peer_addr,omitempty"`
	ConnectedSince int64  `json:"connected_since,omitempty"`
	ReconnectCount uint64 `json:"reconnect_count,omitempty"`
	BackoffSeconds uint32 `json:"backoff_seconds,omitempty"`
}

KissResponse is the body returned by GET/POST/PUT for a KISS interface. Keeps the current shape exactly: tcp_port is derived from listen_addr, and bogus/unparseable ports surface as 0.

AllowTxFromGovernor round-trips KissInterface.AllowTxFromGovernor — the Phase 3 opt-in flag that gates per-interface governor TX. The field is always present but meaningful only when Mode == "tnc". NeedsReconfig mirrors KissInterface.NeedsReconfig so the Kiss page can surface a "reconfigure me" banner on rows whose Channel was nulled by a Phase 5 cascade delete.

func KissesFromModels

func KissesFromModels(ms []configstore.KissInterface) []KissResponse

type MapsConfigRequest

type MapsConfigRequest struct {
	Source string `json:"source"`
}

MapsConfigRequest is the body for PUT /api/preferences/maps. Only Source is updatable from the client; Callsign and Token are managed by the /register sub-endpoint to keep the registration ceremony out of generic preference writes.

func (MapsConfigRequest) Validate

func (r MapsConfigRequest) Validate() error

type MapsConfigResponse

type MapsConfigResponse struct {
	Source       string    `json:"source"`
	Callsign     string    `json:"callsign,omitempty"`
	Registered   bool      `json:"registered"`
	RegisteredAt time.Time `json:"registered_at"`
	Token        string    `json:"token,omitempty"`
}

MapsConfigResponse is what GET /api/preferences/maps and the PUT echo back. Token is omitted unless ?include_token=1 is set on the GET — see the handler. Registered is true iff a token is present.

RegisteredAt always serializes; when not registered it will be the Go zero time. The Registered bool is the authoritative source of truth for "is this populated" — don't infer from the timestamp.

type MessageChange

type MessageChange struct {
	ID      uint64           `json:"id"`
	Kind    string           `json:"kind"`
	Message *MessageResponse `json:"message,omitempty"`
}

MessageChange is one entry in a MessageListResponse or an SSE frame. Kind is "created" | "updated" | "deleted"; Message is nil for deletions.

type MessageListResponse

type MessageListResponse struct {
	Cursor  string          `json:"cursor"`
	Changes []MessageChange `json:"changes"`
}

MessageListResponse is the envelope returned by GET /api/messages. The future SSE endpoint reuses the MessageChange shape inside its data frames so clients share a single reconciliation codepath.

type MessagePreferencesRequest

type MessagePreferencesRequest struct {
	FallbackPolicy   string `json:"fallback_policy"`
	DefaultPath      string `json:"default_path"`
	RetryMaxAttempts uint32 `json:"retry_max_attempts"`
	RetentionDays    uint32 `json:"retention_days"`
	// MaxMessageTextOverride raises the default 67-char addressee-line
	// direct-message cap. 0 (or field absent) means "use the default";
	// any positive value must fall in [MaxMessageText+1, MaxMessageTextUnsafe]
	// (68..200). Applies to addressee-line DMs only — bulletins, status
	// beacons, and position/weather frames are unaffected. The server
	// rejects out-of-range values with 400 rather than silently clamping
	// so operators see a clear error.
	MaxMessageTextOverride uint32 `json:"max_message_text_override,omitempty"`
}

MessagePreferencesRequest is the body accepted by PUT /api/messages/preferences.

func (MessagePreferencesRequest) ToModel

ToModel maps the request to the persisted configstore row.

func (MessagePreferencesRequest) Validate

func (r MessagePreferencesRequest) Validate() error

Validate clamps FallbackPolicy to the canonical enum and enforces retry_max_attempts > 0 (zero is surprising — treat as invalid so operators see a clear error instead of the defaults swallow silently).

type MessagePreferencesResponse

type MessagePreferencesResponse struct {
	FallbackPolicy   string `json:"fallback_policy"`
	DefaultPath      string `json:"default_path"`
	RetryMaxAttempts uint32 `json:"retry_max_attempts"`
	RetentionDays    uint32 `json:"retention_days"`
	// MaxMessageTextOverride mirrors the request field on read. 0
	// means "default enforce 67" — older servers that have never been
	// upgraded return 0 here, which is also what a fresh singleton with
	// no override set returns. Positive values fall in
	// (MaxMessageText, MaxMessageTextUnsafe].
	MaxMessageTextOverride uint32 `json:"max_message_text_override"`
}

MessagePreferencesResponse is the body returned by GET/PUT preferences.

func MessagePreferencesFromModel

func MessagePreferencesFromModel(m configstore.MessagePreferences) MessagePreferencesResponse

MessagePreferencesFromModel renders one row. Applies policy normalization so GETs against an uninitialised row return a sensible default instead of an empty string.

type MessageResponse

type MessageResponse struct {
	ID             uint64     `json:"id"`
	Direction      string     `json:"direction"` // "in" | "out"
	Status         string     `json:"status"`    // derived — see DeriveMessageStatus
	OurCall        string     `json:"our_call"`
	PeerCall       string     `json:"peer_call"`
	FromCall       string     `json:"from_call"`
	ToCall         string     `json:"to_call"`
	Text           string     `json:"text"`
	MsgID          string     `json:"msg_id,omitempty"`
	CreatedAt      time.Time  `json:"created_at"`
	ReceivedAt     *time.Time `json:"received_at,omitempty"`
	SentAt         *time.Time `json:"sent_at,omitempty"`
	AckedAt        *time.Time `json:"acked_at,omitempty"`
	Source         string     `json:"source,omitempty"` // "rf" | "is"
	Channel        *uint32    `json:"channel,omitempty"`
	Path           string     `json:"path,omitempty"`
	Via            string     `json:"via,omitempty"`
	Unread         bool       `json:"unread"`
	Attempts       uint32     `json:"attempts"`
	NextRetryAt    *time.Time `json:"next_retry_at,omitempty"`
	FailureReason  string     `json:"failure_reason,omitempty"`
	IsAck          bool       `json:"is_ack,omitempty"`
	IsBulletin     bool       `json:"is_bulletin,omitempty"`
	ThreadKind     string     `json:"thread_kind"`
	ThreadKey      string     `json:"thread_key"`
	ReceivedByCall string     `json:"received_by_call,omitempty"`
	// Kind is the body classification. Always populated — "text" for
	// normal messages, "invite" for tactical invitations. Never omitted
	// so clients can use a simple equality check without worrying about
	// a legacy empty string.
	Kind string `json:"kind"`
	// InviteTactical is the tactical callsign referenced by an invite.
	// Empty (and omitted) on non-invite rows.
	InviteTactical string `json:"invite_tactical,omitempty"`
	// InviteAcceptedAt is audit-only: set when the local operator
	// accepted this invite. The UI must NOT use this to decide "joined"
	// state — that comes from the live TacticalSet cache. Kept so
	// operators can see when/if an invite was acted on.
	InviteAcceptedAt *time.Time `json:"invite_accepted_at,omitempty"`
	// Extended is true when the transmitted body exceeded the default
	// MaxMessageText (67). The UI renders an "extended" badge on these
	// rows so operators can correlate if recipients report missing or
	// truncated messages. Derived from len(Text) > MaxMessageText; no
	// dedicated column.
	Extended bool `json:"extended,omitempty"`
}

MessageResponse is the full wire shape for one message row. The Status field is derived server-side; clients don't infer it from the underlying columns.

func MessageFromModel

func MessageFromModel(m configstore.Message) MessageResponse

MessageFromModel renders one row into its DTO. Channel is surfaced as a *uint32 so "unset" (0) serializes as omitted rather than as a semantic "channel 0" that would confuse clients.

func MessagesFromModels

func MessagesFromModels(ms []configstore.Message) []MessageResponse

MessagesFromModels renders a slice of rows.

type MessagesConfig added in v0.12.4

type MessagesConfig struct {
	TxChannel uint32 `json:"tx_channel"` // 0 = auto-resolve
}

MessagesConfig is the on-wire shape of GET/PUT /api/messages/config.

type OTPCredential added in v0.13.0

type OTPCredential struct {
	ID         uint     `json:"id"`
	Name       string   `json:"name"`
	Issuer     string   `json:"issuer"`
	Account    string   `json:"account"`
	Algorithm  string   `json:"algorithm"`
	Digits     int      `json:"digits"`
	Period     int      `json:"period"`
	CreatedAt  string   `json:"created_at"`
	LastUsedAt *string  `json:"last_used_at,omitempty"`
	UsedBy     []string `json:"used_by,omitempty"` // Action names that reference this cred
}

OTPCredential is the safe wire shape for a TOTP credential. SecretB32 is intentionally absent — see OTPCredentialCreated for the one-shot reveal on POST.

type OTPCredentialCreated added in v0.13.0

type OTPCredentialCreated struct {
	OTPCredential
	SecretB32  string `json:"secret_b32"`
	OtpAuthURI string `json:"otpauth_uri"`
}

OTPCredentialCreated is the response body for POST /api/otp-credentials. SecretB32 and OtpAuthURI are returned only on this response and never read back.

type ParticipantResponse

type ParticipantResponse struct {
	Callsign     string    `json:"callsign"`
	LastActive   time.Time `json:"last_active"`
	MessageCount int       `json:"message_count"`
}

ParticipantResponse is one distinct sender on a tactical thread.

type ParticipantsEnvelope

type ParticipantsEnvelope struct {
	Participants        []ParticipantResponse `json:"participants"`
	EffectiveWithinDays int                   `json:"effective_within_days"`
}

ParticipantsEnvelope wraps ParticipantResponse with the effective window (in days) after retention clamp, so the UI can caption the chip row honestly even when a 7d request was clamped to 3d.

type PositionLogRequest

type PositionLogRequest struct {
	Enabled bool `json:"enabled"`
}

PositionLogRequest is the body accepted by PUT /api/position-log. The database path is controlled by the -history-db flag, not the API.

type PositionLogResponse

type PositionLogResponse struct {
	Enabled bool   `json:"enabled"`
	DBPath  string `json:"db_path"`
}

PositionLogResponse is returned by GET/PUT for the singleton. DBPath is informational (read-only from the client's perspective).

type PttRequest

type PttRequest struct {
	ChannelID  uint32 `json:"channel_id"`
	Method     string `json:"method"`
	DevicePath string `json:"device_path"`
	// GpioPin is the CM108 HID GPIO pin number (1-indexed, default 3). Not used
	// by the `gpio` method, which references `gpio_line` instead to avoid
	// indexing ambiguity between CM108 pin numbers and gpiochip line offsets.
	GpioPin uint32 `json:"gpio_pin"`
	// GpioLine is the gpiochip v2 line offset (0-indexed) used by the `gpio`
	// method. Ignored for every other method.
	GpioLine   uint32 `json:"gpio_line"`
	Invert     bool   `json:"invert"`
	SlotTimeMs uint32 `json:"slot_time_ms"`
	Persist    uint32 `json:"persist"`
	DwaitMs    uint32 `json:"dwait_ms"`
}

PttRequest is the body accepted by POST /api/ptt and PUT /api/ptt/{channel}. The store upserts by channel_id.

func (PttRequest) ToModel

func (r PttRequest) ToModel() configstore.PttConfig

func (PttRequest) ToUpdate

func (r PttRequest) ToUpdate(channelID uint32) configstore.PttConfig

ToUpdate maps an update request to a storage model, pinning the channel id from the URL instead of the body so path-wins semantics match the current handler.

func (PttRequest) Validate

func (r PttRequest) Validate() error

type PttResponse

type PttResponse struct {
	ID uint32 `json:"id"`
	PttRequest
}

PttResponse is the body returned by GET/POST/PUT for a PTT config.

func PttFromModel

func PttFromModel(m configstore.PttConfig) PttResponse

func PttsFromModels

func PttsFromModels(ms []configstore.PttConfig) []PttResponse

type RegisterRequest

type RegisterRequest struct {
	Callsign string `json:"callsign"`
}

RegisterRequest is the body for POST /api/preferences/maps/register.

type RegisterResponse

type RegisterResponse = MapsConfigResponse

RegisterResponse mirrors MapsConfigResponse — after a successful registration, the endpoint returns the same shape the GET would return next, including the freshly issued token (always, just this once, so the UI can offer the operator an export-token-to-file flow before it goes back to being suppressed).

type ReleaseNoteDTO

type ReleaseNoteDTO struct {
	SchemaVersion int    `json:"schema_version"`
	Version       string `json:"version"`
	Date          string `json:"date"`  // ISO YYYY-MM-DD
	Style         string `json:"style"` // "info" | "cta"
	Title         string `json:"title"`
	Body          string `json:"body"` // pre-sanitized HTML
}

ReleaseNoteDTO is a single release-note entry. Body carries server-sanitized, server-rendered HTML — the frontend binds it via {@html ...} untouched.

type ReleaseNotesResponse

type ReleaseNotesResponse struct {
	SchemaVersion int    `json:"schema_version"`
	Current       string `json:"current"`
	// LastSeen is the authenticated caller's last acknowledged release
	// version at the moment the request was served. Empty on the
	// /api/release-notes endpoint (caller-agnostic) and on /unseen for
	// a user who has never acked. The frontend uses this to render a
	// "Since your last visit · vA → vB" subtitle in the news popup.
	LastSeen string           `json:"last_seen,omitempty"`
	Notes    []ReleaseNoteDTO `json:"notes"`
}

ReleaseNotesResponse is the envelope returned by GET /api/release-notes and GET /api/release-notes/unseen.

SchemaVersion represents the response-envelope schema. Clients that know about envelope version N can safely ignore notes whose per-note schema_version exceeds their own MAX_SCHEMA (forward-compat; see plan D9).

type RemoteActionMacro added in v0.13.0

type RemoteActionMacro struct {
	ID                    uint   `json:"id"`
	TargetCall            string `json:"target_call"`
	Label                 string `json:"label"`
	ActionName            string `json:"action_name"`
	ArgsString            string `json:"args_string"`
	RemoteOTPCredentialID *uint  `json:"remote_otp_credential_id,omitempty"`
	Position              int    `json:"position"`
	CreatedAt             string `json:"created_at"`
	UpdatedAt             string `json:"updated_at"`
}

RemoteActionMacro is the wire shape of one saved macro.

type RemoteActionMacroReorderRequest added in v0.13.0

type RemoteActionMacroReorderRequest struct {
	TargetCall string `json:"target_call"`
	IDs        []uint `json:"ids"`
}

RemoteActionMacroReorderRequest is the body of POST /api/remote-actions/macros/reorder. The IDs list defines the new order: index 0 -> position 0, etc. Macros for the target that are not in the list are left unchanged.

type RemoteActionMacroRequest added in v0.13.0

type RemoteActionMacroRequest struct {
	TargetCall            string `json:"target_call"`
	Label                 string `json:"label"`
	ActionName            string `json:"action_name"`
	ArgsString            string `json:"args_string"`
	RemoteOTPCredentialID *uint  `json:"remote_otp_credential_id,omitempty"`
	Position              int    `json:"position"`
}

RemoteActionMacroRequest is the create / update body. TargetCall is uppercased on the server; clients may send any case.

Update (PUT) semantics:

  • Label, ActionName: gated — empty string leaves the field unchanged.
  • ArgsString, RemoteOTPCredentialID: always overwrite. Empty string clears args; nil unbinds the credential. Clients must send the full update body.
  • Position: ignored on PUT. The /macros/reorder endpoint is the sole owner of macro ordering.

type RemoteOTPCode added in v0.13.0

type RemoteOTPCode struct {
	Code      string `json:"code"`
	ExpiresAt string `json:"expires_at"`
}

RemoteOTPCode is the response from POST /api/remote-actions/otp/{credId}. ExpiresAt is RFC3339 UTC and marks the inclusive upper edge of the step that produced this code. The drawer uses (ExpiresAt - now) as the countdown source.

type RemoteOTPCredential added in v0.13.0

type RemoteOTPCredential struct {
	ID         uint     `json:"id"`
	Name       string   `json:"name"`
	Algorithm  string   `json:"algorithm"`
	Digits     int      `json:"digits"`
	Period     int      `json:"period"`
	CreatedAt  string   `json:"created_at"`
	LastUsedAt *string  `json:"last_used_at,omitempty"`
	UsedBy     []string `json:"used_by"`
}

RemoteOTPCredential is the safe wire shape for a remote-station TOTP secret. SecretB32 is intentionally absent — it is only echoed back at create time via RemoteOTPCredentialCreated, and never retrievable thereafter.

UsedBy is the list of distinct uppercased target callsigns whose macros reference this credential. The credential cannot be deleted while non-empty (HTTP 409); the UI surfaces "Unbind from N macro(s) first" using its length.

type RemoteOTPCredentialRequest added in v0.13.0

type RemoteOTPCredentialRequest struct {
	Name      string `json:"name"`
	SecretB32 string `json:"secret_b32"`
	Algorithm string `json:"algorithm,omitempty"`
	Digits    int    `json:"digits,omitempty"`
	Period    int    `json:"period,omitempty"`
}

RemoteOTPCredentialRequest is the create / update body. Algorithm, Digits, Period are optional on create; the server fills sha1/6/30.

type SendMessageRequest

type SendMessageRequest struct {
	// To is the addressee: a station callsign for a DM or a tactical
	// label for a group broadcast. Uppercase-normalized server-side.
	To string `json:"to"`
	// Text is the message body (<= 67 APRS chars after validation).
	// Ignored when Kind == "invite" — the server builds the wire body
	// from InviteTactical.
	Text string `json:"text"`
	// PreferIS, when true, routes the outbound via APRS-IS regardless
	// of the current fallback policy.
	PreferIS bool `json:"prefer_is,omitempty"`
	// Path overrides the default RF path from preferences. Empty =
	// use MessagePreferences.DefaultPath.
	Path string `json:"path,omitempty"`
	// Channel overrides the configured TX channel. Nil = use default.
	Channel *uint32 `json:"channel,omitempty"`
	// ClientID is an opaque client-side correlation token. Echoed back
	// unchanged in the response so the optimistic UI can reconcile its
	// local row with the persisted ID.
	ClientID string `json:"client_id,omitempty"`
	// Kind classifies the outbound row. Empty or "text" is a normal
	// DM/tactical message; "invite" makes the sender build a
	// `!GW1 INVITE <InviteTactical>` body and stamp the row with
	// Kind=invite + InviteTactical. The sender (Phase 2) is
	// responsible for honoring this; the DTO just carries it.
	Kind string `json:"kind,omitempty"`
	// InviteTactical is the tactical callsign referenced by an invite.
	// Must be set when Kind == "invite"; ignored otherwise.
	InviteTactical string `json:"invite_tactical,omitempty"`
}

SendMessageRequest is the body accepted by POST /api/messages.

func (SendMessageRequest) Validate

func (r SendMessageRequest) Validate() error

Validate enforces the minimal invariants every compose request must satisfy. Loopback / tactical-vs-DM classification is handler-local because it needs the OurCall context.

type SmartBeaconConfigRequest

type SmartBeaconConfigRequest struct {
	// Enabled is true when SmartBeacon curve computation is active.
	// When false, every beacon with smart_beacon=true falls back to
	// its fixed interval.
	Enabled bool `json:"enabled"`
	// FastSpeedKt is the knots threshold at or above which beacons
	// transmit at FastRateSec. The "moving fast" end of the curve.
	// Must be greater than SlowSpeedKt.
	FastSpeedKt uint32 `json:"fast_speed"`
	// FastRateSec is the beacon interval in seconds at or above
	// FastSpeedKt. Must be shorter than SlowRateSec.
	FastRateSec uint32 `json:"fast_rate"`
	// SlowSpeedKt is the knots threshold at or below which beacons
	// transmit at SlowRateSec. Must be greater than zero to prevent a
	// degenerate middle-branch division by zero inside
	// beacon.SmartBeaconConfig.Interval().
	SlowSpeedKt uint32 `json:"slow_speed"`
	// SlowRateSec is the beacon interval in seconds at or below
	// SlowSpeedKt. Must be longer than FastRateSec.
	SlowRateSec uint32 `json:"slow_rate"`
	// MinTurnDeg is the fixed-component turn angle threshold, in
	// degrees, used in the corner-pegging formula. Must be in
	// [1, 179].
	MinTurnDeg uint32 `json:"min_turn_angle"`
	// TurnSlope is the speed-dependent component (degrees·knots) of
	// the corner-pegging turn threshold. Higher speed → lower
	// effective threshold → corner pegs fire sooner. Must be greater
	// than zero.
	TurnSlope uint32 `json:"turn_slope"`
	// MinTurnSec is the minimum interval in seconds between
	// turn-triggered beacons. Must be greater than zero.
	MinTurnSec uint32 `json:"min_turn_time"`
}

SmartBeaconConfigRequest is the body accepted by PUT /api/smart-beacon.

The wire shape is snake_case and matches the UI mock byte-for-byte. Speeds are in knots, rates are in seconds, the turn angle is in degrees, and the turn slope is in degrees·knots. These field names are the source of truth for the on-the-wire contract; renaming any of them is a breaking change for every consumer of the generated TypeScript client.

func (SmartBeaconConfigRequest) Validate

func (r SmartBeaconConfigRequest) Validate() error

Validate enforces the SmartBeacon parameter constraints documented in the HamHUD/direwolf references. Errors use human-readable field names that mirror the wire tags so 400 responses point the UI at the offending field.

type SmartBeaconConfigResponse

type SmartBeaconConfigResponse struct {
	// Enabled is true when SmartBeacon curve computation is active.
	Enabled bool `json:"enabled"`
	// FastSpeedKt is the knots threshold at or above which beacons
	// transmit at FastRateSec.
	FastSpeedKt uint32 `json:"fast_speed"`
	// FastRateSec is the beacon interval in seconds at or above
	// FastSpeedKt.
	FastRateSec uint32 `json:"fast_rate"`
	// SlowSpeedKt is the knots threshold at or below which beacons
	// transmit at SlowRateSec.
	SlowSpeedKt uint32 `json:"slow_speed"`
	// SlowRateSec is the beacon interval in seconds at or below
	// SlowSpeedKt.
	SlowRateSec uint32 `json:"slow_rate"`
	// MinTurnDeg is the fixed-component turn angle threshold in
	// degrees.
	MinTurnDeg uint32 `json:"min_turn_angle"`
	// TurnSlope is the speed-dependent component (degrees·knots) of
	// the corner-pegging turn threshold.
	TurnSlope uint32 `json:"turn_slope"`
	// MinTurnSec is the minimum interval in seconds between
	// turn-triggered beacons.
	MinTurnSec uint32 `json:"min_turn_time"`
}

SmartBeaconConfigResponse is the body returned by GET/PUT /api/smart-beacon. Shape matches SmartBeaconConfigRequest — the singleton has no id or timestamps exposed on the wire.

func SmartBeaconConfigDefaults

func SmartBeaconConfigDefaults() SmartBeaconConfigResponse

SmartBeaconConfigDefaults returns the response DTO populated from beacon.DefaultSmartBeacon(), which is the single source of truth for SmartBeacon parameter defaults. GET /api/smart-beacon uses this on a fresh install where no singleton row has been written yet.

Unit conversions applied to cross the package boundary:

  • FastSpeed / SlowSpeed (float64 knots) → uint32 knots (rounded).
  • FastRate / SlowRate / TurnTime (time.Duration) → uint32 seconds.
  • TurnAngle / TurnSlope (float64) → uint32 (rounded).

func SmartBeaconConfigFromModel

func SmartBeaconConfigFromModel(m configstore.SmartBeaconConfig) SmartBeaconConfigResponse

SmartBeaconConfigFromModel converts the persisted singleton into the response DTO. Straight field copy — model and DTO share the same units.

type StationAutocomplete

type StationAutocomplete struct {
	Callsign    string `json:"callsign"`
	LastHeard   string `json:"last_heard,omitempty"` // RFC3339, empty for bots / missing
	Source      string `json:"source"`               // "bot" | "cache" | "history" | "cache+history"
	Description string `json:"description,omitempty"`
}

StationAutocomplete is one suggestion in GET /api/stations/autocomplete. Description is only set for "bot" sources; the station cache and history sources leave it empty.

type StationConfigRequest

type StationConfigRequest struct {
	Callsign string `json:"callsign"`
}

StationConfigRequest is the body accepted by PUT /api/station/config. An empty Callsign is permitted and triggers the clear-with-auto-disable flow defined in the centralized station-callsign plan (D7 rule 2): iGate and Digipeater Enabled are flipped to false atomically when they were previously true.

func (StationConfigRequest) Validate

func (r StationConfigRequest) Validate() error

Validate is a no-op. Any non-empty callsign that fails shape validation is normalized (trim + uppercase) at the store boundary via configstore.UpsertStationConfig; completely invalid strings (internal whitespace etc.) are persisted verbatim and filtered at the resolve site. This mirrors other singleton request DTOs.

type StationConfigResponse

type StationConfigResponse struct {
	Callsign string   `json:"callsign"`
	Disabled []string `json:"disabled,omitempty"`
}

StationConfigResponse is the body returned by both GET and PUT on /api/station/config. Disabled is populated only on the PUT path when the clear-with-auto-disable rule fired; on GET it is omitted from the JSON envelope (omitempty).

Disabled values are the canonical feature names "igate" and "digipeater" (lowercase, exactly those strings) — clients can match on them to surface a "these features were disabled because you cleared the station callsign" notice.

type TacticalCallsignRequest

type TacticalCallsignRequest struct {
	Callsign string `json:"callsign"`
	Alias    string `json:"alias,omitempty"`
	Enabled  bool   `json:"enabled"`
}

TacticalCallsignRequest is the body accepted by POST + PUT on /api/messages/tactical.

func (TacticalCallsignRequest) ToModel

ToModel builds a configstore row from the request. Callsign is uppercased by the model's BeforeSave hook; we upper here too so validation and collision checks use the canonical form.

func (TacticalCallsignRequest) Validate

func (r TacticalCallsignRequest) Validate() error

Validate enforces addressee syntax and non-empty callsign. The handler additionally rejects well-known bot labels after this.

type TacticalCallsignResponse

type TacticalCallsignResponse struct {
	ID        uint32    `json:"id"`
	Callsign  string    `json:"callsign"`
	Alias     string    `json:"alias,omitempty"`
	Enabled   bool      `json:"enabled"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

TacticalCallsignResponse is the body returned by GET/POST/PUT.

func TacticalCallsignFromModel

func TacticalCallsignFromModel(m configstore.TacticalCallsign) TacticalCallsignResponse

TacticalCallsignFromModel renders one row.

type TestFireRequest added in v0.13.0

type TestFireRequest struct {
	Args map[string]string `json:"args,omitempty"`
	Text *string           `json:"text,omitempty"`
}

TestFireRequest is the body accepted by POST /api/actions/{id}/test-fire.

Args is used for kv-mode actions; Text is used for freeform-mode actions. The handler rejects requests that mix shapes against the Action's mode.

type TestFireResponse added in v0.13.0

type TestFireResponse struct {
	Status         string   `json:"status"`
	StatusDetail   string   `json:"status_detail,omitempty"`
	OutputCapture  string   `json:"output_capture,omitempty"`
	ReplyText      string   `json:"reply_text"`
	ReplyLines     []string `json:"reply_lines"`
	ReplyLineCount int      `json:"reply_line_count"`
	Truncated      bool     `json:"truncated"`
	ExitCode       *int     `json:"exit_code,omitempty"`
	HTTPStatus     *int     `json:"http_status,omitempty"`
	InvocationID   uint     `json:"invocation_id"`
}

TestFireResponse is the body returned by POST /api/actions/{id}/test-fire.

Truncated mirrors the value the audit row would have stored for a real on-air invocation, so the UI can warn the operator that their reply got chopped to fit the 67-char APRS message cap.

type TestRigctldRequest

type TestRigctldRequest struct {
	Host string `json:"host"`
	Port uint16 `json:"port"`
}

TestRigctldRequest is the body accepted by POST /api/ptt/test-rigctld. The handler opens a short-lived TCP connection to the given rigctld endpoint and sends a non-disruptive `t` (get_ptt) probe.

type TestRigctldResponse

type TestRigctldResponse struct {
	OK        bool   `json:"ok"`
	Message   string `json:"message"`
	LatencyMs int64  `json:"latency_ms"`
}

TestRigctldResponse reports the outcome of a rigctld probe. OK is the single source of truth — clients must not infer success from HTTP status. Message is a human-readable diagnostic; LatencyMs is populated only on success and measures wall-clock from dial start to RPRT 0.

type TestToneResponse

type TestToneResponse struct {
	Status string `json:"status"`
}

TestToneResponse is the body returned by POST /api/audio-devices/{id}/test-tone on success. Preserves the pre-typed `{"status":"ok"}` wire shape.

type ThemeConfigRequest

type ThemeConfigRequest struct {
	ID string `json:"id"`
}

ThemeConfigRequest is the body accepted by PUT /api/preferences/theme. ID must match the kebab-case/lowercase pattern validated by configstore.IsValidTheme. The server does not enforce that the id matches a shipped theme — that's the frontend's responsibility, and keeping it so lets contributors add themes without touching Go.

func (ThemeConfigRequest) Validate

func (r ThemeConfigRequest) Validate() error

type ThemeConfigResponse

type ThemeConfigResponse struct {
	ID string `json:"id"`
}

ThemeConfigResponse is the body returned by GET and PUT on /api/preferences/theme.

type TxCapability

type TxCapability struct {
	Capable bool   `json:"capable"`
	Reason  string `json:"reason,omitempty"`
}

TxCapability reports whether a channel can currently transmit.

Capable is true when the channel has at least one usable TX path:

  • ≥ 1 TNC-mode KissInterface attached (the KISS path short-circuits first, so a KISS-only channel with InputDeviceID == nil still reports Capable=true, not "no input device configured"), OR
  • a modem backing with both InputDeviceID != nil AND OutputDeviceID != 0 (the zero-sentinel on OutputDeviceID marks RX-only modem configs; those cannot TX even though Backing.Summary is "modem").

Reason is a short human-legible explanation of why Capable is false (e.g. "no input device configured", "no output device configured").

Contract: Reason is the empty string if and only if Capable == true. Callers may rely on this invariant — don't overload Reason with a hint when Capable is true, and don't leave Reason empty when Capable is false. The reason strings are stable wire values (consumed by the UI picker's disabled-option secondary text and the server-side 400 body on referrer writes), so treat them as API surface.

type TxTimingRequest

type TxTimingRequest struct {
	Channel   uint32 `json:"channel"`
	TxDelayMs uint32 `json:"tx_delay_ms"`
	TxTailMs  uint32 `json:"tx_tail_ms"`
	SlotMs    uint32 `json:"slot_ms"`
	Persist   uint32 `json:"persist"`
	FullDup   bool   `json:"full_dup"`
	Rate1Min  uint32 `json:"rate_1min"`
	Rate5Min  uint32 `json:"rate_5min"`
}

TxTimingRequest is the body accepted by POST /api/tx-timing and PUT /api/tx-timing/{channel}.

func (TxTimingRequest) ToModel

func (r TxTimingRequest) ToModel() configstore.TxTiming

func (TxTimingRequest) ToUpdate

func (r TxTimingRequest) ToUpdate(channel uint32) configstore.TxTiming

ToUpdate maps an update request to a storage model, pinning the channel from the URL instead of the body.

func (TxTimingRequest) Validate

func (r TxTimingRequest) Validate() error

type TxTimingResponse

type TxTimingResponse struct {
	ID uint32 `json:"id"`
	TxTimingRequest
}

func TxTimingFromModel

func TxTimingFromModel(m configstore.TxTiming) TxTimingResponse

func TxTimingsFromModels

func TxTimingsFromModels(ms []configstore.TxTiming) []TxTimingResponse

type UnitsConfigRequest

type UnitsConfigRequest struct {
	System string `json:"system"`
}

UnitsConfigRequest is the body accepted by PUT /api/preferences/units. System must be "imperial" or "metric".

func (UnitsConfigRequest) Validate

func (r UnitsConfigRequest) Validate() error

Validate rejects anything other than the two recognized systems so a bad payload doesn't reach the store (which would reject it anyway) and the client gets a clean 400 instead of a 500.

type UnitsConfigResponse

type UnitsConfigResponse struct {
	System string `json:"system"`
}

UnitsConfigResponse is the body returned by GET and PUT on /api/preferences/units.

type UpdatesConfigRequest

type UpdatesConfigRequest struct {
	Enabled bool `json:"enabled"`
}

UpdatesConfigRequest is the body accepted by PUT /api/updates/config. Enabled controls whether the daily GitHub update check runs at all. Disabling stops the ticker and causes GET /api/updates/status to report status="disabled".

func (UpdatesConfigRequest) Validate

func (r UpdatesConfigRequest) Validate() error

Validate is a no-op. A single bool field has no input shape that can fail validation; the method exists for symmetry with the other singleton-config request DTOs that implement dto.Validator.

type UpdatesConfigResponse

type UpdatesConfigResponse struct {
	Enabled bool `json:"enabled"`
}

UpdatesConfigResponse is the body returned by GET and PUT on /api/updates/config. Mirrors UpdatesConfigRequest — the stored state is a single bool.

type UpdatesStatusResponse

type UpdatesStatusResponse struct {
	Status    string `json:"status"`
	Current   string `json:"current"`
	Latest    string `json:"latest,omitempty"`
	URL       string `json:"url,omitempty"`
	CheckedAt string `json:"checked_at,omitempty"` // RFC3339, omitted if zero
}

UpdatesStatusResponse is the body returned by GET /api/updates/status. Status is a server-computed enum with exactly four values: "disabled", "pending", "current", "available" (D6). Latest / URL / CheckedAt are omitted from the JSON envelope when empty so a "disabled" or "pending" response collapses to a minimal shape.

type Validator

type Validator interface {
	Validate() error
}

Validator is implemented by request DTOs that can validate themselves. The generic handler helpers require it so bad input turns into a 400 before any store work happens.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL