6 Commits

18 changed files with 396 additions and 100 deletions

View File

@@ -3,6 +3,50 @@
All notable changes to LST will be documented in this file. All notable changes to LST will be documented in this file.
## [0.0.1-alpha.6](https://git.tuffraid.net/cowch/logistics_support_tool/compare/v0.0.1-alpha.5...v0.0.1-alpha.6) (2025-07-31)
### 🌟 Enhancements
* **logging:** added in db and logging with websocket ([52ef39f](https://git.tuffraid.net/cowch/logistics_support_tool/commit/52ef39fd5c129ed02ed9f38dbf7e49ae06807ad6))
* **settings:** migrated all settings endpoints confirmed as well for updates ([0575a34](https://git.tuffraid.net/cowch/logistics_support_tool/commit/0575a344229ba0ff5c0f47781c6d596e5c08e5eb))
* **ws server:** added in a websocket on port system to help with better logging ([5bcbdaf](https://git.tuffraid.net/cowch/logistics_support_tool/commit/5bcbdaf3d0e889729d4dce3df51f4330d7793868))
### 🐛 Bug fixes
* **update server:** fixed to make sure everything is stopped before doing the remaining update ([13e282e](https://git.tuffraid.net/cowch/logistics_support_tool/commit/13e282e815c1c95a0a5298ede2f6497cdf036440))
* **websocket:** errors in saving client info during ping ping ([4368111](https://git.tuffraid.net/cowch/logistics_support_tool/commit/4368111311c48e73a11a6b24febdcc3be31a2a59))
* **wrapper:** corrections to properly handle websockets :D ([a761a36](https://git.tuffraid.net/cowch/logistics_support_tool/commit/a761a3634b6cb0aeeb571dd634bd158cee530779))
### 📚 Documentation
* **.env example:** added postrgres example ([14dd87e](https://git.tuffraid.net/cowch/logistics_support_tool/commit/14dd87e335a63d76d64c07a15cf593cb286a9833))
* **dockerbuild:** comments as a reminder for my seld ([52956ec](https://git.tuffraid.net/cowch/logistics_support_tool/commit/52956ecaa45cd556ba7832d6cb9ec2cf883d983a))
* **docker:** docs about the custom network for the db is seperated ([6a631be](https://git.tuffraid.net/cowch/logistics_support_tool/commit/6a631be909b56a899af393510edffd70d7901a7a))
* **wss:** more ws stuff ([63c053b](https://git.tuffraid.net/cowch/logistics_support_tool/commit/63c053b38ce3ab3c3a94cda620da930f4e8615bd))
### 🛠️ Code Refactor
* **app port:** changed to have the port be dyncamic on the iis side ([074032f](https://git.tuffraid.net/cowch/logistics_support_tool/commit/074032f20dc90810416c5899e44fefe86b52f98a))
* **build:** added back in the build name stuff ([92ce51e](https://git.tuffraid.net/cowch/logistics_support_tool/commit/92ce51eb7cf14ebb599c29fea4721e21badafbf6))
* **config:** changed to settings to match the other lst in node. makes it more easy to manage ([3bc3801](https://git.tuffraid.net/cowch/logistics_support_tool/commit/3bc3801ffbb544a814d52c72e566e8d4866a7f38))
* **createzip:** added in env-example to the zip file ([6c8ac33](https://git.tuffraid.net/cowch/logistics_support_tool/commit/6c8ac33be73f203137b883e33feb625ccc0945e9))
* **docker compose example:** added in postgress stuff plus network ([623e19f](https://git.tuffraid.net/cowch/logistics_support_tool/commit/623e19f028d27fbfc46bee567ce78169cddba8fb))
* **settings:** changed config to settings and added in the update method for this as well ([a0aa75c](https://git.tuffraid.net/cowch/logistics_support_tool/commit/a0aa75c5a0b4a6e3a10b88bbcccf43d096e532b4))
* **wrapper:** removed the logger stuff so we dont fill up space ([8a08d3e](https://git.tuffraid.net/cowch/logistics_support_tool/commit/8a08d3eac6540b00ff23115936d56b4f22f16d53))
* **ws:** ws logging and channel manager added no auth currently ([a1a30cf](https://git.tuffraid.net/cowch/logistics_support_tool/commit/a1a30cffd18e02e1061959fa3164f8237522880c))
### 🚀 Performance
* **websocket:** added in base url to help with ssl stuff and iis ([daf9e8a](https://git.tuffraid.net/cowch/logistics_support_tool/commit/daf9e8a966fd440723b1aec932a02873a5e27eb7))
### 📝 Testing Code
* **iis:** wrapper test for ws ([75c17d2](https://git.tuffraid.net/cowch/logistics_support_tool/commit/75c17d20659dcc5a762e00928709c4d3dd277284))
### 📈 Project changes
* **hotreload:** added in air for hot reloading ([78be07c](https://git.tuffraid.net/cowch/logistics_support_tool/commit/78be07c8bbf5acbcdac65351f693941f47be4cb5))
## [0.0.1-alpha.5](https://git.tuffraid.net/cowch/logistics_support_tool/compare/v0.0.1-alpha.4...v0.0.1-alpha.5) (2025-07-21) ## [0.0.1-alpha.5](https://git.tuffraid.net/cowch/logistics_support_tool/compare/v0.0.1-alpha.4...v0.0.1-alpha.5) (2025-07-21)
### 🌟 Enhancements ### 🌟 Enhancements

View File

@@ -10,3 +10,5 @@ this will also include a primary server to house all the common configs across a
The new lst will run in docker by building your own image and deploying or pulling the image down. The new lst will run in docker by building your own image and deploying or pulling the image down.
you will also be able to run it in windows or linux. you will also be able to run it in windows or linux.
when developing in lst and you want to run hotloads installed and configure https://github.com/air-verse/air

0
backend/.air.toml Normal file
View File

View File

@@ -1,53 +0,0 @@
package config
import (
"github.com/gin-gonic/gin"
"lst.net/utils/db"
logging "lst.net/utils/logger"
)
type ConfigUpdateInput struct {
Description *string `json:"description"`
Value *string `json:"value"`
Enabled *bool `json:"enabled"`
AppService *string `json:"app_service"`
}
func RegisterConfigRoutes(l *gin.Engine, baseUrl string) {
// seed the db on start up
db.SeedConfigs(db.DB)
configGroup := l.Group(baseUrl + "/api/config")
configGroup.GET("/configs", getconfigs)
configGroup.POST("/configs", newConfig)
}
func getconfigs(c *gin.Context) {
log := logging.New()
configs, err := db.GetAllConfigs(db.DB)
log.Info("Current Configs", "system", map[string]interface{}{
"endpoint": "/api/config/configs",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
if err != nil {
c.JSON(500, gin.H{"message": "There was an error getting the configs", "error": err})
}
c.JSON(200, gin.H{"message": "Current configs", "data": configs})
}
func newConfig(c *gin.Context) {
var config ConfigUpdateInput
err := c.ShouldBindBodyWithJSON(&config)
if err != nil {
c.JSON(500, gin.H{"message": "Internal Server Error"})
}
c.JSON(200, gin.H{"message": "New config was just added", "data": config})
}

View File

@@ -1 +1 @@
package system package servers

View File

@@ -0,0 +1,88 @@
package settings
import (
"encoding/json"
"github.com/gin-gonic/gin"
"lst.net/utils/db"
"lst.net/utils/inputs"
logging "lst.net/utils/logger"
)
func RegisterSettingsRoutes(l *gin.Engine, baseUrl string) {
// seed the db on start up
db.SeedConfigs(db.DB)
s := l.Group(baseUrl + "/api/v1")
s.GET("/settings", getSettings)
s.PATCH("/settings/:id", updateSettingById)
}
func getSettings(c *gin.Context) {
logger := logging.New()
configs, err := db.GetAllConfigs(db.DB)
logger.Info("Current Settings", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
if err != nil {
logger.Error("Current Settings", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
"error": err,
})
c.JSON(500, gin.H{"message": "There was an error getting the settings", "error": err})
return
}
c.JSON(200, gin.H{"message": "Current settings", "data": configs})
}
func updateSettingById(c *gin.Context) {
logger := logging.New()
settingID := c.Param("id")
if settingID == "" {
c.JSON(500, gin.H{"message": "Invalid data"})
logger.Error("Invalid data", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
return
}
var setting inputs.SettingUpdateInput
//err := c.ShouldBindBodyWithJSON(&setting)
decoder := json.NewDecoder(c.Request.Body) // more strict and will force us to have correct data
decoder.DisallowUnknownFields()
if err := decoder.Decode(&setting); err != nil {
c.JSON(400, gin.H{"message": "Invalid request body", "error": err.Error()})
logger.Error("Invalid request body", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
"error": err,
})
return
}
if err := db.UpdateConfig(db.DB, settingID, setting); err != nil {
c.JSON(500, gin.H{"message": "Failed to update setting", "error": err.Error()})
logger.Error("Failed to update setting", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
"error": err,
})
return
}
c.JSON(200, gin.H{"message": "Setting was just updated", "data": setting})
}

View File

@@ -1 +0,0 @@
package system

View File

@@ -83,6 +83,7 @@ func CleanupChannels() {
} }
func StartBroadcasting(broadcaster chan logging.Message, channels map[string]*Channel) { func StartBroadcasting(broadcaster chan logging.Message, channels map[string]*Channel) {
logger := logging.New()
go func() { go func() {
for msg := range broadcaster { for msg := range broadcaster {
switch msg.Channel { switch msg.Channel {
@@ -90,7 +91,9 @@ func StartBroadcasting(broadcaster chan logging.Message, channels map[string]*Ch
// Just forward the message - filtering happens in RunChannel() // Just forward the message - filtering happens in RunChannel()
messageBytes, err := json.Marshal(msg) messageBytes, err := json.Marshal(msg)
if err != nil { if err != nil {
log.Printf("Error marshaling message: %v", err) logger.Error("Error marshaling message", "websocket", map[string]interface{}{
"errors": err,
})
continue continue
} }
channels["logServices"].Broadcast <- messageBytes channels["logServices"].Broadcast <- messageBytes
@@ -99,7 +102,9 @@ func StartBroadcasting(broadcaster chan logging.Message, channels map[string]*Ch
// Future labels handling // Future labels handling
messageBytes, err := json.Marshal(msg) messageBytes, err := json.Marshal(msg)
if err != nil { if err != nil {
log.Printf("Error marshaling message: %v", err) logger.Error("Error marshaling message", "websocket", map[string]interface{}{
"errors": err,
})
continue continue
} }
channels["labels"].Broadcast <- messageBytes channels["labels"].Broadcast <- messageBytes

View File

@@ -27,9 +27,10 @@ import (
) )
func LogServices(broadcaster chan logging.Message) { func LogServices(broadcaster chan logging.Message) {
fmt.Println("[LogServices] started - single channel for all logs")
logger := logging.New() logger := logging.New()
logger.Info("[LogServices] started - single channel for all logs", "websocket", map[string]interface{}{})
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
os.Getenv("DB_HOST"), os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"), os.Getenv("DB_PORT"),

View File

@@ -1,6 +1,7 @@
package websocket package websocket
import ( import (
"fmt"
"log" "log"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -9,6 +10,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"lst.net/utils/db" "lst.net/utils/db"
logging "lst.net/utils/logger"
) )
var ( var (
@@ -30,7 +32,8 @@ type Client struct {
ConnectedAt time.Time `json:"connected_at"` ConnectedAt time.Time `json:"connected_at"`
done chan struct{} // For graceful shutdown done chan struct{} // For graceful shutdown
isAlive atomic.Bool isAlive atomic.Bool
//mu sync.Mutex // Protects isAlive if not using atomic lastActive time.Time // Tracks last activity
} }
func (c *Client) SaveToDB() { func (c *Client) SaveToDB() {
@@ -59,7 +62,10 @@ func (c *Client) SaveToDB() {
} }
func (c *Client) MarkDisconnected() { func (c *Client) MarkDisconnected() {
log.Printf("Client %v just lefts us", c.ClientID) logger := logging.New()
clientData := fmt.Sprintf("Client %v just lefts us", c.ClientID)
logger.Info(clientData, "websocket", map[string]interface{}{})
now := time.Now() now := time.Now()
res := db.DB.Model(&db.ClientRecord{}). res := db.DB.Model(&db.ClientRecord{}).
Where("client_id = ?", c.ClientID). Where("client_id = ?", c.ClientID).
@@ -68,10 +74,17 @@ func (c *Client) MarkDisconnected() {
}) })
if res.RowsAffected == 0 { if res.RowsAffected == 0 {
log.Println("⚠️ No rows updated for client_id:", c.ClientID)
logger.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{
"clientID": c.ClientID,
})
} }
if res.Error != nil { if res.Error != nil {
log.Println("❌ Error updating disconnected_at:", res.Error)
logger.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{
"clientID": c.ClientID,
"error": res.Error,
})
} }
} }
@@ -123,6 +136,8 @@ const (
) )
func (c *Client) StartHeartbeat() { func (c *Client) StartHeartbeat() {
logger := logging.New()
log.Println("Started hearbeat")
ticker := time.NewTicker(pingPeriod) ticker := time.NewTicker(pingPeriod)
defer ticker.Stop() defer ticker.Stop()
@@ -140,6 +155,27 @@ func (c *Client) StartHeartbeat() {
return return
} }
now := time.Now()
res := db.DB.Model(&db.ClientRecord{}).
Where("client_id = ?", c.ClientID).
Updates(map[string]interface{}{
"last_heartbeat": &now,
})
if res.RowsAffected == 0 {
logger.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{
"clientID": c.ClientID,
})
}
if res.Error != nil {
logger.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{
"clientID": c.ClientID,
"error": res.Error,
})
}
case <-c.done: case <-c.done:
return return
} }
@@ -155,6 +191,75 @@ func (c *Client) Close() {
} }
} }
func (c *Client) startServerPings() {
ticker := time.NewTicker(60 * time.Second) // Ping every 30s
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
c.Close() // Disconnect if ping fails
return
}
case <-c.done:
return
}
}
}
func (c *Client) markActive() {
c.lastActive = time.Now() // No mutex needed if single-writer
}
func (c *Client) IsActive() bool {
return time.Since(c.lastActive) < 45*time.Second // 1.5x ping interval
}
func (c *Client) updateHeartbeat() {
//fmt.Println("Updating heatbeat")
now := time.Now()
logger := logging.New()
//fmt.Printf("Updating heartbeat for client: %s at %v\n", c.ClientID, now)
//db.DB = db.DB.Debug()
res := db.DB.Model(&db.ClientRecord{}).
Where("client_id = ?", c.ClientID).
Updates(map[string]interface{}{
"last_heartbeat": &now, // Explicit format
})
//fmt.Printf("Executed SQL: %v\n", db.DB.Statement.SQL.String())
if res.RowsAffected == 0 {
logger.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{
"clientID": c.ClientID,
})
}
if res.Error != nil {
logger.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{
"clientID": c.ClientID,
"error": res.Error,
})
}
// 2. Verify DB connection
if db.DB == nil {
logger.Error("DB connection is nil", "websocket", map[string]interface{}{})
return
}
// 3. Test raw SQL execution first
testRes := db.DB.Exec("SELECT 1")
if testRes.Error != nil {
logger.Error("DB ping failed", "websocket", map[string]interface{}{
"error": testRes.Error,
})
return
}
}
// work on this stats later // work on this stats later
// Add to your admin endpoint // Add to your admin endpoint
// type ConnectionStats struct { // type ConnectionStats struct {

View File

@@ -35,22 +35,10 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
} }
//defer conn.Close() //defer conn.Close()
// Set ping handler on the connection
conn.SetPingHandler(func(appData string) error {
log.Println("Received ping:", appData)
conn.SetReadDeadline(time.Now().Add(60 * time.Second)) // Reset read timeout
return nil // Return nil to send pong automatically
})
// Optional: Custom pong handler
conn.SetPongHandler(func(appData string) error {
log.Println("Received pong:", appData)
return nil
})
// Create new client // Create new client
client := &Client{ client := &Client{
Conn: conn, Conn: conn,
APIKey: "exampleAPIKey",
Send: make(chan []byte, 256), // Buffered channel Send: make(chan []byte, 256), // Buffered channel
Channels: make(map[string]bool), Channels: make(map[string]bool),
IPAddress: c.ClientIP(), IPAddress: c.ClientIP(),
@@ -73,8 +61,22 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
// return // return
// } // }
//client.StartHeartbeat() // Set handlers
// Cleanup on disconnect conn.SetPingHandler(func(string) error {
return nil // Auto-responds with pong
})
conn.SetPongHandler(func(string) error {
now := time.Now()
client.markActive() // Track last pong time
client.lastActive = now
client.updateHeartbeat()
return nil
})
// Start server-side ping ticker
go client.startServerPings()
defer func() { defer func() {
// Unregister from all channels // Unregister from all channels
for channelName := range client.Channels { for channelName := range client.Channels {
@@ -96,11 +98,6 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
log.Printf("Client disconnected: %s", client.ClientID) log.Printf("Client disconnected: %s", client.ClientID)
}() }()
client.Conn.SetPingHandler(func(appData string) error {
log.Printf("Custom ping handler for client %s", client.ClientID)
return nil
})
// Send welcome message immediately // Send welcome message immediately
welcomeMsg := map[string]string{ welcomeMsg := map[string]string{
"status": "connected", "status": "connected",

View File

@@ -8,7 +8,7 @@ require (
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/swaggo/swag v1.16.5 github.com/swaggo/swag v1.16.6
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0 gorm.io/gorm v1.30.0
) )

View File

@@ -24,10 +24,10 @@ import (
"github.com/gin-contrib/cors" "github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"lst.net/cmd/services/system/config" "lst.net/cmd/services/system/settings"
"lst.net/cmd/services/websocket" "lst.net/cmd/services/websocket"
_ "lst.net/docs" // _ "lst.net/docs"
"lst.net/utils/db" "lst.net/utils/db"
logging "lst.net/utils/logger" logging "lst.net/utils/logger"
@@ -115,7 +115,7 @@ func main() {
//logging.RegisterLoggerRoutes(r, basePath) //logging.RegisterLoggerRoutes(r, basePath)
websocket.RegisterSocketRoutes(r, basePath) websocket.RegisterSocketRoutes(r, basePath)
config.RegisterConfigRoutes(r, basePath) settings.RegisterSettingsRoutes(r, basePath)
r.Any(basePath+"/api", errorApiLoc) r.Any(basePath+"/api", errorApiLoc)

View File

@@ -2,14 +2,17 @@ package db
import ( import (
"log" "log"
"reflect"
"strings"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"lst.net/utils/inputs"
) )
type Settings struct { type Settings struct {
ConfigID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"` SettingID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;not null"` Name string `gorm:"uniqueIndex;not null"`
Description string `gorm:"type:text"` Description string `gorm:"type:text"`
Value string `gorm:"not null"` Value string `gorm:"not null"`
@@ -21,16 +24,48 @@ type Settings struct {
} }
var seedConfigData = []Settings{ var seedConfigData = []Settings{
{Name: "serverPort", Description: "The port the server will listen on if not running in docker", Value: "4000", Enabled: true}, {Name: "serverPort", Description: "The port the server will listen on if not running in docker", Value: "4000", Enabled: true, AppService: "server"},
{Name: "server", Description: "The server we will use when connecting to the alplaprod sql", Value: "usmcd1vms006", Enabled: true}, {Name: "server", Description: "The server we will use when connecting to the alplaprod sql", Value: "usmcd1vms006", Enabled: true, AppService: "server"},
{Name: "timezone", Value: "America/Chicago", Description: "What time zone is the server in this is used for cronjobs and some other time stuff", AppService: "server", Enabled: true},
{Name: "dbUser", Value: "alplaprod", Description: "What is the db userName", AppService: "server", Enabled: true},
{Name: "dbPass", Value: "b2JlbGl4", Description: "What is the db password", AppService: "server", Enabled: true},
{Name: "tcpPort", Value: "2222", Description: "TCP port for printers to connect send data and the zedra cameras", AppService: "server", Enabled: true},
{Name: "prolinkCheck", Value: "1", Description: "Will prolink be considered to check if matches, maninly used in plants that do not fully utilize prolink + ocp", AppService: "production", Enabled: true},
{Name: "bookin", Value: "1", Description: "do we want to book in after a label is printed", AppService: "ocp", Enabled: true},
{Name: "dbServer", Value: "usmcd1vms036", Description: "What server is the prod db on?", AppService: "server", Enabled: true},
{Name: "printDelay", Value: "90", Description: "How long in seconds between prints", AppService: "ocp", Enabled: true},
{Name: "plantToken", Value: "test3", Description: "What is the plant token", AppService: "server", Enabled: true},
{Name: "dualPrinting", Value: "0", Description: "Dose the plant have 2 machines that go to 1?", AppService: "ocp", Enabled: true},
{Name: "ocmeService", Value: "0", Description: "Is the ocme service enabled. this is gernerally only for Dayton.", AppService: "ocme", Enabled: true},
{Name: "fifoCheck", Value: "45", Description: "How far back do we want to check for fifo default 45, putting 0 will ignore.", AppService: "ocme", Enabled: true},
{Name: "dayCheck", Value: "3", Description: "how many days +/- to check for shipments in alplaprod", AppService: "ocme", Enabled: true},
{Name: "maxLotPerTruck", Value: "3", Description: "How mant lots can we have per truck?", AppService: "ocme", Enabled: true},
{Name: "monitorAddress", Value: "8", Description: "What address is monitored to be limited to the amount of lots that can be added to a truck.", AppService: "ocme", Enabled: true},
{Name: "ocmeCycleCount", Value: "1", Description: "Are we allowing ocme cycle counts?", AppService: "ocme", Enabled: true},
{Name: "devDir", Value: "", Description: "This is the dev dir and strictly only for updating the servers.", AppService: "server", Enabled: true},
{Name: "demandMGTActivated", Value: "0", Description: "Do we allow for new fake edi?", AppService: "logistics", Enabled: true},
{Name: "qualityRequest", Value: "0", Description: "quality request module?", AppService: "quality", Enabled: true},
{Name: "ocpLogsCheck", Value: "4", Description: "How long do we want to allow logs to show that have not been cleared?", AppService: "ocp", Enabled: true},
{Name: "inhouseDelivery", Value: "0", Description: "Are we doing auto inhouse delivery?", AppService: "ocp", Enabled: true},
// dyco settings
{Name: "dycoConnect", Value: "0", Description: "Are we running the dyco system?", AppService: "dycp", Enabled: true},
{Name: "dycoPrint", Value: "0", Description: "Are we using the dyco to get the labels or the rfid?", AppService: "dyco", Enabled: true},
{Name: "strapperCheck", Value: "1", Description: "Are we monitoring the strapper for faults?", AppService: "dyco", Enabled: true},
// ocp
{Name: "ocpActive", Value: `1`, Description: "Are we pritning on demand?", AppService: "ocp", Enabled: true},
{Name: "ocpCycleDelay", Value: `10`, Description: "How long between printer cycles do we want to monitor.", AppService: "ocp", Enabled: true},
{Name: "pNgAddress", Value: `139`, Description: "What is the address for p&g so we can make sure we have the correct fake edi forcast going in.", AppService: "logisitcs", Enabled: true},
{Name: "scannerID", Value: `500`, Description: "What scanner id will we be using for the app", AppService: "logistics", Enabled: true},
{Name: "scannerPort", Value: `50002`, Description: "What port instance will we be using?", AppService: "logistics", Enabled: true},
{Name: "stagingReturnLocations", Value: `30125,31523`, Description: "What are the staging location IDs we will use to select from. seperated by commas", AppService: "logistics", Enabled: true},
} }
func SeedConfigs(db *gorm.DB) error { func SeedConfigs(db *gorm.DB) error {
for _, cfg := range seedConfigData { for _, cfg := range seedConfigData {
var existing Settings var existing Settings
// Try to find config by unique name // Try to find config by unique Name
result := db.Where("name =?", cfg.Name).First(&existing) result := db.Where("Name =?", cfg.Name).First(&existing)
if result.Error != nil { if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound { if result.Error == gorm.ErrRecordNotFound {
@@ -38,7 +73,7 @@ func SeedConfigs(db *gorm.DB) error {
if err := db.Create(&cfg).Error; err != nil { if err := db.Create(&cfg).Error; err != nil {
log.Printf("Failed to seed config %s: %v", cfg.Name, err) log.Printf("Failed to seed config %s: %v", cfg.Name, err)
} }
log.Printf("Seeded new config: %s", cfg.Name) //log.Printf("Seeded new config: %s", cfg.Name)
} else { } else {
// Some other error // Some other error
return result.Error return result.Error
@@ -50,18 +85,83 @@ func SeedConfigs(db *gorm.DB) error {
log.Printf("Failed to update config %s: %v", cfg.Name, err) log.Printf("Failed to update config %s: %v", cfg.Name, err)
return err return err
} }
log.Printf("Updated existing config: %s", cfg.Name) //log.Printf("Updated existing config: %s", cfg.Name)
} }
} }
return nil return nil
} }
func GetAllConfigs(db *gorm.DB) ([]Settings, error) { func GetAllConfigs(db *gorm.DB) ([]map[string]interface{}, error) {
var settings []Settings var settings []Settings
result := db.Find(&settings) result := db.Find(&settings)
return settings, result.Error if result.Error != nil {
return nil, result.Error
}
// Function to convert struct to map with lowercase keys
toLowercase := func(s Settings) map[string]interface{} {
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
data := make(map[string]interface{})
for i := 0; i < t.NumField(); i++ {
field := strings.ToLower(t.Field(i).Name)
data[field] = v.Field(i).Interface()
}
return data
}
// Convert each struct in settings slice to a map with lowercase keys
var lowercaseSettings []map[string]interface{}
for _, setting := range settings {
lowercaseSettings = append(lowercaseSettings, toLowercase(setting))
}
return lowercaseSettings, nil
}
func UpdateConfig(db *gorm.DB, id string, input inputs.SettingUpdateInput) error {
var cfg Settings
if err := db.Where("setting_id =?", id).First(&cfg).Error; err != nil {
return err
}
updates := map[string]interface{}{}
if input.Description != nil {
updates["description"] = *input.Description
}
if input.Value != nil {
updates["value"] = *input.Value
}
if input.Enabled != nil {
updates["enabled"] = *input.Enabled
}
if input.AppService != nil {
updates["app_service"] = *input.AppService
}
if len(updates) == 0 {
return nil // nothing to update
}
return db.Model(&cfg).Updates(updates).Error
}
func DeleteConfig(db *gorm.DB, id uint) error {
// Soft delete by ID
return db.Delete(&Settings{}, id).Error
}
func RestoreConfig(db *gorm.DB, id uint) error {
var cfg Settings
if err := db.Unscoped().First(&cfg, id).Error; err != nil {
return err
}
cfg.DeletedAt = gorm.DeletedAt{}
return db.Unscoped().Save(&cfg).Error
} }

View File

@@ -12,7 +12,7 @@ type ClientRecord struct {
IPAddress string `gorm:"not null"` IPAddress string `gorm:"not null"`
UserAgent string `gorm:"size:255"` UserAgent string `gorm:"size:255"`
ConnectedAt time.Time `gorm:"index"` ConnectedAt time.Time `gorm:"index"`
LastHeartbeat time.Time `gorm:"index"` LastHeartbeat time.Time `gorm:"column:last_heartbeat"`
Channels JSONB `gorm:"type:jsonb"` Channels JSONB `gorm:"type:jsonb"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time

View File

@@ -0,0 +1,8 @@
package inputs
type SettingUpdateInput struct {
Description *string `json:"description"`
Value *string `json:"value"`
Enabled *bool `json:"enabled"`
AppService *string `json:"app_service"`
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "logistics_support_tool", "name": "logistics_support_tool",
"version": "0.0.1-alpha.5", "version": "0.0.1-alpha.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "logistics_support_tool", "name": "logistics_support_tool",
"version": "0.0.1-alpha.5", "version": "0.0.1-alpha.6",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"dotenv": "^17.2.0", "dotenv": "^17.2.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "logistics_support_tool", "name": "logistics_support_tool",
"version": "0.0.1-alpha.5", "version": "0.0.1-alpha.6",
"description": "This is the new logisitcs support tool", "description": "This is the new logisitcs support tool",
"private": true, "private": true,
"main": "index.js", "main": "index.js",