This commit is contained in:
2026-06-15 23:45:14 +02:00
parent 29fd832bcd
commit 22e3bb4a18
32 changed files with 2531 additions and 362 deletions
+41 -2
View File
@@ -11,10 +11,23 @@ package profile
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
)
// ProfileDB is a profile's logbook database target. Backend "" or "sqlite" =
// the local SQLite file; "mysql" = the shared MySQL server described by the
// rest. Switching the active profile switches the live logbook to this.
type ProfileDB struct {
Backend string `json:"backend"`
Host string `json:"host"`
Port int `json:"port"`
User string `json:"user"`
Password string `json:"password"`
Database string `json:"database"`
}
// Profile is one operating configuration. A user typically keeps a few:
// "Home", "Portable", "SOTA Pic du Midi", "/MM cruise"…
type Profile struct {
@@ -43,6 +56,7 @@ type Profile struct {
TxPower *float64 `json:"tx_pwr,omitempty"`
IsActive bool `json:"is_active"`
SortOrder int `json:"sort_order"`
DB ProfileDB `json:"db"` // per-profile logbook target (empty backend = local SQLite)
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -56,7 +70,7 @@ func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} }
const selectCols = `id, name, callsign, operator, op_name, owner_callsign, my_grid, my_country, my_state, my_cnty,
my_street, my_city, my_postal_code, my_sota_ref, my_pota_ref,
my_rig, my_antenna, my_dxcc, my_cqz, my_ituz, my_lat, my_lon, tx_pwr,
is_active, sort_order, created_at, updated_at`
is_active, sort_order, db_config, created_at, updated_at`
// List returns every profile, active first then by sort_order/id.
func (r *Repo) List(ctx context.Context) ([]Profile, error) {
@@ -136,6 +150,20 @@ func (r *Repo) Save(ctx context.Context, p *Profile) error {
return err
}
// SetDB updates only a profile's logbook database target, leaving the rest of
// the profile untouched (the general Save deliberately doesn't write db_config).
func (r *Repo) SetDB(ctx context.Context, id int64, cfg ProfileDB) error {
b, err := json.Marshal(cfg)
if err != nil {
return err
}
now := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
_, err = r.db.ExecContext(ctx,
`UPDATE station_profiles SET db_config = ?, updated_at = ? WHERE id = ?`,
string(b), now, id)
return err
}
// SetActive atomically switches the active profile. Clears the flag on all
// rows first to keep the "only one active" invariant from the schema doc.
func (r *Repo) SetActive(ctx context.Context, id int64) error {
@@ -202,6 +230,7 @@ func (r *Repo) Duplicate(ctx context.Context, srcID int64, newName string) (Prof
if err != nil {
return Profile{}, err
}
dbCfg := src.DB // Save deliberately doesn't write db_config — copy it after.
src.ID = 0
src.Name = newName
src.IsActive = false
@@ -209,6 +238,12 @@ func (r *Repo) Duplicate(ctx context.Context, srcID int64, newName string) (Prof
if err := r.Save(ctx, &src); err != nil {
return Profile{}, err
}
if dbCfg.Backend != "" {
if err := r.SetDB(ctx, src.ID, dbCfg); err != nil {
return Profile{}, err
}
src.DB = dbCfg
}
return src, nil
}
@@ -227,15 +262,19 @@ func scan(row scannable) (Profile, error) {
myDXCC, myCQZ, myITUZ sql.NullInt64
myLat, myLon, txPwr sql.NullFloat64
isActive, sortOrder int
dbConfig sql.NullString
createdAt, updatedAt string
)
err := row.Scan(&p.ID, &p.Name, &callsign, &operator, &opName, &ownerCall, &myGrid, &myCountry, &myState, &myCnty,
&myStreet, &myCity, &myPostal, &mySOTA, &myPOTA,
&myRig, &myAntenna, &myDXCC, &myCQZ, &myITUZ, &myLat, &myLon, &txPwr,
&isActive, &sortOrder, &createdAt, &updatedAt)
&isActive, &sortOrder, &dbConfig, &createdAt, &updatedAt)
if err != nil {
return p, err
}
if s := dbConfig.String; s != "" {
_ = json.Unmarshal([]byte(s), &p.DB)
}
p.Callsign = callsign.String
p.Operator = operator.String
p.OpName = opName.String