12 Commits

28 changed files with 1371 additions and 134 deletions

View File

@@ -1,6 +1,9 @@
# uncomment this out to run in productions # uncomment this out to run in productions
# APP_ENV=production # APP_ENV=production
# Server port that will allow vite to talk to the backend.
VITE_SERVER_PORT=4000
# lstv2 loc # lstv2 loc
LSTV2="C\drive\loc" LSTV2="C\drive\loc"
@@ -12,7 +15,17 @@ GITEA_USERNAME=username
GITEA_REPO=logistics_support_tool GITEA_REPO=logistics_support_tool
GITEA_TOKEN=ad8eac91a01e3a1885a1dc10 GITEA_TOKEN=ad8eac91a01e3a1885a1dc10
# postgres db
DB_HOST=localhost
DB_PORT=5433
DB_USER=username
DB_PASSWORD=password
DB_NAME=lst # db must be created before you start the app
# dev locs # dev locs
DEV_FOLDER=C\drive\loc DEV_FOLDER=C\drive\loc
ADMUSER=username ADMUSER=username
ADMPASSWORD=password ADMPASSWORD=password
# Build number info
BUILD_NAME=leBlfRaj

2
.gitignore vendored
View File

@@ -10,6 +10,7 @@ LstWrapper/obj
scripts/tmp scripts/tmp
backend/docs backend/docs
backend/frontend backend/frontend
testFolder
# ---> Go # ---> Go
# If you prefer the allow list template instead of the deny list, see community template: # If you prefer the allow list template instead of the deny list, see community template:
@@ -191,4 +192,5 @@ backend/go.sum
BUILD_NUMBER BUILD_NUMBER
scripts/resetDanger.js scripts/resetDanger.js
LstWrapper/Program_vite_as_Static.txt LstWrapper/Program_vite_as_Static.txt
LstWrapper/Program_proxy_backend.txt
scripts/stopPool.go scripts/stopPool.go

View File

@@ -1,65 +1,150 @@
using System.Net;
using System.Net.WebSockets;
using System.Text;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Configure clients builder.Services.AddHttpClient("GoBackend", client =>
builder.Services.AddHttpClient("GoBackend", client => { {
client.BaseAddress = new Uri("http://localhost:8080"); client.BaseAddress = new Uri("http://localhost:8080");
}); });
var app = builder.Build(); var app = builder.Build();
// Handle trailing slash redirects // Enable WebSocket support
app.Use(async (context, next) => { app.UseWebSockets();
if (context.Request.Path.Equals("/lst", StringComparison.OrdinalIgnoreCase)) {
context.Response.Redirect("/lst/", permanent: true); app.Use(async (context, next) =>
return; {
// Proxy WebSocket requests for /lst/api/logger/logs (adjust path as needed)
if (context.WebSockets.IsWebSocketRequest &&
context.Request.Path.StartsWithSegments("/lst/api/logger/logs"))
{
Console.WriteLine("WebSocket request received!");
try
{
var backendUri = new UriBuilder("ws", "localhost", 8080)
{
Path = context.Request.Path,
Query = context.Request.QueryString.ToString()
}.Uri;
using var backendSocket = new ClientWebSocket();
// Forward most headers except those managed by WebSocket protocol
foreach (var header in context.Request.Headers)
{
if (!header.Key.Equals("Host", StringComparison.OrdinalIgnoreCase) &&
!header.Key.Equals("Upgrade", StringComparison.OrdinalIgnoreCase) &&
!header.Key.Equals("Connection", StringComparison.OrdinalIgnoreCase) &&
!header.Key.Equals("Sec-WebSocket-Key", StringComparison.OrdinalIgnoreCase) &&
!header.Key.Equals("Sec-WebSocket-Version", StringComparison.OrdinalIgnoreCase))
{
backendSocket.Options.SetRequestHeader(header.Key, header.Value);
}
}
await backendSocket.ConnectAsync(backendUri, context.RequestAborted);
using var frontendSocket = await context.WebSockets.AcceptWebSocketAsync();
var cts = new CancellationTokenSource();
// Bidirectional forwarding tasks
var forwardToBackend = ForwardWebSocketAsync(frontendSocket, backendSocket, cts.Token);
var forwardToFrontend = ForwardWebSocketAsync(backendSocket, frontendSocket, cts.Token);
await Task.WhenAny(forwardToBackend, forwardToFrontend);
cts.Cancel();
return;
}
catch (Exception ex)
{
context.Response.StatusCode = (int)HttpStatusCode.BadGateway;
await context.Response.WriteAsync($"WebSocket proxy error: {ex.Message}");
return;
}
} }
await next(); await next();
}); });
// Proxy all requests to Go backend // Proxy normal HTTP requests
app.Use(async (context, next) => { app.Use(async (context, next) =>
// Skip special paths {
if (context.Request.Path.StartsWithSegments("/.well-known")) { if (context.WebSockets.IsWebSocketRequest)
{
await next(); await next();
return; return;
} }
var client = context.RequestServices.GetRequiredService<IHttpClientFactory>() var client = context.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient("GoBackend");
.CreateClient("GoBackend");
try { try
var request = new HttpRequestMessage( {
new HttpMethod(context.Request.Method), var request = new HttpRequestMessage(new HttpMethod(context.Request.Method),
context.Request.Path + context.Request.QueryString); context.Request.Path + context.Request.QueryString);
// Copy headers foreach (var header in context.Request.Headers)
foreach (var header in context.Request.Headers) { {
if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray())) { if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()))
{
request.Content ??= new StreamContent(context.Request.Body); request.Content ??= new StreamContent(context.Request.Body);
request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
} }
} }
if (context.Request.ContentLength > 0) { if (context.Request.ContentLength > 0 && request.Content == null)
{
request.Content = new StreamContent(context.Request.Body); request.Content = new StreamContent(context.Request.Body);
} }
var response = await client.SendAsync(request); var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted);
context.Response.StatusCode = (int)response.StatusCode; context.Response.StatusCode = (int)response.StatusCode;
foreach (var header in response.Headers) { foreach (var header in response.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray();
}
foreach (var header in response.Content.Headers)
{
context.Response.Headers[header.Key] = header.Value.ToArray(); context.Response.Headers[header.Key] = header.Value.ToArray();
} }
if (response.Content.Headers.ContentType != null) { context.Response.Headers.Remove("transfer-encoding");
context.Response.ContentType = response.Content.Headers.ContentType.ToString();
}
await response.Content.CopyToAsync(context.Response.Body); await response.Content.CopyToAsync(context.Response.Body);
} }
catch (HttpRequestException) { catch (HttpRequestException ex)
context.Response.StatusCode = 502; {
context.Response.StatusCode = (int)HttpStatusCode.BadGateway;
await context.Response.WriteAsync($"Backend request failed: {ex.Message}");
} }
}); });
async Task ForwardWebSocketAsync(WebSocket source, WebSocket destination, CancellationToken cancellationToken)
{
var buffer = new byte[4 * 1024];
try
{
while (source.State == WebSocketState.Open &&
destination.State == WebSocketState.Open &&
!cancellationToken.IsCancellationRequested)
{
var result = await source.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
if (result.MessageType == WebSocketMessageType.Close)
{
await destination.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
break;
}
await destination.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellationToken);
}
}
catch (WebSocketException)
{
// Normal close or network error
}
}
app.Run(); app.Run();

View File

@@ -1,36 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<system.webServer> <system.webServer>
<!-- Enable WebSockets -->
<webSocket enabled="true" receiveBufferLimit="4194304" pingInterval="00:01:00" />
<rewrite> <rewrite>
<rules> <rules>
<!-- Redirect root to /lst/ --> <!-- Proxy all requests starting with /lst/ to the .NET wrapper (port 4000) -->
<rule name="Root Redirect" stopProcessing="true"> <rule name="Proxy to Wrapper" stopProcessing="true">
<match url="^$" />
<action type="Redirect" url="/lst/" redirectType="Permanent" />
</rule>
<!-- Proxy static assets -->
<rule name="Static Assets" stopProcessing="true">
<match url="^lst/assets/(.*)" />
<action type="Rewrite" url="http://localhost:8080/lst/assets/{R:1}" />
</rule>
<!-- Proxy API requests -->
<rule name="API Routes" stopProcessing="true">
<match url="^lst/api/(.*)" />
<action type="Rewrite" url="http://localhost:8080/lst/api/{R:1}" />
</rule>
<!-- Proxy all other requests -->
<rule name="Frontend Routes" stopProcessing="true">
<match url="^lst/(.*)" /> <match url="^lst/(.*)" />
<action type="Rewrite" url="http://localhost:8080/lst/{R:1}" /> <conditions>
<!-- Skip this rule if it's a WebSocket request -->
<add input="{HTTP_UPGRADE}" pattern="^WebSocket$" negate="true" />
</conditions>
<action type="Rewrite" url="http://localhost:8080/{R:1}" />
</rule> </rule>
</rules> </rules>
</rewrite> </rewrite>
<staticContent> <staticContent>
<clear />
<mimeMap fileExtension=".js" mimeType="application/javascript" /> <mimeMap fileExtension=".js" mimeType="application/javascript" />
<mimeMap fileExtension=".mjs" mimeType="application/javascript" /> <mimeMap fileExtension=".mjs" mimeType="application/javascript" />
<mimeMap fileExtension=".css" mimeType="text/css" /> <mimeMap fileExtension=".css" mimeType="text/css" />
@@ -38,6 +26,8 @@
</staticContent> </staticContent>
<handlers> <handlers>
<!-- Let AspNetCoreModule handle all requests -->
<remove name="WebSocketHandler" />
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" /> <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers> </handlers>

View File

@@ -0,0 +1,111 @@
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

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,148 @@
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

@@ -0,0 +1,53 @@
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

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

View File

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

View File

@@ -0,0 +1,83 @@
package channelmgt
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"time"
"github.com/lib/pq"
logging "lst.net/utils/logger"
)
// setup the notifiyer
// -- Only needs to be run once in DB
// CREATE OR REPLACE FUNCTION notify_new_log() RETURNS trigger AS $$
// BEGIN
// PERFORM pg_notify('new_log', row_to_json(NEW)::text);
// RETURN NEW;
// END;
// $$ LANGUAGE plpgsql;
// DROP TRIGGER IF EXISTS new_log_trigger ON logs;
// CREATE TRIGGER new_log_trigger
// AFTER INSERT ON logs
// FOR EACH ROW EXECUTE FUNCTION notify_new_log();
func AllLogs(db *sql.DB, broadcaster chan logging.Message) {
fmt.Println("[AllLogs] started")
log := logging.New()
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"),
)
listener := pq.NewListener(dsn, 10*time.Second, time.Minute, nil)
err := listener.Listen("new_log")
if err != nil {
log.Panic("Failed to LISTEN on new_log", "logger", map[string]interface{}{
"error": err.Error(),
})
}
fmt.Println("Listening for new logs...")
for {
select {
case notify := <-listener.Notify:
if notify != nil {
fmt.Println("New log notification received")
// Unmarshal the JSON payload of the inserted row
var logData map[string]interface{}
if err := json.Unmarshal([]byte(notify.Extra), &logData); err != nil {
log.Error("Failed to unmarshal notification payload", "logger", map[string]interface{}{
"error": err.Error(),
})
continue
}
// Build message to broadcast
msg := logging.Message{
Channel: "logs", // This matches your logs channel name
Data: logData,
}
broadcaster <- msg
//fmt.Printf("[Broadcasting] sending: %+v\n", msg)
}
case <-time.After(90 * time.Second):
go func() {
log.Debug("Re-pinging Postgres LISTEN", "logger", map[string]interface{}{})
listener.Ping()
}()
}
}
}

View File

@@ -0,0 +1,93 @@
package socketio
import (
"log"
"sync"
"github.com/google/uuid"
"github.com/gorilla/websocket"
logging "lst.net/utils/logger"
)
type Client struct {
ClientID uuid.UUID
Conn *websocket.Conn
APIKey string
IPAddress string
UserAgent string
Send chan []byte
Channels map[string]bool // e.g., {"logs": true, "labels": true}
}
var clients = make(map[*Client]bool)
var clientsLock sync.Mutex
func init() {
var broadcast = make(chan string)
go func() {
for {
msg := <-broadcast
clientsLock.Lock()
for client := range clients {
if client.Channels["logs"] {
err := client.Conn.WriteMessage(websocket.TextMessage, []byte(msg))
if err != nil {
log.Println("Write error:", err)
client.Conn.Close()
//client.MarkDisconnected()
delete(clients, client)
}
}
}
clientsLock.Unlock()
}
}()
}
func StartBroadcasting(broadcaster chan logging.Message) {
go func() {
log.Println("StartBroadcasting goroutine started")
for {
msg := <-broadcaster
//log.Printf("Received msg on broadcaster: %+v\n", msg)
clientsLock.Lock()
for client := range clients {
if client.Channels[msg.Channel] {
log.Println("Sending message to client")
err := client.Conn.WriteJSON(msg)
if err != nil {
log.Println("Write error:", err)
client.Conn.Close()
client.MarkDisconnected()
delete(clients, client)
}
} else {
log.Println("Skipping client, channel mismatch")
}
}
clientsLock.Unlock()
}
}()
}
// func (c *Client) JoinChannel(name string) {
// ch := GetOrCreateChannel(name)
// c.Channels[name] = ch
// ch.Register <- c
// }
// func (c *Client) LeaveChannel(name string) {
// if ch, ok := c.Channels[name]; ok {
// ch.Unregister <- c
// delete(c.Channels, name)
// }
// }
func (c *Client) Disconnect() {
// for _, ch := range c.Channels {
// ch.Unregister <- c
// }
close(c.Send)
}

View File

@@ -0,0 +1,163 @@
package socketio
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"lst.net/utils/db"
)
type JoinPayload struct {
Channel string `json:"channel"`
Services []string `json:"services,omitempty"`
APIKey string `json:"apiKey"`
}
// type Channel struct {
// Name string
// Clients map[*Client]bool
// Register chan *Client
// Unregister chan *Client
// Broadcast chan Message
// }
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, // allow all origins; customize for prod
}
func SocketHandler(c *gin.Context) {
// Upgrade HTTP to websocket
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Println("Failed to upgrade:", err)
c.AbortWithStatus(http.StatusBadRequest)
return
}
defer conn.Close()
// Create client struct
client := &Client{
Conn: conn,
IPAddress: c.ClientIP(),
UserAgent: c.Request.UserAgent(),
Channels: make(map[string]bool),
}
clientsLock.Lock()
clients[client] = true
clientsLock.Unlock()
defer func() {
clientsLock.Lock()
delete(clients, client)
clientsLock.Unlock()
client.MarkDisconnected()
client.Disconnect()
conn.Close()
}()
for {
// Read message from client
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("Read error:", err)
clientsLock.Lock()
delete(clients, client)
clientsLock.Unlock()
client.MarkDisconnected()
client.Disconnect()
break
}
var payload JoinPayload
err = json.Unmarshal(msg, &payload)
if err != nil {
log.Println("Invalid JSON payload:", err)
clientsLock.Lock()
delete(clients, client)
clientsLock.Unlock()
client.MarkDisconnected()
client.Disconnect()
continue
}
// Simple API key check (replace with real auth)
if payload.APIKey == "" {
conn.WriteMessage(websocket.TextMessage, []byte("Missing API Key"))
continue
}
client.APIKey = payload.APIKey
// Handle channel subscription, add more here as we get more in.
switch payload.Channel {
case "logs":
client.Channels["logs"] = true
case "logServices":
for _, svc := range payload.Services {
client.Channels["logServices:"+svc] = true
}
case "labels":
client.Channels["labels"] = true
default:
conn.WriteMessage(websocket.TextMessage, []byte("Unknown channel"))
continue
}
// Save client info in DB
client.SaveToDB()
// Confirm subscription
resp := map[string]string{
"status": "subscribed",
"channel": payload.Channel,
}
respJSON, _ := json.Marshal(resp)
conn.WriteMessage(websocket.TextMessage, respJSON)
// You could now start pushing messages to client or keep connection open
// For demo, just wait and keep connection alive
}
}
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 // ✅ Assign the generated UUID back to the client
}
}
func (c *Client) MarkDisconnected() {
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)
}
}

View File

@@ -0,0 +1,25 @@
package socketio
import (
"github.com/gin-gonic/gin"
channelmgt "lst.net/cmd/services/websocket/channelMGT"
"lst.net/utils/db"
logging "lst.net/utils/logger"
)
var broadcaster = make(chan logging.Message) // define broadcaster here so its accessible
func RegisterSocketRoutes(r *gin.Engine) {
sqlDB, err := db.DB.DB()
if err != nil {
panic(err)
}
// channels
go channelmgt.AllLogs(sqlDB, broadcaster)
go StartBroadcasting(broadcaster)
wsGroup := r.Group("/ws")
wsGroup.GET("/connect", SocketHandler)
}

View File

@@ -3,35 +3,59 @@ module lst.net
go 1.24.3 go 1.24.3
require ( require (
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/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/rs/zerolog v1.34.0
github.com/swaggo/swag v1.16.5
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0
) )
require ( require (
github.com/bytedance/sonic v1.11.6 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic v1.13.3 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/gin-contrib/sse v0.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.20.0 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.2 // 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/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // 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.2.7 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // 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-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
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // 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.2.12 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
golang.org/x/arch v0.8.0 // indirect golang.org/x/arch v0.19.0 // indirect
golang.org/x/crypto v0.23.0 // indirect golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/mod v0.26.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/net v0.42.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/sync v0.16.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect golang.org/x/sys v0.34.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
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@@ -1,24 +1,65 @@
// @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"
"fmt" "fmt"
"log"
"net/http" "net/http"
"os" "os"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"lst.net/cmd/services/system/config"
socketio "lst.net/cmd/services/websocket"
_ "lst.net/docs"
"lst.net/utils/db"
logging "lst.net/utils/logger"
) )
func main() { func main() {
log := logging.New()
// Load .env only in dev (not Docker/production) // 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.Println("Warning: .env file not found (ok in Docker/production)") log.Info("Warning: .env file not found (ok in Docker/production)", "system", map[string]interface{}{})
} }
} }
// Initialize DB
if err := db.InitDB(); err != nil {
log.Panic("Database intialize failed", "db", map[string]interface{}{
"error": err.Error(),
"casue": errors.Unwrap(err),
"timeout": "30s",
"details": fmt.Sprintf("%+v", err), // Full stack trace if available
})
}
defer func() {
if r := recover(); r != nil {
sqlDB, _ := db.DB.DB()
sqlDB.Close()
log.Error("Recovered from panic during DB shutdown", "db", map[string]interface{}{
"panic": r,
})
}
}()
// Set basePath dynamically // Set basePath dynamically
basePath := "/" basePath := "/"
@@ -34,6 +75,16 @@ func main() {
gin.SetMode(gin.ReleaseMode) 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 --- // // --- Add Redirects Here ---
// // Redirect root ("/") to "/app" or "/lst/app" // // Redirect root ("/") to "/app" or "/lst/app"
// r.GET("/", func(c *gin.Context) { // r.GET("/", func(c *gin.Context) {
@@ -52,35 +103,26 @@ func main() {
r.StaticFS(basePath+"/app", http.Dir("frontend")) r.StaticFS(basePath+"/app", http.Dir("frontend"))
r.GET(basePath+"/api/ping", func(c *gin.Context) { 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"}) c.JSON(200, gin.H{"message": "pong"})
}) })
//logging.RegisterLoggerRoutes(r, basePath)
socketio.RegisterSocketRoutes(r)
config.RegisterConfigRoutes(r, basePath)
r.Any(basePath+"/api", errorApiLoc) r.Any(basePath+"/api", errorApiLoc)
// // Serve static assets for Vite app // get the server port
// r.Static("/lst/app/assets", "./dist/app/assets") port := "8080"
if os.Getenv("VITE_SERVER_PORT") != "" {
// // Catch-all for Vite app routes port = os.Getenv("VITE_SERVER_PORT")
// r.NoRoute(func(c *gin.Context) { }
// path := c.Request.URL.Path r.Run(":" + port)
// // Don't handle API, assets, or docs
// if strings.HasPrefix(path, "/lst/api") ||
// strings.HasPrefix(path, "/lst/app/assets") ||
// strings.HasPrefix(path, "/lst/docs") {
// c.JSON(404, gin.H{"error": "Not found"})
// return
// }
// // Serve index.html for all /lst/app routes
// if strings.HasPrefix(path, "/lst/app") {
// c.File("./dist/app/index.html")
// return
// }
// c.JSON(404, gin.H{"error": "Not found"})
// })
r.Run(":8080")
} }
// func serveViteApp(c *gin.Context) { // func serveViteApp(c *gin.Context) {
@@ -93,5 +135,11 @@ func main() {
// c.JSON(http.StatusBadRequest, gin.H{"message": "welcome to lst system you might have just encountered an incorrect area of the app"}) // 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) { func errorApiLoc(c *gin.Context) {
log := logging.New()
log.Info("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"}) c.JSON(http.StatusBadRequest, gin.H{"message": "looks like you have encountered an api route that dose not exist"})
} }

View File

@@ -0,0 +1,67 @@
package db
import (
"log"
"time"
"gorm.io/gorm"
)
type Config struct {
gorm.Model
ID uint `gorm:"primaryKey;autoIncrement"`
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 = []Config{
{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 Config
// 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) ([]Config, error) {
var configs []Config
result := db.Find(&configs)
return configs, result.Error
}

43
backend/utils/db/db.go Normal file
View File

@@ -0,0 +1,43 @@
package db
import (
"fmt"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
type JSONB map[string]interface{}
func InitDB() error {
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s",
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"))
var err error
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return fmt.Errorf("failed to connect to database: %v", err)
}
fmt.Println("✅ Connected to database")
// ensures we have the uuid stuff setup properly
DB.Exec(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`)
err = DB.AutoMigrate(&Log{}, &Config{}, &ClientRecord{})
if err != nil {
return fmt.Errorf("failed to auto-migrate models: %v", err)
}
fmt.Println("✅ Database migration completed successfully")
return nil
}

48
backend/utils/db/logs.go Normal file
View File

@@ -0,0 +1,48 @@
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

@@ -0,0 +1,20 @@
package db
import (
"time"
"github.com/google/uuid"
)
type ClientRecord struct {
ClientID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4();primaryKey" json:"id"`
APIKey string `gorm:"not null"`
IPAddress string `gorm:"not null"`
UserAgent string `gorm:"size:255"`
ConnectedAt time.Time `gorm:"index"`
LastHeartbeat time.Time `gorm:"index"`
Channels JSONB `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
DisconnectedAt *time.Time `gorm:"column:disconnected_at"`
}

View File

@@ -0,0 +1,116 @@
package logging
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
}
type Message struct {
Channel string `json:"channel"`
Data interface{} `json:"data"`
}
// 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

@@ -7,9 +7,20 @@ services:
no_cache: true no_cache: true
image: git.tuffraid.net/cowch/logistics_support_tool:latest image: git.tuffraid.net/cowch/logistics_support_tool:latest
container_name: lst_backend container_name: lst_backend
networks:
- docker-network
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USER: username
DB_PASSWORD: passwordl
DB_NAME: lst
volumes: volumes:
- /path/to/backend/data:/data - /path/to/backend/data:/data
ports: ports:
- "8080:8080" - "8080:8080"
restart: unless-stopped restart: unless-stopped
pull_policy: never pull_policy: never
networks:
docker-network:
external: true

View File

@@ -1,35 +1,41 @@
import { useState } from 'react' import { useState } from "react";
import reactLogo from './assets/react.svg' import reactLogo from "./assets/react.svg";
import viteLogo from '/vite.svg' import viteLogo from "/vite.svg";
import './App.css' import "./App.css";
import WebSocketViewer from "./WebSocketTest";
function App() { function App() {
const [count, setCount] = useState(0) const [count, setCount] = useState(0);
return ( return (
<> <>
<div> <div>
<a href="https://vite.dev" target="_blank"> <a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" /> <img src={viteLogo} className="logo" alt="Vite logo" />
</a> </a>
<a href="https://react.dev" target="_blank"> <a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" /> <img
</a> src={reactLogo}
</div> className="logo react"
<h1>Vite + React</h1> alt="React logo"
<div className="card"> />
<button onClick={() => setCount((count) => count + 1)}> </a>
count is {count} </div>
</button> <h1>Vite + React</h1>
<p> <div className="card">
Edit <code>src/App.tsx</code> and save to test HMR <button onClick={() => setCount((count) => count + 1)}>
</p> count is {count}
</div> </button>
<p className="read-the-docs"> <p>
Click on the Vite and React logos to learn more Edit <code>src/App.tsx</code> and save to test HMR
</p> </p>
</> </div>
) <p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
<WebSocketViewer />
</>
);
} }
export default App export default App;

View File

@@ -0,0 +1,41 @@
import { useEffect, useRef } from "react";
const WebSocketViewer = () => {
const ws = useRef<any>(null);
useEffect(() => {
// Connect to your Go backend WebSocket endpoint
ws.current = new WebSocket(
(window.location.protocol === "https:" ? "wss://" : "ws://") +
window.location.host +
"/lst/api/logger/logs"
);
ws.current.onopen = () => {
console.log("[WebSocket] Connected");
};
ws.current.onmessage = (event: any) => {
console.log("[WebSocket] Message received:", event.data);
};
ws.current.onerror = (error: any) => {
console.error("[WebSocket] Error:", error);
};
ws.current.onclose = () => {
console.log("[WebSocket] Disconnected");
};
// Cleanup on unmount
return () => {
if (ws.current) {
ws.current.close();
}
};
}, []);
return <div>Check the console for WebSocket messages</div>;
};
export default WebSocketViewer;

View File

@@ -26,10 +26,10 @@ if (Test-Path $envFile) {
Write-Host ".env file not found at $envFile" Write-Host ".env file not found at $envFile"
} }
# if (-not $env:BUILD_NAME) { if (-not $env:BUILD_NAME) {
# Write-Warning "BUILD_NAME environment variable is not set. Please make sure you have entered the correct info the env" Write-Warning "BUILD_NAME environment variable is not set. Please make sure you have entered the correct info the env"
# exit 1 exit 1
# } }
function Get-PackageVersion { function Get-PackageVersion {
param ( param (
@@ -78,7 +78,7 @@ function Update-BuildNumber {
$name = $matches[2] $name = $matches[2]
$newNumber = $number + 1 $newNumber = $number + 1
$newBuildNumber = "$newNumber" $newBuildNumber = "$($newNumber)-$($name)"
Set-Content -Path $buildNumberFile -Value $newBuildNumber Set-Content -Path $buildNumberFile -Value $newBuildNumber
@@ -87,14 +87,17 @@ function Update-BuildNumber {
return $newBuildNumber return $newBuildNumber
} else { } else {
Write-Warning "BUILD_NUMBER file content '$current' is not in the expected 'number-name' format." Write-Warning "BUILD_NUMBER file content '$current' is not in the expected 'number-name' format."
Set-Content -Path $buildNumberFile -Value "1-"$($env:BUILD_NAME)
return $null return $null
} }
} }
Push-Location $rootDir/backend Push-Location $rootDir/backend
Write-Host "Building the app" Write-Host "Building the app"
go get go get
# swag init -o swagger -g main.go
go build -ldflags "-X main.version=$($version)-$($initialBuildValue)" -o lst_app.exe ./main.go go build -ldflags "-X main.version=$($version)-$($initialBuildValue)" -o lst_app.exe ./main.go
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Warning "app build failed!" Write-Warning "app build failed!"
@@ -122,6 +125,22 @@ function Update-BuildNumber {
Write-Host "Building wrapper" Write-Host "Building wrapper"
Push-Location $rootDir/LstWrapper Push-Location $rootDir/LstWrapper
Write-Host "Changing the port to match the server port in the env file"
$port = $env:VITE_SERVER_PORT
if (-not $port) {
$port = "8080" # Default port if env var not set
}
$webConfigPath = "web.config"
$content = Get-Content -Path $webConfigPath -Raw
$newContent = $content -replace '(?<=Rewrite" url="http://localhost:)\d+(?=/\{R:1\}")', $port
$newContent | Set-Content -Path $webConfigPath -NoNewline
Write-Host "Updated web.config rewrite port to $port"
#remove the publish folder as we done need it #remove the publish folder as we done need it
if (-not (Test-Path "publish")) { if (-not (Test-Path "publish")) {
Write-Host "The publish folder is already deleted nothing else to do" Write-Host "The publish folder is already deleted nothing else to do"
@@ -131,6 +150,15 @@ function Update-BuildNumber {
dotnet publish -c Release -o ./publish dotnet publish -c Release -o ./publish
$webConfigPath = "web.config"
$content = Get-Content -Path $webConfigPath -Raw
$newContent = $content -replace '(?<=Rewrite" url="http://localhost:)\d+(?=/\{R:1\}")', "8080"
$newContent | Set-Content -Path $webConfigPath -NoNewline
Write-Host "Updated web.config rewrite port back to 8080"
Pop-Location Pop-Location
Write-Host "Building Docs" Write-Host "Building Docs"

View File

@@ -82,6 +82,7 @@ $filesToCopy = @(
@{ Source = "package.json"; Destination = "package.json" }, @{ Source = "package.json"; Destination = "package.json" },
@{ Source = "CHANGELOG.md"; Destination = "CHANGELOG.md" }, @{ Source = "CHANGELOG.md"; Destination = "CHANGELOG.md" },
@{ Source = "README.md"; Destination = "README.md" }, @{ Source = "README.md"; Destination = "README.md" },
@{ Source = ".env-example"; Destination = ".env-example" },
# scripts to be copied over # scripts to be copied over
@{ Source = "scripts\tmp"; Destination = "tmp" } @{ Source = "scripts\tmp"; Destination = "tmp" }
@{ Source = "scripts\iisControls.ps1"; Destination = "scripts\iisControls.ps1" } @{ Source = "scripts\iisControls.ps1"; Destination = "scripts\iisControls.ps1" }

View File

@@ -13,3 +13,6 @@ docker push git.tuffraid.net/cowch/logistics_support_tool:latest
Write-Host "Pull the new images to our docker system" Write-Host "Pull the new images to our docker system"
docker compose -f ./docker-compose.yml up -d --force-recreate docker compose -f ./docker-compose.yml up -d --force-recreate
# in case we get logged out docker login git.tuffraid.net
# create a docker network so we have this for us docker network create -d bridge my-bridge-network

View File

@@ -138,6 +138,7 @@ $plantFunness = {
Write-Host "Stopping iis application" Write-Host "Stopping iis application"
Stop-WebAppPool -Name LogisticsSupportTool #-ErrorAction Stop Stop-WebAppPool -Name LogisticsSupportTool #-ErrorAction Stop
Start-Sleep -Seconds 3
###################################################################################### ######################################################################################