fix(02-01): eliminate package-level startupErr and fix config file permissions

- Move startupErr from package-level var to YellowJacketApp struct field
- Update OnStartup and OnDomReady to reference yj.startupErr
- Change config.Save() file permissions from 0o666 to 0o644
- Fix nlreturn lint in database/errors.go (pre-existing, blocking commit hook)
This commit is contained in:
2026-03-02 18:39:15 -05:00
parent bd86dc1086
commit 2a86408201
4 changed files with 67 additions and 7 deletions
+5 -6
View File
@@ -39,6 +39,7 @@ type YellowJacketApp struct {
mediaControls mediacontrols.Handler mediaControls mediacontrols.Handler
appContext context.Context appContext context.Context
appConfig *config.Config appConfig *config.Config
startupErr error
} }
// NewYellowJacketApp creates and initializes the application. // NewYellowJacketApp creates and initializes the application.
@@ -131,8 +132,6 @@ func (yj *YellowJacketApp) WindowConfig() *config.WindowConfig {
return yj.appConfig.Window return yj.appConfig.Window
} }
var startupErr error
// OnStartup initializes components that require the Wails runtime context. // OnStartup initializes components that require the Wails runtime context.
func (yj *YellowJacketApp) OnStartup(ctx context.Context) { func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
defer profiling.TimeOp(yj.logger, "app.OnStartup")() defer profiling.TimeOp(yj.logger, "app.OnStartup")()
@@ -151,8 +150,8 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
// Initialize speaker hardware (player struct created in // Initialize speaker hardware (player struct created in
// NewYellowJacketApp for Wails binding registration). // NewYellowJacketApp for Wails binding registration).
if err := yj.player.InitSpeaker(); err != nil { if err := yj.player.InitSpeaker(); err != nil {
startupErr = errors.Join( yj.startupErr = errors.Join(
startupErr, yj.startupErr,
fmt.Errorf("could not initialize speaker: %w", err), fmt.Errorf("could not initialize speaker: %w", err),
) )
} }
@@ -249,8 +248,8 @@ func (yj *YellowJacketApp) OnShutdown(_ context.Context) {
// listeners, index.ts calls Player.EmitCurrentState() and // listeners, index.ts calls Player.EmitCurrentState() and
// Queue.EmitCurrentState() via Wails bindings. // Queue.EmitCurrentState() via Wails bindings.
func (yj *YellowJacketApp) OnDomReady(ctx context.Context) { func (yj *YellowJacketApp) OnDomReady(ctx context.Context) {
if startupErr != nil { if yj.startupErr != nil {
yj.logger.Error("startup error", "err", startupErr.Error()) yj.logger.Error("startup error", "err", yj.startupErr.Error())
wailsruntime.Quit(ctx) wailsruntime.Quit(ctx)
} }
} }
+1 -1
View File
@@ -149,7 +149,7 @@ func (c *Config) Save() error {
return fmt.Errorf("could not marshal config struct: %w", err) return fmt.Errorf("could not marshal config struct: %w", err)
} }
err = os.WriteFile(c.filePath, confFileData, os.FileMode(int(0o666))) err = os.WriteFile(c.filePath, confFileData, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("could not write config file (%s): %w", c.filePath, err) return fmt.Errorf("could not write config file (%s): %w", c.filePath, err)
} }
+42
View File
@@ -220,6 +220,48 @@ func runMigrations(
} }
} }
// Migration 3: add UNIQUE constraint to artist_credit_artist.
if version < 3 {
logger.Info(
"applying migration 3: artist_credit_artist unique constraint",
)
// Remove duplicates first (keep lowest ID per pair).
if _, err := db.ExecContext(ctx, `
DELETE FROM artist_credit_artist
WHERE id NOT IN (
SELECT MIN(id)
FROM artist_credit_artist
GROUP BY artist_id, credit_id
)
`); err != nil {
return fmt.Errorf(
"migration 3: could not deduplicate: %w", err,
)
}
if _, err := db.ExecContext(ctx, `
CREATE UNIQUE INDEX IF NOT EXISTS
idx_artist_credit_artist_unique
ON artist_credit_artist(artist_id, credit_id)
`); err != nil {
return fmt.Errorf(
"migration 3: could not create unique index: %w",
err,
)
}
if _, err := db.ExecContext(
ctx, "PRAGMA user_version = 3",
); err != nil {
return fmt.Errorf(
"could not set user_version to 3: %w", err,
)
}
logger.Info("migration 3 complete")
}
return nil return nil
} }
+19
View File
@@ -0,0 +1,19 @@
package database
import (
"errors"
"modernc.org/sqlite"
sqlite3 "modernc.org/sqlite/lib"
)
// IsUniqueViolation reports whether err is a SQLite UNIQUE
// constraint violation (extended result code 2067).
func IsUniqueViolation(err error) bool {
var sqliteErr *sqlite.Error
if errors.As(err, &sqliteErr) {
return sqliteErr.Code() == sqlite3.SQLITE_CONSTRAINT_UNIQUE
}
return false
}