9 Commits

38 changed files with 911 additions and 672 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

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

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

@@ -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.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.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 {
@@ -13,7 +14,7 @@ type ClientRecord struct {
UserAgent string `gorm:"size:255"` UserAgent string `gorm:"size:255"`
ConnectedAt time.Time `gorm:"index"` ConnectedAt time.Time `gorm:"index"`
LastHeartbeat time.Time `gorm:"column:last_heartbeat"` 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,8 +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 := logging.New() logger := logger.New()
go func() { go func() {
for msg := range broadcaster { for msg := range broadcaster {
switch msg.Channel { switch msg.Channel {
@@ -147,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

@@ -1,16 +1,17 @@
package websocket package ws
import ( import (
"fmt" "fmt"
"log"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"lst.net/utils/db" "gorm.io/gorm"
logging "lst.net/utils/logger" "lst.net/internal/models"
"lst.net/pkg"
"lst.net/pkg/logger"
) )
var ( var (
@@ -36,38 +37,44 @@ type Client struct {
} }
func (c *Client) SaveToDB() { func (c *Client) SaveToDB(log *logger.CustomLogger, db *gorm.DB) {
// Convert c.Channels (map[string]bool) to map[string]interface{} for JSONB // Convert c.Channels (map[string]bool) to map[string]interface{} for JSONB
channels := make(map[string]interface{}) channels := make(map[string]interface{})
for ch := range c.Channels { for ch := range c.Channels {
channels[ch] = true channels[ch] = true
} }
clientRecord := &db.ClientRecord{ clientRecord := &models.ClientRecord{
APIKey: c.APIKey, APIKey: c.APIKey,
IPAddress: c.IPAddress, IPAddress: c.IPAddress,
UserAgent: c.UserAgent, UserAgent: c.UserAgent,
Channels: db.JSONB(channels), Channels: pkg.JSONB(channels),
ConnectedAt: time.Now(), ConnectedAt: time.Now(),
LastHeartbeat: time.Now(), LastHeartbeat: time.Now(),
} }
if err := db.DB.Create(&clientRecord).Error; err != nil { if err := db.Create(&clientRecord).Error; err != nil {
log.Println("❌ Error saving client:", err) log.Error("❌ Error saving client", "websocket", map[string]interface{}{
"error": err,
})
} else { } else {
c.ClientID = clientRecord.ClientID c.ClientID = clientRecord.ClientID
c.ConnectedAt = clientRecord.ConnectedAt 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() { func (c *Client) MarkDisconnected(log *logger.CustomLogger, db *gorm.DB) {
logger := logging.New()
clientData := fmt.Sprintf("Client %v just lefts us", c.ClientID) clientData := fmt.Sprintf("Client %v Dicconected", c.ClientID)
logger.Info(clientData, "websocket", map[string]interface{}{}) log.Info(clientData, "websocket", map[string]interface{}{})
now := time.Now() now := time.Now()
res := db.DB.Model(&db.ClientRecord{}). res := db.Model(&models.ClientRecord{}).
Where("client_id = ?", c.ClientID). Where("client_id = ?", c.ClientID).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"disconnected_at": &now, "disconnected_at": &now,
@@ -75,13 +82,13 @@ func (c *Client) MarkDisconnected() {
if res.RowsAffected == 0 { if res.RowsAffected == 0 {
logger.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{ log.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{
"clientID": c.ClientID, "clientID": c.ClientID,
}) })
} }
if res.Error != nil { if res.Error != nil {
logger.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{ log.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{
"clientID": c.ClientID, "clientID": c.ClientID,
"error": res.Error, "error": res.Error,
}) })
@@ -135,28 +142,31 @@ const (
writeWait = 10 * time.Second writeWait = 10 * time.Second
) )
func (c *Client) StartHeartbeat() { func (c *Client) StartHeartbeat(log *logger.CustomLogger, db *gorm.DB) {
logger := logging.New()
log.Println("Started hearbeat") log.Debug("Started hearbeat", "websocket", map[string]interface{}{})
ticker := time.NewTicker(pingPeriod) ticker := time.NewTicker(pingPeriod)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
if !c.isAlive.Load() { // Correct way to read atomic.Bool if !c.isAlive.Load() {
return return
} }
c.Conn.SetWriteDeadline(time.Now().Add(writeWait)) c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Printf("Heartbeat failed for %s: %v", c.ClientID, err) log.Error("Heartbeat failed", "websocket", map[string]interface{}{
c.Close() "client_id": c.ClientID,
"error": err,
})
c.Close(log, db)
return return
} }
now := time.Now() now := time.Now()
res := db.DB.Model(&db.ClientRecord{}). res := db.Model(&models.ClientRecord{}).
Where("client_id = ?", c.ClientID). Where("client_id = ?", c.ClientID).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"last_heartbeat": &now, "last_heartbeat": &now,
@@ -164,17 +174,21 @@ func (c *Client) StartHeartbeat() {
if res.RowsAffected == 0 { if res.RowsAffected == 0 {
logger.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{ log.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{
"clientID": c.ClientID, "clientID": c.ClientID,
}) })
} }
if res.Error != nil { if res.Error != nil {
logger.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{ log.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{
"clientID": c.ClientID, "clientID": c.ClientID,
"error": res.Error, "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: case <-c.done:
return return
@@ -182,16 +196,16 @@ func (c *Client) StartHeartbeat() {
} }
} }
func (c *Client) Close() { func (c *Client) Close(log *logger.CustomLogger, db *gorm.DB) {
if c.isAlive.CompareAndSwap(true, false) { // Atomic swap if c.isAlive.CompareAndSwap(true, false) { // Atomic swap
close(c.done) close(c.done)
c.Conn.Close() c.Conn.Close()
// Add any other cleanup here // Add any other cleanup here
c.MarkDisconnected() c.MarkDisconnected(log, db)
} }
} }
func (c *Client) startServerPings() { func (c *Client) startServerPings(log *logger.CustomLogger, db *gorm.DB) {
ticker := time.NewTicker(60 * time.Second) // Ping every 30s ticker := time.NewTicker(60 * time.Second) // Ping every 30s
defer ticker.Stop() defer ticker.Stop()
@@ -200,7 +214,13 @@ func (c *Client) startServerPings() {
case <-ticker.C: case <-ticker.C:
c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) c.Conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil { if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
c.Close() // Disconnect if ping fails
log.Error("Server Ping failed", "websocket", map[string]interface{}{
"clientID": c.ClientID,
"error": err,
})
c.Close(log, db)
return return
} }
case <-c.done: case <-c.done:
@@ -217,15 +237,14 @@ func (c *Client) IsActive() bool {
return time.Since(c.lastActive) < 45*time.Second // 1.5x ping interval return time.Since(c.lastActive) < 45*time.Second // 1.5x ping interval
} }
func (c *Client) updateHeartbeat() { func (c *Client) updateHeartbeat(log *logger.CustomLogger, db *gorm.DB) {
//fmt.Println("Updating heatbeat") //fmt.Println("Updating heatbeat")
now := time.Now() now := time.Now()
logger := logging.New()
//fmt.Printf("Updating heartbeat for client: %s at %v\n", c.ClientID, now) //fmt.Printf("Updating heartbeat for client: %s at %v\n", c.ClientID, now)
//db.DB = db.DB.Debug() //db.DB = db.DB.Debug()
res := db.DB.Model(&db.ClientRecord{}). res := db.Model(&models.ClientRecord{}).
Where("client_id = ?", c.ClientID). Where("client_id = ?", c.ClientID).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"last_heartbeat": &now, // Explicit format "last_heartbeat": &now, // Explicit format
@@ -233,27 +252,27 @@ func (c *Client) updateHeartbeat() {
//fmt.Printf("Executed SQL: %v\n", db.DB.Statement.SQL.String()) //fmt.Printf("Executed SQL: %v\n", db.DB.Statement.SQL.String())
if res.RowsAffected == 0 { if res.RowsAffected == 0 {
logger.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{ log.Info("⚠️ No rows updated for client_id", "websocket", map[string]interface{}{
"clientID": c.ClientID, "clientID": c.ClientID,
}) })
} }
if res.Error != nil { if res.Error != nil {
logger.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{ log.Error("❌ Error updating disconnected_at", "websocket", map[string]interface{}{
"clientID": c.ClientID, "clientID": c.ClientID,
"error": res.Error, "error": res.Error,
}) })
} }
// 2. Verify DB connection // 2. Verify DB connection
if db.DB == nil { if db == nil {
logger.Error("DB connection is nil", "websocket", map[string]interface{}{}) log.Error("DB connection is nil", "websocket", map[string]interface{}{})
return return
} }
// 3. Test raw SQL execution first // 3. Test raw SQL execution first
testRes := db.DB.Exec("SELECT 1") testRes := db.Exec("SELECT 1")
if testRes.Error != nil { if testRes.Error != nil {
logger.Error("DB ping failed", "websocket", map[string]interface{}{ log.Error("DB ping failed", "websocket", map[string]interface{}{
"error": testRes.Error, "error": testRes.Error,
}) })
return return

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,11 +27,11 @@ 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()
@@ -53,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)
@@ -70,12 +71,12 @@ func SocketHandler(c *gin.Context, channels map[string]*Channel) {
now := time.Now() now := time.Now()
client.markActive() // Track last pong time client.markActive() // Track last pong time
client.lastActive = now client.lastActive = now
client.updateHeartbeat() client.updateHeartbeat(log, db)
return nil return nil
}) })
// Start server-side ping ticker // Start server-side ping ticker
go client.startServerPings() go client.startServerPings(log, db)
defer func() { defer func() {
// Unregister from all channels // Unregister from all channels
@@ -91,11 +92,13 @@ 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,
})
}() }()
// Send welcome message immediately // Send welcome message immediately
@@ -104,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
} }
@@ -118,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
} }
@@ -173,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",
}) })
@@ -194,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,
@@ -217,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,13 +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) {
logger := logging.New()
logger.Info("[LogServices] started - single channel for all logs", "websocket", map[string]interface{}{}) 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"),
@@ -42,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

@@ -1,4 +1,4 @@
package inputs package settings
type SettingUpdateInput struct { type SettingUpdateInput struct {
Description *string `json:"description"` Description *string `json:"description"`

View File

@@ -4,31 +4,31 @@ import (
"encoding/json" "encoding/json"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"lst.net/utils/db" "gorm.io/gorm"
"lst.net/utils/inputs"
logging "lst.net/utils/logger" "lst.net/pkg/logger"
) )
func RegisterSettingsRoutes(l *gin.Engine, baseUrl string) { func RegisterSettingsRoutes(l *gin.Engine, baseUrl string, log *logger.CustomLogger, db *gorm.DB) {
// seed the db on start up // seed the db on start up
db.SeedConfigs(db.DB) SeedSettings(db, log)
s := l.Group(baseUrl + "/api/v1") s := l.Group(baseUrl + "/api/v1")
s.GET("/settings", getSettings) s.GET("/settings", func(c *gin.Context) { getSettings(c, log, db) })
s.PATCH("/settings/:id", updateSettingById) s.PATCH("/settings/:id", func(c *gin.Context) { updateSettingById(c, log, db) })
} }
func getSettings(c *gin.Context) { func getSettings(c *gin.Context, log *logger.CustomLogger, db *gorm.DB) {
logger := logging.New() configs, err := GetAllSettings(db)
configs, err := db.GetAllConfigs(db.DB) log.Info("Current Settings", "settings", map[string]interface{}{
logger.Info("Current Settings", "system", map[string]interface{}{
"endpoint": "/api/v1/settings", "endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(), "client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(), "user_agent": c.Request.UserAgent(),
}) })
if err != nil { if err != nil {
logger.Error("Current Settings", "system", map[string]interface{}{ log := logger.New()
log.Error("Current Settings", "settings", map[string]interface{}{
"endpoint": "/api/v1/settings", "endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(), "client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(), "user_agent": c.Request.UserAgent(),
@@ -41,20 +41,20 @@ func getSettings(c *gin.Context) {
c.JSON(200, gin.H{"message": "Current settings", "data": configs}) c.JSON(200, gin.H{"message": "Current settings", "data": configs})
} }
func updateSettingById(c *gin.Context) { func updateSettingById(c *gin.Context, log *logger.CustomLogger, db *gorm.DB) {
logger := logging.New()
settingID := c.Param("id") settingID := c.Param("id")
if settingID == "" { if settingID == "" {
c.JSON(500, gin.H{"message": "Invalid data"}) c.JSON(500, gin.H{"message": "Invalid data"})
logger.Error("Invalid data", "system", map[string]interface{}{ log.Error("Invalid data", "settings", map[string]interface{}{
"endpoint": "/api/v1/settings", "endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(), "client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(), "user_agent": c.Request.UserAgent(),
}) })
return return
} }
var setting inputs.SettingUpdateInput var setting SettingUpdateInput
//err := c.ShouldBindBodyWithJSON(&setting) //err := c.ShouldBindBodyWithJSON(&setting)
@@ -63,7 +63,7 @@ func updateSettingById(c *gin.Context) {
if err := decoder.Decode(&setting); err != nil { if err := decoder.Decode(&setting); err != nil {
c.JSON(400, gin.H{"message": "Invalid request body", "error": err.Error()}) c.JSON(400, gin.H{"message": "Invalid request body", "error": err.Error()})
logger.Error("Invalid request body", "system", map[string]interface{}{ log.Error("Invalid request body", "settings", map[string]interface{}{
"endpoint": "/api/v1/settings", "endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(), "client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(), "user_agent": c.Request.UserAgent(),
@@ -72,9 +72,9 @@ func updateSettingById(c *gin.Context) {
return return
} }
if err := db.UpdateConfig(db.DB, settingID, setting); err != nil { if err := UpdateSetting(log, db, settingID, setting); err != nil {
c.JSON(500, gin.H{"message": "Failed to update setting", "error": err.Error()}) c.JSON(500, gin.H{"message": "Failed to update setting", "error": err.Error()})
logger.Error("Failed to update setting", "system", map[string]interface{}{ log.Error("Failed to update setting", "settings", map[string]interface{}{
"endpoint": "/api/v1/settings", "endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(), "client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(), "user_agent": c.Request.UserAgent(),

View File

@@ -1,29 +1,15 @@
package db package settings
import ( import (
"log" "errors"
"reflect" "fmt"
"strings"
"time"
"github.com/google/uuid"
"gorm.io/gorm" "gorm.io/gorm"
"lst.net/utils/inputs" "lst.net/internal/models"
"lst.net/pkg/logger"
) )
type Settings struct { var seedConfigData = []models.Settings{
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"`
}
var seedConfigData = []Settings{
{Name: "serverPort", Description: "The port the server will listen on if not running in docker", Value: "4000", Enabled: true, AppService: "server"}, {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: "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: "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},
@@ -58,110 +44,85 @@ var seedConfigData = []Settings{
{Name: "scannerID", Value: `500`, Description: "What scanner id will we be using for the app", AppService: "logistics", 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: "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: "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 SeedConfigs(db *gorm.DB) error { func SeedSettings(db *gorm.DB, log *logger.CustomLogger) error {
for _, cfg := range seedConfigData { for _, cfg := range seedConfigData {
var existing Settings var existing models.Settings
// Try to find config by unique Name if err := db.Unscoped().Where("name = ?", cfg.Name).First(&existing).Error; err == nil {
result := db.Where("Name =?", cfg.Name).First(&existing)
if result.Error != nil { if existing.DeletedAt.Valid {
if result.Error == gorm.ErrRecordNotFound { // Undelete by setting DeletedAt to NULL
// not here lets add it 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 { if err := db.Create(&cfg).Error; err != nil {
log.Printf("Failed to seed config %s: %v", cfg.Name, err) 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
} }
// // 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 { } else {
// only update the fields we want to update. // 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.Description = cfg.Description
existing.Name = cfg.Name
existing.AppService = cfg.AppService
if err := db.Save(&existing).Error; err != nil { if err := db.Save(&existing).Error; err != nil {
log.Printf("Failed to update config %s: %v", cfg.Name, err) log.Error("Failed to update ettings.", "settings", map[string]interface{}{
"name": cfg.Name,
"error": err,
})
return 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.Printf("Updated existing config: %s", cfg.Name)
} }
} }
log.Info("All settings added or updated.", "settings", map[string]interface{}{})
return nil return nil
} }
func GetAllConfigs(db *gorm.DB) ([]map[string]interface{}, error) {
var settings []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 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

@@ -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/settings"
"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)
settings.RegisterSettingsRoutes(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

@@ -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/, ""),
},
},
},
}); });