Pull Proclamations & Categories from API and store them in the database

This commit is contained in:
NeilClack
2026-03-23 17:35:52 -05:00
parent 17c088cc2f
commit 8d23841391
8 changed files with 161 additions and 54 deletions
+7 -7
View File
@@ -20,6 +20,7 @@ import (
"rhemawavetech.com/rhemawave/internal/mixer"
"rhemawavetech.com/rhemawave/internal/page"
"rhemawavetech.com/rhemawave/internal/shaderManager"
"rhemawavetech.com/rhemawave/internal/sync"
)
const (
@@ -27,9 +28,7 @@ const (
volumeStepSize = 16
openGLMajorVersion = 3
openGLMinorVersion = 3
apiURL = "http://localhost:8000"
apiPath = "/api/v1/licenseKey/validate"
audioFileURL = "https://files.neilclack.com/audio.wav"
apiURL = "http://localhost:5173/api/v1"
)
type Application struct {
@@ -56,7 +55,6 @@ func Run() {
var result *gorm.DB
logger.Info("Starting Application")
logger.Info("Finding $HOME")
homeDir, err = os.UserHomeDir()
if err != nil {
@@ -64,7 +62,6 @@ func Run() {
return
}
logger.Info("Constructing DataDir Path")
dataDir = filepath.Join(homeDir, ".local", "share", "rhemawave")
if err := os.MkdirAll(dataDir, 0755); err != nil {
logger.Error("unable to create data directory %s %v", dataDir, err)
@@ -82,8 +79,6 @@ func Run() {
logger.AddFile(filepath.Join(dataDir, "log.txt"))
logger.Info("Connecting to database")
DB, err = db.Connect(filepath.Join(dataDir, "database.sqlite"))
if err != nil {
logger.Error("application failed to connect to the database: %v", err)
@@ -141,6 +136,11 @@ func Run() {
H: h,
RefreshConfig: app.refreshCache,
})
// Sync the proclamations from the API to the Database
sync.SyncProclamations(apiURL, DB)
sync.SyncCategories(apiURL, DB)
app.runEventLoop()
}
+1 -1
View File
@@ -41,7 +41,7 @@ func Connect(dbpath string) (*gorm.DB, error) {
// Migrate the database schema
func Migrate(db *gorm.DB) error {
// Pass all models to be migrated to this function call.
if err := db.AutoMigrate(&AppConfig{}, &Log{}, &Proclamation{}); err != nil {
if err := db.AutoMigrate(&AppConfig{}, &Log{}, &Proclamation{}, &ProclamationCategory{}); err != nil {
return err
}
return nil
+6 -1
View File
@@ -27,7 +27,6 @@ type Log struct {
}
type Proclamation struct {
gorm.Model
ID string `gorm:"primaryKey"`
Name string
Content string
@@ -37,3 +36,9 @@ type Proclamation struct {
Available bool
Price float64
}
type ProclamationCategory struct {
ID int `gorm:"primaryKey"`
Name string
ParentId int
}
-26
View File
@@ -1,26 +0,0 @@
package netreq
import (
"fmt"
"io"
"net/http"
)
// Makes a GET request to the specified URL and returns the JSON response
// as a byte slice. If the request fails, it returns an error.
func GetJSON(url string) ([]byte, error) {
fmt.Println(url)
res, err := http.Get(url)
if err != nil {
return nil, err
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode > 299 {
return nil, fmt.Errorf("request failed with status code %d: %s", res.StatusCode, string(body))
}
if err != nil {
return nil, err
}
return body, nil
}
-19
View File
@@ -1,19 +0,0 @@
package netreq
import (
"fmt"
"testing"
)
func TestGet(t *testing.T) {
url := "http://localhost:5173/api/v1/proclamations"
body, err := GetJSON(url)
if err != nil {
t.Fatalf("GetJSON failed: %v", err)
}
if len(body) == 0 {
t.Fatal("GetJSON returned empty body")
}
cnt := string(body)
fmt.Printf("%s\n", cnt)
}
+54
View File
@@ -0,0 +1,54 @@
package netreq
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// Makes a GET request to the specified URL and returns the JSON response
// as a byte slice. If the request fails, it returns an error.
func GetJSON[T any](url string) ([]T, error) {
fmt.Println("Fetching:", url)
res, err := http.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode > 299 {
return nil, fmt.Errorf("status %d: %s", res.StatusCode, string(body))
}
// 1. Trim whitespace to find the first actual JSON character
trimmed := bytes.TrimSpace(body)
if len(trimmed) == 0 {
return nil, nil
}
var result []T
// 2. Determine if we are looking at an Array [ ] or an Object { }
if trimmed[0] == '[' {
// It's already an array, unmarshal directly
err = json.Unmarshal(body, &result)
} else {
// It's a single object, unmarshal into a single instance and wrap it
var single T
err = json.Unmarshal(body, &single)
result = []T{single}
}
if err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
return result, nil
}
+26
View File
@@ -0,0 +1,26 @@
package netreq
import (
"fmt"
"testing"
"rhemawavetech.com/rhemawave/internal/db"
)
func TestGet(t *testing.T) {
url := "http://localhost:5173/api/v1/proclamations"
// Explicitly tell GetJSON to return a slice of db.Proclamation
proclamations, err := GetJSON[db.Proclamation](url)
if err != nil {
t.Fatalf("GetJSON failed: %v", err)
}
if len(proclamations) == 0 {
t.Fatal("GetJSON returned no records")
}
// You can now inspect the actual struct fields instead of raw strings
fmt.Printf("Fetched %d records. First ID: %s\n", len(proclamations), proclamations[0].ID)
}
+67
View File
@@ -0,0 +1,67 @@
package sync
import (
"fmt"
"regexp"
"gorm.io/gorm"
"rhemawavetech.com/rhemawave/internal/db"
"rhemawavetech.com/rhemawave/internal/logger"
"rhemawavetech.com/rhemawave/internal/netreq"
)
var htmlRegex = regexp.MustCompile(`<[^>]*>`)
// SyncProclamations fetches data from the API, cleans it, and updates the local DB.
// It returns the slice of proclamations so the UI can update immediately.
func SyncProclamations(apiURL string, database *gorm.DB) ([]db.Proclamation, error) {
fullURL := fmt.Sprintf("%s/proclamations", apiURL)
// 1. Fetch from API
proclamations, err := netreq.GetJSON[db.Proclamation](fullURL)
if err != nil {
// If we're offline or API is down, we log it but don't crash.
// The UI can still use whatever is currently in the DB.
logger.Error("Proclamation sync failed, skipping update: %v", err)
return nil, err
}
// 2. Data Transformation (Cleaning)
for i := range proclamations {
proclamations[i].Content = htmlRegex.ReplaceAllString(proclamations[i].Content, "")
}
// 3. Persist to Database
// Using Transaction ensures we don't end up with partial data if something fails
err = database.Transaction(func(tx *gorm.DB) error {
if err := tx.Save(&proclamations).Error; err != nil {
return err
}
return nil
})
if err != nil {
logger.Error("Failed to save synced proclamations: %v", err)
return nil, err
}
logger.Info("Successfully synced proclamations: %d", len(proclamations))
return proclamations, nil
}
func SyncCategories(apiURL string, database *gorm.DB) ([]db.ProclamationCategory, error) {
fullURL := fmt.Sprintf("%s/categories", apiURL)
categories, err := netreq.GetJSON[db.ProclamationCategory](fullURL)
if err != nil {
logger.Error("Category sync failed, skipping update: %v", err)
return nil, err
}
err = database.Transaction(func(tx *gorm.DB) error {
if err := tx.Save(&categories).Error; err != nil {
return err
}
return nil
})
logger.Info("Successfully synced proclamation categories: %d", len(categories))
return categories, nil
}