15 Commits

Author SHA1 Message Date
c7af1901aa feat(app state): settings are now global and get updated on the fly 2025-08-05 12:40:34 -05:00
2473bfa702 feat(app): changes to dyanmically load systems based on settings 2025-08-05 12:40:11 -05:00
4dd842b3b8 docs(.env-example): updated to include discord info 2025-08-04 21:25:39 -05:00
89ef04cc6f feat(discord): added in a way to get panic messages that would crash the server only or fatal 2025-08-04 21:23:06 -05:00
3cec883356 Merge branch 'main' of https://git.tuffraid.net/cowch/logistics_support_tool 2025-08-04 06:54:26 -05:00
0ecbe29ec1 refactor(app): moved all db and log to one intialize spot 2025-08-04 06:54:21 -05:00
188331c1ad ci(backend): fixed go.sum to have all correct pkg 2025-08-03 09:22:57 -05:00
486e4fb6b8 test(ws): testing ws connection on the frontend 2025-08-01 15:54:52 -05:00
c775bb3354 refactor(correction to folder sturcture): before we got to deep resturctures to best pactice folder 2025-08-01 15:54:20 -05:00
b6968b7b67 chore(release): v0.0.1-alpha.6 2025-07-30 19:39:25 -05:00
a0aa75c5a0 refactor(settings): changed config to settings and added in the update method for this as well
strict fields on the updates so we can only change what we want in here
2025-07-30 19:35:13 -05:00
78be07c8bb ci(hotreload): added in air for hot reloading 2025-07-30 19:31:02 -05:00
0575a34422 feat(settings): migrated all settings endpoints confirmed as well for updates 2025-07-29 20:27:45 -05:00
3bc3801ffb refactor(config): changed to settings to match the other lst in node. makes it more easy to manage 2025-07-29 20:13:35 -05:00
4368111311 fix(websocket): errors in saving client info during ping ping 2025-07-29 20:13:05 -05:00
47 changed files with 1349 additions and 814 deletions

View File

@@ -7,6 +7,10 @@ VITE_SERVER_PORT=4000
# lstv2 loc # lstv2 loc
LSTV2="C\drive\loc" LSTV2="C\drive\loc"
# discord - this us used to monitor the logs and make sure we never have a critial shut down.
# this will be for other critical stuff like nice label and some other events to make sure we are still in a good spot and dont need to jump in
WEBHOOK=
# dev stuff below # dev stuff below
# Gitea Info # Gitea Info

1
.gitignore vendored
View File

@@ -194,3 +194,4 @@ scripts/resetDanger.js
LstWrapper/Program_vite_as_Static.txt LstWrapper/Program_vite_as_Static.txt
LstWrapper/Program_proxy_backend.txt LstWrapper/Program_proxy_backend.txt
scripts/stopPool.go scripts/stopPool.go
backend_bad_practice

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,111 +0,0 @@
package loggingx
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"lst.net/utils/db"
)
type CustomLogger struct {
consoleLogger zerolog.Logger
}
// New creates a configured CustomLogger.
func New() *CustomLogger {
// Colorized console output
consoleWriter := zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: "2006-01-02 15:04:05",
}
return &CustomLogger{
consoleLogger: zerolog.New(consoleWriter).
With().
Timestamp().
Logger(),
}
}
func PrettyFormat(level, message string, metadata map[string]interface{}) string {
timestamp := time.Now().Format("2006-01-02 15:04:05")
base := fmt.Sprintf("[%s] %s| Message: %s", strings.ToUpper(level), timestamp, message)
if len(metadata) > 0 {
metaJSON, _ := json.Marshal(metadata)
return fmt.Sprintf("%s | Metadata: %s", base, string(metaJSON))
}
return base
}
func (l *CustomLogger) logToPostgres(level, message, service string, metadata map[string]interface{}) {
err := db.CreateLog(level, message, service, metadata)
if err != nil {
// Fallback to console if DB fails
log.Error().Err(err).Msg("Failed to write log to PostgreSQL")
}
}
// --- Level-Specific Methods ---
func (l *CustomLogger) Info(message, service string, fields map[string]interface{}) {
l.consoleLogger.Info().Fields(fields).Msg(message)
l.logToPostgres("info", message, service, fields)
PostLog(PrettyFormat("info", message, fields)) // Broadcast pretty message
}
func (l *CustomLogger) Warn(message, service string, fields map[string]interface{}) {
l.consoleLogger.Error().Fields(fields).Msg(message)
l.logToPostgres("warn", message, service, fields)
PostLog(PrettyFormat("warn", message, fields)) // Broadcast pretty message
// Custom logic for errors (e.g., alerting)
if len(fields) > 0 {
l.consoleLogger.Warn().Msg("Additional error context captured")
}
}
func (l *CustomLogger) Error(message, service string, fields map[string]interface{}) {
l.consoleLogger.Error().Fields(fields).Msg(message)
l.logToPostgres("error", message, service, fields)
PostLog(PrettyFormat("error", message, fields)) // Broadcast pretty message
// Custom logic for errors (e.g., alerting)
if len(fields) > 0 {
l.consoleLogger.Warn().Msg("Additional error context captured")
}
}
func (l *CustomLogger) Panic(message, service string, fields map[string]interface{}) {
// Log to console (colored, with fields)
l.consoleLogger.Error().
Str("service", service).
Fields(fields).
Msg(message + " (PANIC)") // Explicitly mark as panic
// Log to PostgreSQL (sync to ensure it's saved before crashing)
err := db.CreateLog("panic", message, service, fields) // isCritical=true
if err != nil {
l.consoleLogger.Error().Err(err).Msg("Failed to save panic log to PostgreSQL")
}
// Additional context (optional)
if len(fields) > 0 {
l.consoleLogger.Warn().Msg("Additional panic context captured")
}
panic(message)
}
func (l *CustomLogger) Debug(message, service string, fields map[string]interface{}) {
l.consoleLogger.Debug().Fields(fields).Msg(message)
l.logToPostgres("debug", message, service, fields)
}

View File

@@ -1,12 +0,0 @@
package loggingx
import (
"github.com/gin-gonic/gin"
)
func RegisterLoggerRoutes(l *gin.Engine, baseUrl string) {
configGroup := l.Group(baseUrl + "/api/logger")
configGroup.GET("/logs", GetLogs)
}

View File

@@ -1,148 +0,0 @@
package loggingx
import (
"fmt"
"log"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var (
logChannel = make(chan string, 1000) // Buffered channel for new logs
wsClients = make(map[*websocket.Conn]bool)
wsClientsMux sync.Mutex
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
//fmt.Println("Origin:", r.Header.Get("Origin"))
return true
},
}
// PostLog sends a new log to all connected SSE clients
func PostLog(message string) {
// Send to SSE channel
select {
case logChannel <- message:
log.Printf("Published to SSE: %s", message)
default:
log.Printf("DROPPED SSE message (channel full): %s", message)
}
wsClientsMux.Lock()
defer wsClientsMux.Unlock()
for client := range wsClients {
err := client.WriteMessage(websocket.TextMessage, []byte(message))
if err != nil {
client.Close()
delete(wsClients, client)
}
}
}
func GetLogs(c *gin.Context) {
// Check if it's a WebSocket request
if websocket.IsWebSocketUpgrade(c.Request) {
handleWebSocket(c)
return
}
// Otherwise, handle as SSE
handleSSE(c)
}
func handleSSE(c *gin.Context) {
log := New()
log.Info("SSE connection established", "logger", map[string]interface{}{
"endpoint": "/api/logger/logs",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Headers", "Content-Type")
c.Header("Access-Control-Allow-Methods", "GET, OPTIONS")
// Handle preflight requests
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
flusher, ok := c.Writer.(http.Flusher)
if !ok {
log.Info("SSE not supported", "logger", nil)
c.AbortWithStatus(http.StatusInternalServerError)
return
}
notify := c.Writer.CloseNotify()
for {
select {
case <-notify:
log.Info("SSE client disconnected", "logger", nil)
return
case message := <-logChannel:
fmt.Fprintf(c.Writer, "data: %s\n\n", message)
flusher.Flush()
}
}
}
func handleWebSocket(c *gin.Context) {
log := New()
log.Info("WebSocket connection established", "logger", map[string]interface{}{
"endpoint": "/api/logger/logs",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Error("WebSocket upgrade failed", "logger", map[string]interface{}{
"error": err.Error(),
})
return
}
// Register client
wsClientsMux.Lock()
wsClients[conn] = true
wsClientsMux.Unlock()
defer func() {
wsClientsMux.Lock()
delete(wsClients, conn)
wsClientsMux.Unlock()
conn.Close()
log.Info("WebSocket client disconnected", "logger", map[string]interface{}{})
}()
// Keep connection alive (or optionally echo, or wait for pings)
for {
// Can just read to keep the connection alive
if _, _, err := conn.NextReader(); err != nil {
break
}
}
}
// func sendRecentLogs(conn *websocket.Conn) {
// // Implement your logic to get recent logs from DB or buffer
// recentLogs := getLast20Logs()
// for _, log := range recentLogs {
// conn.WriteMessage(websocket.TextMessage, []byte(log))
// }
// }

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 +0,0 @@
package system

View File

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

View File

@@ -1,24 +0,0 @@
package websocket
import logging "lst.net/utils/logger"
func LabelProcessor(broadcaster chan logging.Message) {
// Initialize any label-specific listeners
// This could listen to a different PG channel or process differently
// for {
// select {
// // Implementation depends on your label data source
// // Example:
// case labelEvent := <-someLabelChannel:
// broadcaster <- logging.Message{
// Channel: "labels",
// Data: labelEvent.Data,
// Meta: map[string]interface{}{
// "label": labelEvent.Label,
// "type": labelEvent.Type,
// },
// }
// }
// }
}

View File

@@ -1,168 +0,0 @@
package websocket
import (
"log"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"lst.net/utils/db"
)
var (
clients = make(map[*Client]bool)
clientsMu sync.RWMutex
)
type Client struct {
ClientID uuid.UUID `json:"client_id"`
Conn *websocket.Conn `json:"-"` // Excluded from JSON
APIKey string `json:"api_key"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Send chan []byte `json:"-"` // Excluded from JSON
Channels map[string]bool `json:"channels"`
LogLevels []string `json:"levels,omitempty"`
Services []string `json:"services,omitempty"`
Labels []string `json:"labels,omitempty"`
ConnectedAt time.Time `json:"connected_at"`
done chan struct{} // For graceful shutdown
isAlive atomic.Bool
//mu sync.Mutex // Protects isAlive if not using atomic
}
func (c *Client) SaveToDB() {
// Convert c.Channels (map[string]bool) to map[string]interface{} for JSONB
channels := make(map[string]interface{})
for ch := range c.Channels {
channels[ch] = true
}
clientRecord := &db.ClientRecord{
APIKey: c.APIKey,
IPAddress: c.IPAddress,
UserAgent: c.UserAgent,
Channels: db.JSONB(channels),
ConnectedAt: time.Now(),
LastHeartbeat: time.Now(),
}
if err := db.DB.Create(&clientRecord).Error; err != nil {
log.Println("❌ Error saving client:", err)
} else {
c.ClientID = clientRecord.ClientID
c.ConnectedAt = clientRecord.ConnectedAt
}
}
func (c *Client) MarkDisconnected() {
log.Printf("Client %v just lefts us", c.ClientID)
now := time.Now()
res := db.DB.Model(&db.ClientRecord{}).
Where("client_id = ?", c.ClientID).
Updates(map[string]interface{}{
"disconnected_at": &now,
})
if res.RowsAffected == 0 {
log.Println("⚠️ No rows updated for client_id:", c.ClientID)
}
if res.Error != nil {
log.Println("❌ Error updating disconnected_at:", res.Error)
}
}
func (c *Client) SafeClient() *Client {
return &Client{
ClientID: c.ClientID,
APIKey: c.APIKey,
IPAddress: c.IPAddress,
UserAgent: c.UserAgent,
Channels: c.Channels,
LogLevels: c.LogLevels,
Services: c.Services,
Labels: c.Labels,
ConnectedAt: c.ConnectedAt,
}
}
// GetAllClients returns safe representations of all clients
func GetAllClients() []*Client {
clientsMu.RLock()
defer clientsMu.RUnlock()
var clientList []*Client
for client := range clients {
clientList = append(clientList, client.SafeClient())
}
return clientList
}
// GetClientsByChannel returns clients in a specific channel
func GetClientsByChannel(channel string) []*Client {
clientsMu.RLock()
defer clientsMu.RUnlock()
var channelClients []*Client
for client := range clients {
if client.Channels[channel] {
channelClients = append(channelClients, client.SafeClient())
}
}
return channelClients
}
// heat beat stuff
const (
pingPeriod = 30 * time.Second
pongWait = 60 * time.Second
writeWait = 10 * time.Second
)
func (c *Client) StartHeartbeat() {
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if !c.isAlive.Load() { // Correct way to read atomic.Bool
return
}
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Printf("Heartbeat failed for %s: %v", c.ClientID, err)
c.Close()
return
}
case <-c.done:
return
}
}
}
func (c *Client) Close() {
if c.isAlive.CompareAndSwap(true, false) { // Atomic swap
close(c.done)
c.Conn.Close()
// Add any other cleanup here
c.MarkDisconnected()
}
}
// work on this stats later
// Add to your admin endpoint
// type ConnectionStats struct {
// TotalConnections int `json:"total_connections"`
// ActiveConnections int `json:"active_connections"`
// AvgDuration string `json:"avg_duration"`
// }
// func GetConnectionStats() ConnectionStats {
// // Implement your metrics tracking
// }

View File

@@ -3,44 +3,38 @@ module lst.net
go 1.24.3 go 1.24.3
require ( require (
github.com/bensch777/discord-webhook-golang v0.0.6
github.com/gin-contrib/cors v1.7.6 github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1 github.com/gin-gonic/gin v1.10.1
github.com/google/uuid v1.6.0
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/lib/pq v1.10.9
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/swaggo/swag v1.16.5
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.1
) )
require ( require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9
github.com/mailru/easyjson v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -48,14 +42,12 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.19.0 // indirect golang.org/x/arch v0.18.0 // indirect
golang.org/x/crypto v0.40.0 // indirect golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.41.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.35.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -6,12 +6,11 @@ import (
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
"lst.net/internal/models"
) )
var DB *gorm.DB var DB *gorm.DB
type JSONB map[string]interface{}
type DBConfig struct { type DBConfig struct {
DB *gorm.DB DB *gorm.DB
DSN string DSN string
@@ -37,7 +36,8 @@ func InitDB() (*DBConfig, error) {
// ensures we have the uuid stuff setup properly // ensures we have the uuid stuff setup properly
DB.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`) DB.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`)
err = DB.AutoMigrate(&Log{}, &Settings{}, &ClientRecord{}) err = DB.AutoMigrate(&models.Log{}, &models.Settings{}) // &ClientRecord{}, &Servers{}
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to auto-migrate models: %v", err) return nil, fmt.Errorf("failed to auto-migrate models: %v", err)
} }

View File

@@ -0,0 +1,21 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
"lst.net/pkg"
)
type Log struct {
LogID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
Level string `gorm:"size:10;not null"` // "info", "error", etc.
Message string `gorm:"not null"`
Service string `gorm:"size:50"`
Metadata pkg.JSONB `gorm:"type:jsonb"` // fields (e.g., {"user_id": 123})
CreatedAt time.Time `gorm:"index"`
Checked bool `gorm:"type:boolean;default:false"`
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}

View File

@@ -0,0 +1,32 @@
package models
import (
"time"
"github.com/google/uuid"
"lst.net/pkg"
)
type Servers struct {
ServerID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
ServerName string `gorm:"size:50;not null"`
ServerDNS string `gorm:"size:25;not null"`
PlantToken string `gorm:"size:10;not null"`
IPAddress string `gorm:"size:16;not null"`
GreatPlainsPlantCode int `gorm:"size:10;not null"`
StreetAddress string `gorm:"size:255;not null"`
CityState string `gorm:"size:50;not null"`
Zipcode int `gorm:"size:13;not null"`
ContactEmail string `gorm:"size:255"`
ContactPhone string `gorm:"size:255"`
CustomerTiAcc string `gorm:"size:255"`
LstServerPort int `gorm:"size:255; not null"`
Active bool `gorm:"type:boolean;default:true"`
LerverLoc string `gorm:"size:255:not null"`
LastUpdated time.Time `gorm:"index"`
ShippingHours pkg.JSONB `gorm:"type:jsonb;default:'[{\"early\": \"06:30\", \"late\": \"23:00\"}]'"`
TiPostTime pkg.JSONB `gorm:"type:jsonb;default:'[{\"from\": \"24\", \"to\": \"24\"}]'"`
OtherSettings pkg.JSONB `gorm:"type:jsonb;default:'[{\"specialInstructions\": \"something for ti\", \"active\": false}]'"`
IsUpgrading bool `gorm:"type:boolean;default:true"`
AlplaProdApiKey string `gorm:"size:255"`
}

View File

@@ -0,0 +1,20 @@
package models
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Settings struct {
SettingID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;not null"`
Description string `gorm:"type:text"`
Value string `gorm:"not null"`
Enabled bool `gorm:"default:true"`
AppService string `gorm:"default:system"`
CreatedAt time.Time `gorm:"index"`
UpdatedAt time.Time `gorm:"index"`
DeletedAt gorm.DeletedAt `gorm:"index"`
}

View File

@@ -1,9 +1,10 @@
package db package models
import ( import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"lst.net/pkg"
) )
type ClientRecord struct { type ClientRecord struct {
@@ -12,8 +13,8 @@ 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 pkg.JSONB `gorm:"type:jsonb"`
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DisconnectedAt *time.Time `gorm:"column:disconnected_at"` DisconnectedAt *time.Time `gorm:"column:disconnected_at"`

View File

@@ -1,4 +1,4 @@
package websocket package ws
import ( import (
"encoding/json" "encoding/json"
@@ -6,7 +6,7 @@ import (
"strings" "strings"
"sync" "sync"
logging "lst.net/utils/logger" "lst.net/pkg/logger"
) )
type Channel struct { type Channel struct {
@@ -82,7 +82,8 @@ func CleanupChannels() {
channels = make(map[string]*Channel) channels = make(map[string]*Channel)
} }
func StartBroadcasting(broadcaster chan logging.Message, channels map[string]*Channel) { func StartBroadcasting(broadcaster chan logger.Message, channels map[string]*Channel) {
logger := logger.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
@@ -142,7 +147,7 @@ func (ch *Channel) RunChannel() {
ch.lock.Unlock() ch.lock.Unlock()
case message := <-ch.Broadcast: case message := <-ch.Broadcast:
var msg logging.Message var msg logger.Message
if err := json.Unmarshal(message, &msg); err != nil { if err := json.Unmarshal(message, &msg); err != nil {
continue continue
} }

View File

@@ -0,0 +1,292 @@
package ws
import (
"fmt"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"lst.net/internal/models"
"lst.net/pkg"
"lst.net/pkg/logger"
)
var (
clients = make(map[*Client]bool)
clientsMu sync.RWMutex
)
type Client struct {
ClientID uuid.UUID `json:"client_id"`
Conn *websocket.Conn `json:"-"` // Excluded from JSON
APIKey string `json:"api_key"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Send chan []byte `json:"-"` // Excluded from JSON
Channels map[string]bool `json:"channels"`
LogLevels []string `json:"levels,omitempty"`
Services []string `json:"services,omitempty"`
Labels []string `json:"labels,omitempty"`
ConnectedAt time.Time `json:"connected_at"`
done chan struct{} // For graceful shutdown
isAlive atomic.Bool
lastActive time.Time // Tracks last activity
}
func (c *Client) SaveToDB(log *logger.CustomLogger, db *gorm.DB) {
// Convert c.Channels (map[string]bool) to map[string]interface{} for JSONB
channels := make(map[string]interface{})
for ch := range c.Channels {
channels[ch] = true
}
clientRecord := &models.ClientRecord{
APIKey: c.APIKey,
IPAddress: c.IPAddress,
UserAgent: c.UserAgent,
Channels: pkg.JSONB(channels),
ConnectedAt: time.Now(),
LastHeartbeat: time.Now(),
}
if err := db.Create(&clientRecord).Error; err != nil {
log.Error("❌ Error saving client", "websocket", map[string]interface{}{
"error": err,
})
} else {
c.ClientID = clientRecord.ClientID
c.ConnectedAt = clientRecord.ConnectedAt
clientData := fmt.Sprintf("A new client %v, just connected", c.ClientID)
log.Info(clientData, "websocket", map[string]interface{}{})
}
}
func (c *Client) MarkDisconnected(log *logger.CustomLogger, db *gorm.DB) {
clientData := fmt.Sprintf("Client %v Dicconected", c.ClientID)
log.Info(clientData, "websocket", map[string]interface{}{})
now := time.Now()
res := db.Model(&models.ClientRecord{}).
Where("client_id = ?", c.ClientID).
Updates(map[string]interface{}{
"disconnected_at": &now,
})
if res.RowsAffected == 0 {
log.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{
"clientID": c.ClientID,
})
}
if res.Error != nil {
log.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{
"clientID": c.ClientID,
"error": res.Error,
})
}
}
func (c *Client) SafeClient() *Client {
return &Client{
ClientID: c.ClientID,
APIKey: c.APIKey,
IPAddress: c.IPAddress,
UserAgent: c.UserAgent,
Channels: c.Channels,
LogLevels: c.LogLevels,
Services: c.Services,
Labels: c.Labels,
ConnectedAt: c.ConnectedAt,
}
}
// GetAllClients returns safe representations of all clients
func GetAllClients() []*Client {
clientsMu.RLock()
defer clientsMu.RUnlock()
var clientList []*Client
for client := range clients {
clientList = append(clientList, client.SafeClient())
}
return clientList
}
// GetClientsByChannel returns clients in a specific channel
func GetClientsByChannel(channel string) []*Client {
clientsMu.RLock()
defer clientsMu.RUnlock()
var channelClients []*Client
for client := range clients {
if client.Channels[channel] {
channelClients = append(channelClients, client.SafeClient())
}
}
return channelClients
}
// heat beat stuff
const (
pingPeriod = 30 * time.Second
pongWait = 60 * time.Second
writeWait = 10 * time.Second
)
func (c *Client) StartHeartbeat(log *logger.CustomLogger, db *gorm.DB) {
log.Debug("Started hearbeat", "websocket", map[string]interface{}{})
ticker := time.NewTicker(pingPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if !c.isAlive.Load() {
return
}
c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Error("Heartbeat failed", "websocket", map[string]interface{}{
"client_id": c.ClientID,
"error": err,
})
c.Close(log, db)
return
}
now := time.Now()
res := db.Model(&models.ClientRecord{}).
Where("client_id = ?", c.ClientID).
Updates(map[string]interface{}{
"last_heartbeat": &now,
})
if res.RowsAffected == 0 {
log.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{
"clientID": c.ClientID,
})
}
if res.Error != nil {
log.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{
"clientID": c.ClientID,
"error": res.Error,
})
}
clientStuff := fmt.Sprintf("HeartBeat just done on: %v", c.ClientID)
log.Info(clientStuff, "websocket", map[string]interface{}{
"clientID": c.ClientID,
})
case <-c.done:
return
}
}
}
func (c *Client) Close(log *logger.CustomLogger, db *gorm.DB) {
if c.isAlive.CompareAndSwap(true, false) { // Atomic swap
close(c.done)
c.Conn.Close()
// Add any other cleanup here
c.MarkDisconnected(log, db)
}
}
func (c *Client) startServerPings(log *logger.CustomLogger, db *gorm.DB) {
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 {
log.Error("Server Ping failed", "websocket", map[string]interface{}{
"clientID": c.ClientID,
"error": err,
})
c.Close(log, db)
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(log *logger.CustomLogger, db *gorm.DB) {
//fmt.Println("Updating heatbeat")
now := time.Now()
//fmt.Printf("Updating heartbeat for client: %s at %v\n", c.ClientID, now)
//db.DB = db.DB.Debug()
res := db.Model(&models.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 {
log.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{
"clientID": c.ClientID,
})
}
if res.Error != nil {
log.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{
"clientID": c.ClientID,
"error": res.Error,
})
}
// 2. Verify DB connection
if db == nil {
log.Error("DB connection is nil", "websocket", map[string]interface{}{})
return
}
// 3. Test raw SQL execution first
testRes := db.Exec("SELECT 1")
if testRes.Error != nil {
log.Error("DB ping failed", "websocket", map[string]interface{}{
"error": testRes.Error,
})
return
}
}
// work on this stats later
// Add to your admin endpoint
// type ConnectionStats struct {
// TotalConnections int `json:"total_connections"`
// ActiveConnections int `json:"active_connections"`
// AvgDuration string `json:"avg_duration"`
// }
// func GetConnectionStats() ConnectionStats {
// // Implement your metrics tracking
// }

View File

@@ -1,13 +1,14 @@
package websocket package ws
import ( import (
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"gorm.io/gorm"
"lst.net/pkg/logger"
) )
type JoinPayload struct { type JoinPayload struct {
@@ -26,31 +27,19 @@ var upgrader = websocket.Upgrader{
EnableCompression: true, EnableCompression: true,
} }
func SocketHandler(c *gin.Context, channels map[string]*Channel) { func SocketHandler(c *gin.Context, channels map[string]*Channel, log *logger.CustomLogger, db *gorm.DB) {
// Upgrade HTTP to WebSocket // Upgrade HTTP to WebSocket
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
log.Println("WebSocket upgrade failed:", err) log.Error("WebSocket upgrade failed", "websocket", map[string]interface{}{"error": err})
return return
} }
//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(),
@@ -65,7 +54,7 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
clientsMu.Unlock() clientsMu.Unlock()
// Save initial connection to DB // Save initial connection to DB
client.SaveToDB() client.SaveToDB(log, db)
// Save initial connection to DB // Save initial connection to DB
// if err := client.SaveToDB(); err != nil { // if err := client.SaveToDB(); err != nil {
// log.Println("Failed to save client to DB:", err) // log.Println("Failed to save client to DB:", err)
@@ -73,8 +62,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(log, db)
return nil
})
// Start server-side ping ticker
go client.startServerPings(log, db)
defer func() { defer func() {
// Unregister from all channels // Unregister from all channels
for channelName := range client.Channels { for channelName := range client.Channels {
@@ -89,17 +92,14 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
clientsMu.Unlock() clientsMu.Unlock()
// Mark disconnected in DB // Mark disconnected in DB
client.MarkDisconnected() client.MarkDisconnected(log, db)
// Close connection // Close connection
conn.Close() conn.Close()
log.Printf("Client disconnected: %s", client.ClientID) log.Info("Client disconnected", "websocket", map[string]interface{}{
}() "client": 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{
@@ -107,7 +107,7 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
"message": "Welcome to the WebSocket server. Send subscription request to begin.", "message": "Welcome to the WebSocket server. Send subscription request to begin.",
} }
if err := conn.WriteJSON(welcomeMsg); err != nil { if err := conn.WriteJSON(welcomeMsg); err != nil {
log.Println("Failed to send welcome message:", err) log.Error("Failed to send welcome message", "websocket", map[string]interface{}{"error": err})
return return
} }
@@ -121,14 +121,14 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
} }
} }
close(client.Send) close(client.Send)
client.MarkDisconnected() client.MarkDisconnected(log, db)
}() }()
for { for {
_, msg, err := conn.ReadMessage() _, msg, err := conn.ReadMessage()
if err != nil { if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Printf("Client disconnected unexpectedly: %v", err) log.Error("Client disconnected unexpectedl", "websocket", map[string]interface{}{"error": err})
} }
break break
} }
@@ -176,6 +176,7 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
client.Channels["logServices"] = true client.Channels["logServices"] = true
conn.WriteJSON(map[string]string{ conn.WriteJSON(map[string]string{
"message": "You are now subscribed to the the service channel",
"status": "subscribed", "status": "subscribed",
"channel": "logServices", "channel": "logServices",
}) })
@@ -197,12 +198,13 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
client.Channels["labels"] = true client.Channels["labels"] = true
// Update DB record // Update DB record
client.SaveToDB() client.SaveToDB(log, db)
// if err := client.SaveToDB(); err != nil { // if err := client.SaveToDB(); err != nil {
// log.Println("Failed to update client labels:", err) // log.Println("Failed to update client labels:", err)
// } // }
conn.WriteJSON(map[string]interface{}{ conn.WriteJSON(map[string]interface{}{
"message": "You are now subscribed to the label channel",
"status": "subscribed", "status": "subscribed",
"channel": "labels", "channel": "labels",
"filters": client.Labels, "filters": client.Labels,
@@ -220,7 +222,7 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
// Send messages to client // Send messages to client
for message := range client.Send { for message := range client.Send {
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil { if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
log.Println("Write error:", err) log.Error("Write erro", "websocket", map[string]interface{}{"error": err})
break break
} }
} }

View File

@@ -1,4 +1,4 @@
package websocket package ws
// setup the notifiyer // setup the notifiyer
@@ -23,12 +23,12 @@ import (
"time" "time"
"github.com/lib/pq" "github.com/lib/pq"
logging "lst.net/utils/logger" "lst.net/pkg/logger"
) )
func LogServices(broadcaster chan logging.Message) { func LogServices(broadcaster chan logger.Message, log *logger.CustomLogger) {
fmt.Println("[LogServices] started - single channel for all logs")
logger := logging.New() log.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"),
@@ -41,26 +41,26 @@ func LogServices(broadcaster chan logging.Message) {
listener := pq.NewListener(dsn, 10*time.Second, time.Minute, nil) listener := pq.NewListener(dsn, 10*time.Second, time.Minute, nil)
err := listener.Listen("new_log") err := listener.Listen("new_log")
if err != nil { if err != nil {
logger.Panic("Failed to LISTEN on new_log", "logger", map[string]interface{}{ log.Panic("Failed to LISTEN on new_log", "logger", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
}) })
} }
fmt.Println("Listening for all logs through single logServices channel...") log.Info("Listening for all logs through single logServices channel...", "wbsocker", map[string]interface{}{})
for { for {
select { select {
case notify := <-listener.Notify: case notify := <-listener.Notify:
if notify != nil { if notify != nil {
var logData map[string]interface{} var logData map[string]interface{}
if err := json.Unmarshal([]byte(notify.Extra), &logData); err != nil { if err := json.Unmarshal([]byte(notify.Extra), &logData); err != nil {
logger.Error("Failed to unmarshal notification payload", "logger", map[string]interface{}{ log.Error("Failed to unmarshal notification payload", "logger", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
}) })
continue continue
} }
// Always send to logServices channel // Always send to logServices channel
broadcaster <- logging.Message{ broadcaster <- logger.Message{
Channel: "logServices", Channel: "logServices",
Data: logData, Data: logData,
Meta: map[string]interface{}{ Meta: map[string]interface{}{

View File

@@ -1,17 +1,18 @@
package websocket package ws
import ( import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
logging "lst.net/utils/logger" "gorm.io/gorm"
"lst.net/pkg/logger"
) )
var ( var (
broadcaster = make(chan logging.Message) broadcaster = make(chan logger.Message)
) )
func RegisterSocketRoutes(r *gin.Engine, base_url string) { func RegisterSocketRoutes(r *gin.Engine, base_url string, log *logger.CustomLogger, db *gorm.DB) {
// Initialize all channels // Initialize all channels
InitializeChannels() InitializeChannels()
@@ -19,12 +20,12 @@ func RegisterSocketRoutes(r *gin.Engine, base_url string) {
StartAllChannels() StartAllChannels()
// Start background services // Start background services
go LogServices(broadcaster) go LogServices(broadcaster, log)
go StartBroadcasting(broadcaster, channels) go StartBroadcasting(broadcaster, channels)
// WebSocket route // WebSocket route
r.GET(base_url+"/ws", func(c *gin.Context) { r.GET(base_url+"/ws", func(c *gin.Context) {
SocketHandler(c, channels) SocketHandler(c, channels, log, db)
}) })
r.GET(base_url+"/ws/clients", AdminAuthMiddleware(), handleGetClients) r.GET(base_url+"/ws/clients", AdminAuthMiddleware(), handleGetClients)

View File

@@ -0,0 +1,41 @@
package middleware
import (
"github.com/gin-gonic/gin"
"lst.net/internal/system/settings"
)
func SettingCheckMiddleware(settingName string) gin.HandlerFunc {
return func(c *gin.Context) {
// Debug: Log the setting name we're checking
//log.Printf("Checking setting '%s' for path: %s", settingName, c.Request.URL.Path)
// Get the current setting value
value, err := settings.GetString(settingName)
if err != nil {
//log.Printf("Error getting setting '%s': %v", settingName, err)
c.AbortWithStatusJSON(404, gin.H{
"error": "endpoint not available",
"details": "setting error",
})
return
}
// Debug: Log the actual value received
//log.Printf("Setting '%s' value: '%s'", settingName, value)
// Changed condition to check for "1" (enable) instead of "0" (disable)
if value != "1" {
//log.Printf("Setting '%s' not enabled (value: '%s')", settingName, value)
c.AbortWithStatusJSON(404, gin.H{
"error": "endpoint not available",
"details": "required feature is disabled",
})
return
}
// Debug: Log successful check
//log.Printf("Setting check passed for '%s'", settingName)
c.Next()
}
}

View File

@@ -0,0 +1,66 @@
package router
import (
"net/http"
"os"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"lst.net/internal/notifications/ws"
"lst.net/internal/router/middleware"
"lst.net/internal/system/servers"
"lst.net/internal/system/settings"
"lst.net/pkg/logger"
)
func Setup(db *gorm.DB, basePath string, log *logger.CustomLogger) *gin.Engine {
r := gin.Default()
if os.Getenv("APP_ENV") == "production" {
gin.SetMode(gin.ReleaseMode)
}
// Enable CORS (adjust origins as needed)
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"}, // Allow all origins (change in production)
AllowMethods: []string{"GET", "OPTIONS", "POST", "DELETE", "PATCH", "CONNECT"},
AllowHeaders: []string{"Origin", "Cache-Control", "Content-Type"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
AllowWebSockets: true,
}))
// Serve Docusaurus static files
r.StaticFS(basePath+"/docs", http.Dir("docs"))
r.StaticFS(basePath+"/app", http.Dir("frontend"))
// all routes to there respective systems.
ws.RegisterSocketRoutes(r, basePath, log, db)
settings.RegisterSettingsRoutes(r, basePath, log, db)
servers.RegisterServersRoutes(r, basePath, log, db)
r.GET(basePath+"/api/ping", middleware.SettingCheckMiddleware("testingApiFunction"), func(c *gin.Context) {
log.Info("Checking if the server is up", "system", map[string]interface{}{
"endpoint": "/api/ping",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
c.JSON(200, gin.H{"message": "pong"})
})
r.Any(basePath+"/", func(c *gin.Context) { errorApiLoc(c, log) })
return r
}
func errorApiLoc(c *gin.Context, log *logger.CustomLogger) {
log.Error("Api endpoint hit that dose not exist", "system", map[string]interface{}{
"endpoint": c.Request.URL.Path,
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
c.JSON(http.StatusBadRequest, gin.H{"message": "looks like you have encountered a route that dose not exist"})
}

View File

@@ -0,0 +1,65 @@
package servers
import (
"reflect"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"lst.net/internal/models"
"lst.net/pkg/logger"
)
func getServers(c *gin.Context, log *logger.CustomLogger, db *gorm.DB) {
servers, err := GetServers(log, db)
log.Info("Current Settings", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
if err != nil {
log.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": servers})
}
func GetServers(log *logger.CustomLogger, db *gorm.DB) ([]map[string]interface{}, error) {
var servers []models.Servers
res := db.Find(&servers)
if res.Error != nil {
return nil, res.Error
}
toLowercase := func(s models.Servers) 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
}
var lowercaseServers []map[string]interface{}
for _, server := range servers {
lowercaseServers = append(lowercaseServers, toLowercase(server))
}
return lowercaseServers, nil
}

View File

@@ -0,0 +1,21 @@
package servers
import (
"gorm.io/gorm"
"lst.net/internal/models"
"lst.net/pkg/logger"
)
func NewServer(serverData models.Servers, log *logger.CustomLogger, db *gorm.DB) (string, error) {
err := db.Create(&serverData).Error
if err != nil {
log.Error("There was an error adding the new server", "server", map[string]interface{}{
"error": err,
})
return "There was an error adding the new server", err
}
return "New server was just created", nil
}

View File

@@ -0,0 +1,13 @@
package servers
import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"lst.net/pkg/logger"
)
func RegisterServersRoutes(l *gin.Engine, baseUrl string, log *logger.CustomLogger, db *gorm.DB) {
s := l.Group(baseUrl + "/api/v1")
s.GET("/servers", func(c *gin.Context) { getServers(c, log, db) })
}

View File

@@ -0,0 +1,59 @@
package servers
// import (
// "encoding/json"
// "github.com/gin-gonic/gin"
// "lst.net/internal/db"
// "lst.net/pkg/logger"
// )
// func updateSettingById(c *gin.Context) {
// log := logger.New()
// settingID := c.Param("id")
// if settingID == "" {
// c.JSON(500, gin.H{"message": "Invalid data"})
// log.Error("Invalid data", "system", map[string]interface{}{
// "endpoint": "/api/v1/settings",
// "client_ip": c.ClientIP(),
// "user_agent": c.Request.UserAgent(),
// })
// return
// }
// var setting 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()})
// log.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 := UpdateServer(db.DB, settingID, setting); err != nil {
// c.JSON(500, gin.H{"message": "Failed to update setting", "error": err.Error()})
// log.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})
// }
// func UpdateServer() (string, error) {
// return "Server was just updated", nil
// }

View File

@@ -0,0 +1,39 @@
package settings
import (
"gorm.io/gorm"
)
func GetAllSettings(db *gorm.DB) ([]map[string]interface{}, error) {
// var settings []models.Settings
// result := db.Find(&settings)
// if result.Error != nil {
// return nil, result.Error
// }
// // Function to convert struct to map with lowercase keys
// toLowercase := func(s models.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))
// }
convertedSettings := GetMap()
return convertedSettings, nil
}

View File

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

View File

@@ -0,0 +1,88 @@
package settings
import (
"encoding/json"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"lst.net/pkg/logger"
)
func RegisterSettingsRoutes(l *gin.Engine, baseUrl string, log *logger.CustomLogger, db *gorm.DB) {
// seed the db on start up
SeedSettings(db, log)
s := l.Group(baseUrl + "/api/v1")
s.GET("/settings", func(c *gin.Context) { getSettings(c, log, db) })
s.PATCH("/settings/:id", func(c *gin.Context) { updateSettingById(c, log, db) })
}
func getSettings(c *gin.Context, log *logger.CustomLogger, db *gorm.DB) {
configs, err := GetAllSettings(db)
log.Info("Current Settings", "settings", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
if err != nil {
log := logger.New()
log.Error("Current Settings", "settings", 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, log *logger.CustomLogger, db *gorm.DB) {
settingID := c.Param("id")
if settingID == "" {
c.JSON(500, gin.H{"message": "Invalid data"})
log.Error("Invalid data", "settings", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
return
}
var setting 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()})
log.Error("Invalid request body", "settings", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
"error": err,
})
return
}
if err := UpdateSetting(log, db, settingID, setting); err != nil {
c.JSON(500, gin.H{"message": "Failed to update setting", "error": err.Error()})
log.Error("Failed to update setting", "settings", 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

@@ -0,0 +1,128 @@
package settings
import (
"errors"
"fmt"
"gorm.io/gorm"
"lst.net/internal/models"
"lst.net/pkg/logger"
)
var seedConfigData = []models.Settings{
{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, 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},
{Name: "testingApiFunction", Value: `1`, Description: "This is a test to validate if we set to 0 it will actaully not allow the route", AppService: "logistics", Enabled: true},
}
func SeedSettings(db *gorm.DB, log *logger.CustomLogger) error {
for _, cfg := range seedConfigData {
var existing models.Settings
if err := db.Unscoped().Where("name = ?", cfg.Name).First(&existing).Error; err == nil {
if existing.DeletedAt.Valid {
// Undelete by setting DeletedAt to NULL
if err := db.Unscoped().Model(&existing).Update("DeletedAt", gorm.DeletedAt{}).Error; err != nil {
log.Error("Failed to undelete settings", "settings", map[string]interface{}{
"name": cfg.Name,
"error": err,
})
return nil
}
}
if errors.Is(err, gorm.ErrRecordNotFound) {
if err := db.Create(&cfg).Error; err != nil {
log.Error("Failed to seed settings", "settings", map[string]interface{}{
"name": cfg.Name,
"error": err,
})
}
}
// // Try to find config by unique Name
// result := db.Where("Name =?", cfg.Name).First(&existing)
// if result.Error != nil {
// if result.Error == gorm.ErrRecordNotFound && cfg.Enabled {
// // not here lets add it
// if err := db.Create(&cfg).Error; err != nil && !existing.DeletedAt.Valid {
// log.Error("Failed to seed settings", "settings", map[string]interface{}{
// "name": cfg.Name,
// "error": err,
// })
// }
// //log.Printf("Seeded new config: %s", cfg.Name)
// } else {
// // Some other error
// return result.Error
// }
} else {
// remove the setting if we change to false this will help with future proofing our seeder in the event we need to add it back
if cfg.Enabled {
existing.Description = cfg.Description
existing.Name = cfg.Name
existing.AppService = cfg.AppService
if err := db.Save(&existing).Error; err != nil {
log.Error("Failed to update ettings.", "settings", map[string]interface{}{
"name": cfg.Name,
"error": err,
})
return err
}
} else {
// we delete the setting so its no longer there
if err := db.Delete(&existing).Error; err != nil {
log.Error("Failed to delete ettings.", "settings", map[string]interface{}{
"name": cfg.Name,
"error": err,
})
return err
}
settingDelete := fmt.Sprintf("Updated existing config: %s", cfg.Name)
log.Info(settingDelete, "settings", map[string]interface{}{})
}
//log.Printf("Updated existing config: %s", cfg.Name)
}
}
log.Info("All settings added or updated.", "settings", map[string]interface{}{})
return nil
}

View File

@@ -0,0 +1,110 @@
package settings
import (
"errors"
"fmt"
"reflect"
"strings"
"sync"
"gorm.io/gorm"
"lst.net/internal/models"
)
var (
// Global state
appSettings []models.Settings
appSettingsLock sync.RWMutex
dbInstance *gorm.DB
)
// Initialize loads settings into memory at startup
func Initialize(db *gorm.DB) error {
dbInstance = db
return Refresh()
}
// Refresh reloads settings from DB (call after updates)
func Refresh() error {
appSettingsLock.Lock()
defer appSettingsLock.Unlock()
var settings []models.Settings
if err := dbInstance.Find(&settings).Error; err != nil {
return err
}
appSettings = settings
return nil
}
// GetAll returns a thread-safe copy of settings
func GetAll() []models.Settings {
appSettingsLock.RLock()
defer appSettingsLock.RUnlock()
// Return copy to prevent external modification
copied := make([]models.Settings, len(appSettings))
copy(copied, appSettings)
return copied
}
// GetMap returns settings as []map[string]interface{}
func GetMap() []map[string]interface{} {
return convertToMap(GetAll())
}
// convertToMap helper (move your existing conversion logic here)
func convertToMap(settings []models.Settings) []map[string]interface{} {
toLowercase := func(s models.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
}
func GetString(name string) (string, error) {
appSettingsLock.RLock()
defer appSettingsLock.RUnlock()
for _, s := range appSettings {
if s.Name == name { // assuming your model has a "Name" field
fmt.Println(s.Value)
return s.Value, nil // assuming your model has a "Value" field
}
}
return "", errors.New("setting not found")
}
func SetTemp(name, value string) {
appSettingsLock.Lock()
defer appSettingsLock.Unlock()
for i, s := range appSettings {
if s.Name == name {
appSettings[i].Value = value
return
}
}
// If not found, add new setting
appSettings = append(appSettings, models.Settings{
Name: name,
Value: value,
})
}

View File

@@ -0,0 +1,56 @@
package settings
import (
"gorm.io/gorm"
"lst.net/internal/models"
"lst.net/pkg/logger"
)
func UpdateSetting(log *logger.CustomLogger, db *gorm.DB, id string, input SettingUpdateInput) error {
var cfg models.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
}
settingUpdate := db.Model(&cfg).Updates(updates)
if settingUpdate.Error != nil {
log.Error("There was an error updating the setting", "settings", map[string]interface{}{
"error": settingUpdate.Error,
})
return settingUpdate.Error
}
if err := Refresh(); err != nil {
log.Error("There was an error refreshing the settings after a setting update", "settings", map[string]interface{}{
"error": err,
})
}
log.Info("The setting was just updated", "settings", map[string]interface{}{
"id": id,
"name": cfg.Name,
"updated": updates,
})
return nil
}

View File

@@ -1,59 +1,41 @@
// @title My Awesome API
// @version 1.0
// @description This is a sample server for a pet store.
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io
// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html
// @host localhost:8080
// @BasePath /api/v1
package main package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"net/http"
"os" "os"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"lst.net/cmd/services/system/config"
"lst.net/cmd/services/websocket"
_ "lst.net/docs" "lst.net/internal/db"
"lst.net/internal/router"
"lst.net/utils/db" "lst.net/internal/system/settings"
logging "lst.net/utils/logger" "lst.net/pkg/logger"
) )
func main() { func main() {
log := logging.New() log := logger.New()
// Load .env only in dev (not Docker/production)
if os.Getenv("RUNNING_IN_DOCKER") != "true" { if os.Getenv("RUNNING_IN_DOCKER") != "true" {
err := godotenv.Load("../.env") err := godotenv.Load("../.env")
if err != nil { if err != nil {
log := logger.New()
log.Info("Warning: .env file not found (ok in Docker/production)", "system", map[string]interface{}{}) log.Info("Warning: .env file not found (ok in Docker/production)", "system", map[string]interface{}{})
} }
} }
// Initialize DB // Initialize DB
if _, err := db.InitDB(); err != nil { if _, err := db.InitDB(); err != nil {
log.Panic("Database intialize failed", "db", map[string]interface{}{
log.Panic("Database intialize failed, please check the server asap.", "db", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"casue": errors.Unwrap(err), "cause": errors.Unwrap(err),
"timeout": "30s", "timeout": "30s",
"details": fmt.Sprintf("%+v", err), // Full stack trace if available "details": fmt.Sprintf("%+v", err), // Full stack trace if available
}) })
} }
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
sqlDB, _ := db.DB.DB() sqlDB, _ := db.DB.DB()
sqlDB.Close() sqlDB.Close()
log.Error("Recovered from panic during DB shutdown", "db", map[string]interface{}{ log.Error("Recovered from panic during DB shutdown", "db", map[string]interface{}{
@@ -62,6 +44,16 @@ func main() {
} }
}() }()
if err := settings.Initialize(db.DB); err != nil {
log.Panic("There was an error intilizing the settings", "settings", map[string]interface{}{
"error": err,
})
}
// long lived process like ocp running all the time should go here and base the db struct over.
// go ocp.MonitorPrinters
// go notifcations.Processor
// Set basePath dynamically // Set basePath dynamically
basePath := "/" basePath := "/"
@@ -69,79 +61,19 @@ func main() {
basePath = "/lst" // Dev only basePath = "/lst" // Dev only
} }
// fmt.Println(name) log.Info("Welcome to lst backend where all the fun happens.", "system", map[string]interface{}{})
fmt.Println("Welcome to lst backend where all the fun happens.") // Init Gin router and pass DB to services
r := gin.Default() r := router.Setup(db.DB, basePath, log)
if os.Getenv("APP_ENV") == "production" {
gin.SetMode(gin.ReleaseMode)
}
// Enable CORS (adjust origins as needed)
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"}, // Allow all origins (change in production)
AllowMethods: []string{"GET", "OPTIONS", "POST", "DELETE", "PATCH", "CONNECT"},
AllowHeaders: []string{"Origin", "Cache-Control", "Content-Type"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
AllowWebSockets: true,
}))
// // --- Add Redirects Here ---
// // Redirect root ("/") to "/app" or "/lst/app"
// r.GET("/", func(c *gin.Context) {
// c.Redirect(http.StatusMovedPermanently, basePath+"/app")
// })
// // Redirect "/lst" (if applicable) to "/lst/app"
// if basePath == "/lst" {
// r.GET("/lst", func(c *gin.Context) {
// c.Redirect(http.StatusMovedPermanently, basePath+"/app")
// })
// }
// Serve Docusaurus static files
r.StaticFS(basePath+"/docs", http.Dir("docs"))
r.StaticFS(basePath+"/app", http.Dir("frontend"))
r.GET(basePath+"/api/ping", func(c *gin.Context) {
log.Info("Checking if the server is up", "system", map[string]interface{}{
"endpoint": "/api/ping",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
c.JSON(200, gin.H{"message": "pong"})
})
//logging.RegisterLoggerRoutes(r, basePath)
websocket.RegisterSocketRoutes(r, basePath)
config.RegisterConfigRoutes(r, basePath)
r.Any(basePath+"/api", errorApiLoc)
// get the server port // get the server port
port := "8080" port := "8080"
if os.Getenv("VITE_SERVER_PORT") != "" { if os.Getenv("VITE_SERVER_PORT") != "" {
port = os.Getenv("VITE_SERVER_PORT") port = os.Getenv("VITE_SERVER_PORT")
} }
r.Run(":" + port)
}
// func serveViteApp(c *gin.Context) { if err := r.Run(":" + port); err != nil {
// // Set proper Content-Type for HTML log.Panic("Server failed to start", "system", map[string]interface{}{
// c.Header("Content-Type", "text/html") "error": err,
// c.File("./dist/index.html")
// }
// func errorLoc(c *gin.Context) {
// c.JSON(http.StatusBadRequest, gin.H{"message": "welcome to lst system you might have just encountered an incorrect area of the app"})
// }
func errorApiLoc(c *gin.Context) {
log := logging.New()
log.Error("Api endpoint hit that dose not exist", "system", map[string]interface{}{
"endpoint": "/api",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
}) })
c.JSON(http.StatusBadRequest, gin.H{"message": "looks like you have encountered an api route that dose not exist"}) }
} }

3
backend/pkg/json.go Normal file
View File

@@ -0,0 +1,3 @@
package pkg
type JSONB map[string]interface{}

View File

@@ -0,0 +1,18 @@
package logger
import (
"lst.net/internal/db"
"lst.net/internal/models"
"lst.net/pkg"
)
// CreateLog inserts a new log entry.
func CreateLog(level, message, service string, metadata pkg.JSONB) error {
log := models.Log{
Level: level,
Message: message,
Service: service,
Metadata: metadata,
}
return db.DB.Create(&log).Error
}

View File

@@ -0,0 +1,77 @@
package logger
import (
"encoding/json"
"log"
"os"
"time"
discordwebhook "github.com/bensch777/discord-webhook-golang"
)
func CreateDiscordMsg(message string) {
// we will only run the discord bot if we actaully put a url in the.
if os.Getenv("WEBHOOK") != "" {
var webhookurl = os.Getenv("WEBHOOK")
host, _ := os.Hostname()
embed := discordwebhook.Embed{
Title: "A new crash report from lst.",
Color: 15277667,
Url: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
Timestamp: time.Now(),
// Thumbnail: discordwebhook.Thumbnail{
// Url: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
// },
// Author: discordwebhook.Author{
// Name: "Author Name",
// Icon_URL: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
// },
Fields: []discordwebhook.Field{
discordwebhook.Field{
Name: host,
Value: message,
Inline: false,
},
// discordwebhook.Field{
// Name: "Error reason",
// Value: stack,
// Inline: false,
// },
// discordwebhook.Field{
// Name: "Field 3",
// Value: "Field Value 3",
// Inline: false,
// },
},
// Footer: discordwebhook.Footer{
// Text: "Footer Text",
// Icon_url: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
// },
}
SendEmbed(webhookurl, embed)
} else {
return
}
}
func SendEmbed(link string, embeds discordwebhook.Embed) error {
logging := New()
logging.Info("new messege being posted to discord", "logger", map[string]interface{}{
"message": "Message",
})
hook := discordwebhook.Hook{
Username: "Captain Hook",
Avatar_url: "https://avatars.githubusercontent.com/u/6016509?s=48&v=4",
Content: "Message",
Embeds: []discordwebhook.Embed{embeds},
}
payload, err := json.Marshal(hook)
if err != nil {
log.Fatal(err)
}
err = discordwebhook.ExecuteWebhook(link, payload)
return err
}

View File

@@ -1,4 +1,4 @@
package logging package logger
import ( import (
"encoding/json" "encoding/json"
@@ -9,7 +9,6 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"lst.net/utils/db"
) )
type CustomLogger struct { type CustomLogger struct {
@@ -50,7 +49,7 @@ func PrettyFormat(level, message string, metadata map[string]interface{}) string
} }
func (l *CustomLogger) logToPostgres(level, message, service string, metadata map[string]interface{}) { func (l *CustomLogger) logToPostgres(level, message, service string, metadata map[string]interface{}) {
err := db.CreateLog(level, message, service, metadata) err := CreateLog(level, message, service, metadata)
if err != nil { if err != nil {
// Fallback to console if DB fails // Fallback to console if DB fails
log.Error().Err(err).Msg("Failed to write log to PostgreSQL") log.Error().Err(err).Msg("Failed to write log to PostgreSQL")
@@ -98,7 +97,7 @@ func (l *CustomLogger) Panic(message, service string, fields map[string]interfac
Msg(message + " (PANIC)") // Explicitly mark as panic Msg(message + " (PANIC)") // Explicitly mark as panic
// Log to PostgreSQL (sync to ensure it's saved before crashing) // Log to PostgreSQL (sync to ensure it's saved before crashing)
err := db.CreateLog("panic", message, service, fields) // isCritical=true err := CreateLog("panic", message, service, fields) // isCritical=true
if err != nil { if err != nil {
l.consoleLogger.Error().Err(err).Msg("Failed to save panic log to PostgreSQL") l.consoleLogger.Error().Err(err).Msg("Failed to save panic log to PostgreSQL")
} }
@@ -108,6 +107,7 @@ func (l *CustomLogger) Panic(message, service string, fields map[string]interfac
l.consoleLogger.Warn().Msg("Additional panic context captured") l.consoleLogger.Warn().Msg("Additional panic context captured")
} }
CreateDiscordMsg(message)
panic(message) panic(message)
} }

View File

@@ -1,48 +0,0 @@
package db
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Log struct {
LogID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
Level string `gorm:"size:10;not null"` // "info", "error", etc.
Message string `gorm:"not null"`
Service string `gorm:"size:50"`
Metadata JSONB `gorm:"type:jsonb"` // fields (e.g., {"user_id": 123})
CreatedAt time.Time `gorm:"index"`
Checked bool `gorm:"type:boolean;default:false"`
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
// JSONB is a helper type for PostgreSQL JSONB fields.
//type JSONB map[string]interface{}
// --- CRUD Operations ---
// CreateLog inserts a new log entry.
func CreateLog(level, message, service string, metadata JSONB) error {
log := Log{
Level: level,
Message: message,
Service: service,
Metadata: metadata,
}
return DB.Create(&log).Error
}
// GetLogsByLevel fetches logs filtered by severity.
func GetLogs(level string, limit int, service string) ([]Log, error) {
var logs []Log
err := DB.Where("level = ? and service = ?", level, service).Limit(limit).Find(&logs).Error
return logs, err
}
// DeleteOldLogs removes logs older than `days` and by level.
func DeleteOldLogs(days int, level string) error {
return DB.Where("created_at < ? and level = ?", time.Now().AddDate(0, 0, -days), level).Delete(&Log{}).Error
}

View File

@@ -1,67 +0,0 @@
package db
import (
"log"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Settings struct {
ConfigID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;not null"`
Description string `gorm:"type:text"`
Value string `gorm:"not null"`
Enabled bool `gorm:"default:true"`
AppService string `gorm:"default:system"`
CreatedAt time.Time `gorm:"index"`
UpdatedAt time.Time `gorm:"index"`
DeletedAt gorm.DeletedAt `gorm:"index"`
}
var seedConfigData = []Settings{
{Name: "serverPort", Description: "The port the server will listen on if not running in docker", Value: "4000", Enabled: true},
{Name: "server", Description: "The server we will use when connecting to the alplaprod sql", Value: "usmcd1vms006", Enabled: true},
}
func SeedConfigs(db *gorm.DB) error {
for _, cfg := range seedConfigData {
var existing Settings
// Try to find config by unique name
result := db.Where("name =?", cfg.Name).First(&existing)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
// not here lets add it
if err := db.Create(&cfg).Error; err != nil {
log.Printf("Failed to seed config %s: %v", cfg.Name, err)
}
log.Printf("Seeded new config: %s", cfg.Name)
} else {
// Some other error
return result.Error
}
} else {
// only update the fields we want to update.
existing.Description = cfg.Description
if err := db.Save(&existing).Error; err != nil {
log.Printf("Failed to update config %s: %v", cfg.Name, err)
return err
}
log.Printf("Updated existing config: %s", cfg.Name)
}
}
return nil
}
func GetAllConfigs(db *gorm.DB) ([]Settings, error) {
var settings []Settings
result := db.Find(&settings)
return settings, result.Error
}

View File

@@ -8,7 +8,7 @@ const WebSocketViewer = () => {
ws.current = new WebSocket( ws.current = new WebSocket(
(window.location.protocol === "https:" ? "wss://" : "ws://") + (window.location.protocol === "https:" ? "wss://" : "ws://") +
window.location.host + window.location.host +
"/lst/api/logger/logs" "/lst/ws"
); );
ws.current.onopen = () => { ws.current.onopen = () => {

View File

@@ -1,6 +1,13 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import path from "path";
import dotenv from "dotenv";
import { fileURLToPath } from "url";
dotenv.config({
path: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.env"),
});
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
@@ -10,4 +17,24 @@ export default defineConfig({
assetsDir: "assets", assetsDir: "assets",
emptyOutDir: true, emptyOutDir: true,
}, },
server: {
proxy: {
"/lst/api": {
target: `http://localhost:${Number(
process.env.VITE_SERVER_PORT || 8080
)}`,
changeOrigin: true,
secure: false,
},
"/lst/ws": {
target: `ws://localhost:${Number(
process.env.VITE_SERVER_PORT || 8080
)}`, // Your Go WebSocket endpoint
ws: true,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(/^\/ws/, ""),
},
},
},
}); });

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",