61 Commits

Author SHA1 Message Date
c7af1901aa feat(app state): settings are now global and get updated on the fly 2025-08-05 12:40:34 -05:00
2473bfa702 feat(app): changes to dyanmically load systems based on settings 2025-08-05 12:40:11 -05:00
4dd842b3b8 docs(.env-example): updated to include discord info 2025-08-04 21:25:39 -05:00
89ef04cc6f feat(discord): added in a way to get panic messages that would crash the server only or fatal 2025-08-04 21:23:06 -05:00
3cec883356 Merge branch 'main' of https://git.tuffraid.net/cowch/logistics_support_tool 2025-08-04 06:54:26 -05:00
0ecbe29ec1 refactor(app): moved all db and log to one intialize spot 2025-08-04 06:54:21 -05:00
188331c1ad ci(backend): fixed go.sum to have all correct pkg 2025-08-03 09:22:57 -05:00
486e4fb6b8 test(ws): testing ws connection on the frontend 2025-08-01 15:54:52 -05:00
c775bb3354 refactor(correction to folder sturcture): before we got to deep resturctures to best pactice folder 2025-08-01 15:54:20 -05:00
b6968b7b67 chore(release): v0.0.1-alpha.6 2025-07-30 19:39:25 -05:00
a0aa75c5a0 refactor(settings): changed config to settings and added in the update method for this as well
strict fields on the updates so we can only change what we want in here
2025-07-30 19:35:13 -05:00
78be07c8bb ci(hotreload): added in air for hot reloading 2025-07-30 19:31:02 -05:00
0575a34422 feat(settings): migrated all settings endpoints confirmed as well for updates 2025-07-29 20:27:45 -05:00
3bc3801ffb refactor(config): changed to settings to match the other lst in node. makes it more easy to manage 2025-07-29 20:13:35 -05:00
4368111311 fix(websocket): errors in saving client info during ping ping 2025-07-29 20:13:05 -05:00
daf9e8a966 perf(websocket): added in base url to help with ssl stuff and iis 2025-07-29 18:07:10 -05:00
8a08d3eac6 refactor(wrapper): removed the logger stuff so we dont fill up space 2025-07-29 18:06:27 -05:00
a761a3634b fix(wrapper): corrections to properly handle websockets :D 2025-07-29 15:58:21 -05:00
a1a30cffd1 refactor(ws): ws logging and channel manager added no auth currently 2025-07-29 11:29:59 -05:00
6a631be909 docs(docker): docs about the custom network for the db is seperated 2025-07-25 12:14:51 -05:00
75c17d2065 test(iis): wrapper test for ws 2025-07-25 12:14:05 -05:00
63c053b38c docs(wss): more ws stuff 2025-07-25 12:13:47 -05:00
5bcbdaf3d0 feat(ws server): added in a websocket on port system to help with better logging 2025-07-25 12:13:19 -05:00
074032f20d refactor(app port): changed to have the port be dyncamic on the iis side
docker will default to 8080 and can be adjusted via the docker compose, or passing the same env over
it will change it as well.
2025-07-23 07:36:18 -05:00
13e282e815 fix(update server): fixed to make sure everything is stopped before doing the remaining update 2025-07-22 20:00:43 -05:00
6c8ac33be7 refactor(createzip): added in env-example to the zip file 2025-07-22 19:59:55 -05:00
92ce51eb7c refactor(build): added back in the build name stuff 2025-07-22 19:59:29 -05:00
52ef39fd5c feat(logging): added in db and logging with websocket 2025-07-22 19:59:06 -05:00
623e19f028 refactor(docker compose example): added in postgress stuff plus network 2025-07-22 19:58:26 -05:00
14dd87e335 docs(.env example): added postrgres example 2025-07-22 19:58:01 -05:00
52956ecaa4 docs(dockerbuild): comments as a reminder for my seld 2025-07-22 07:03:29 -05:00
cc51807819 chore(release): v0.0.1-alpha.5 2025-07-21 17:29:21 -05:00
8e039037a9 feat(services): just a 1 to 1 from lstv2 2025-07-21 17:27:09 -05:00
ed40f4c77e refactor(lstv2build): changes to not always build the old app if we dont need too 2025-07-21 17:26:50 -05:00
ccddef8ba8 docs(iiscontrol): added in an example how to run i t 2025-07-21 17:26:18 -05:00
05e31f4b9e feat(update server): new update server added with iis stop and old version included 2025-07-21 17:25:51 -05:00
d2578b8850 refactor(createzip): changes to the way the app looks for better understanding 2025-07-21 17:14:21 -05:00
392a9ef407 refactor(build): changes to remove the build name as it was not really realvent
we will soon include it in the main file if anyone else starts to build with us
2025-07-21 17:13:24 -05:00
2d2337257f refactor(env-example): changes to the env example to include the dev stuff 2025-07-21 17:10:51 -05:00
5a95bf3ef0 test(docker): changes to make all latest now instead of 2 apps 2025-07-20 08:57:55 -05:00
7e9401d8bb test(app): added production into the build so we dont fill logs up 2025-07-20 08:57:31 -05:00
0c4465c91a refactor(createzip): renamed the backend to app now that everything is in one 2025-07-20 08:57:08 -05:00
5f9d49561a docs(env): changes to have more clear info on the example doc 2025-07-20 08:56:34 -05:00
253d998b68 refactor(wrapper): changes to handle docs and frontned now 2025-07-19 11:38:47 -05:00
9acfd1ccd0 refactor(docker): removed frontend from being built 2025-07-19 11:38:29 -05:00
39d23f4a8a refactor(frontend): changes to no longer use server side and only static files 2025-07-19 11:38:11 -05:00
f41a1b3363 feat(env): new env introduced to handle dev and prodution 2025-07-19 11:37:35 -05:00
1e649d1f23 refactor(backend): changes to convert the backend to strictly the app 2025-07-19 11:37:13 -05:00
5cc1fbe919 ci(wrapper): changes to clean the publish folder 2025-07-18 10:28:16 -05:00
528d7af031 fix(builds): added in the gets/installs for our go project and node portons 2025-07-17 10:46:53 -05:00
f0bcea0405 perf(docs): changes to stop the server from opening a browser when it started up 2025-07-16 21:56:59 -05:00
67b5976176 feat(backend): env added 2025-07-16 21:56:17 -05:00
4ef2d90aa8 refactor(config): changes to autoformat go files 2025-07-16 21:55:33 -05:00
7539b1653d refactor(lstv2): moved the loc to .env file 2025-07-16 20:24:01 -05:00
6072afc8c0 build(docs): added building the docs into the build script 2025-07-16 20:23:25 -05:00
0fb2ec5273 fix(docs): added a copy script to cp the build to the backend with ps1 2025-07-16 20:22:43 -05:00
fdf14b06c8 fix(backend): ignored docs this should be built before running by the user 2025-07-16 20:22:15 -05:00
a23b6a6e9e feat(env-example): added in an example env 2025-07-16 20:21:42 -05:00
5a9636cdc1 feat(backend): set the static path to the docs 2025-07-16 19:55:55 -05:00
3fdcc110e3 feat(docs): added in the new docs to build alongside the backend 2025-07-16 19:55:27 -05:00
452bdbedb4 refactor(createzip): added in verbage to be clear what was done at the end 2025-07-16 10:17:02 -05:00
114 changed files with 23789 additions and 8751 deletions

35
.env-example Normal file
View File

@@ -0,0 +1,35 @@
# uncomment this out to run in productions
# APP_ENV=production
# Server port that will allow vite to talk to the backend.
VITE_SERVER_PORT=4000
# lstv2 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
# Gitea Info
GITEA_URL=git repo
GITEA_USERNAME=username
GITEA_REPO=logistics_support_tool
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_FOLDER=C\drive\loc
ADMUSER=username
ADMPASSWORD=password
# Build number info
BUILD_NAME=leBlfRaj

5
.gitignore vendored
View File

@@ -8,6 +8,9 @@ LstWrapper/bin
LstWrapper/publish LstWrapper/publish
LstWrapper/obj LstWrapper/obj
scripts/tmp scripts/tmp
backend/docs
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:
@@ -189,4 +192,6 @@ 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
backend_bad_practice

11
.vscode/settings.json vendored
View File

@@ -24,5 +24,12 @@
}, },
"[handlebars]": { "[handlebars]": {
"editor.formatOnSave": true "editor.formatOnSave": true
} },
} "[go]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "golang.go"
},
// Optional: Configure goimports instead of gofmt
"go.formatTool": "goimports"
}

View File

@@ -3,6 +3,105 @@
All notable changes to LST will be documented in this file. All notable changes to LST will be documented in this file.
## [0.0.1-alpha.6](https://git.tuffraid.net/cowch/logistics_support_tool/compare/v0.0.1-alpha.5...v0.0.1-alpha.6) (2025-07-31)
### 🌟 Enhancements
* **logging:** added in db and logging with websocket ([52ef39f](https://git.tuffraid.net/cowch/logistics_support_tool/commit/52ef39fd5c129ed02ed9f38dbf7e49ae06807ad6))
* **settings:** migrated all settings endpoints confirmed as well for updates ([0575a34](https://git.tuffraid.net/cowch/logistics_support_tool/commit/0575a344229ba0ff5c0f47781c6d596e5c08e5eb))
* **ws server:** added in a websocket on port system to help with better logging ([5bcbdaf](https://git.tuffraid.net/cowch/logistics_support_tool/commit/5bcbdaf3d0e889729d4dce3df51f4330d7793868))
### 🐛 Bug fixes
* **update server:** fixed to make sure everything is stopped before doing the remaining update ([13e282e](https://git.tuffraid.net/cowch/logistics_support_tool/commit/13e282e815c1c95a0a5298ede2f6497cdf036440))
* **websocket:** errors in saving client info during ping ping ([4368111](https://git.tuffraid.net/cowch/logistics_support_tool/commit/4368111311c48e73a11a6b24febdcc3be31a2a59))
* **wrapper:** corrections to properly handle websockets :D ([a761a36](https://git.tuffraid.net/cowch/logistics_support_tool/commit/a761a3634b6cb0aeeb571dd634bd158cee530779))
### 📚 Documentation
* **.env example:** added postrgres example ([14dd87e](https://git.tuffraid.net/cowch/logistics_support_tool/commit/14dd87e335a63d76d64c07a15cf593cb286a9833))
* **dockerbuild:** comments as a reminder for my seld ([52956ec](https://git.tuffraid.net/cowch/logistics_support_tool/commit/52956ecaa45cd556ba7832d6cb9ec2cf883d983a))
* **docker:** docs about the custom network for the db is seperated ([6a631be](https://git.tuffraid.net/cowch/logistics_support_tool/commit/6a631be909b56a899af393510edffd70d7901a7a))
* **wss:** more ws stuff ([63c053b](https://git.tuffraid.net/cowch/logistics_support_tool/commit/63c053b38ce3ab3c3a94cda620da930f4e8615bd))
### 🛠️ Code Refactor
* **app port:** changed to have the port be dyncamic on the iis side ([074032f](https://git.tuffraid.net/cowch/logistics_support_tool/commit/074032f20dc90810416c5899e44fefe86b52f98a))
* **build:** added back in the build name stuff ([92ce51e](https://git.tuffraid.net/cowch/logistics_support_tool/commit/92ce51eb7cf14ebb599c29fea4721e21badafbf6))
* **config:** changed to settings to match the other lst in node. makes it more easy to manage ([3bc3801](https://git.tuffraid.net/cowch/logistics_support_tool/commit/3bc3801ffbb544a814d52c72e566e8d4866a7f38))
* **createzip:** added in env-example to the zip file ([6c8ac33](https://git.tuffraid.net/cowch/logistics_support_tool/commit/6c8ac33be73f203137b883e33feb625ccc0945e9))
* **docker compose example:** added in postgress stuff plus network ([623e19f](https://git.tuffraid.net/cowch/logistics_support_tool/commit/623e19f028d27fbfc46bee567ce78169cddba8fb))
* **settings:** changed config to settings and added in the update method for this as well ([a0aa75c](https://git.tuffraid.net/cowch/logistics_support_tool/commit/a0aa75c5a0b4a6e3a10b88bbcccf43d096e532b4))
* **wrapper:** removed the logger stuff so we dont fill up space ([8a08d3e](https://git.tuffraid.net/cowch/logistics_support_tool/commit/8a08d3eac6540b00ff23115936d56b4f22f16d53))
* **ws:** ws logging and channel manager added no auth currently ([a1a30cf](https://git.tuffraid.net/cowch/logistics_support_tool/commit/a1a30cffd18e02e1061959fa3164f8237522880c))
### 🚀 Performance
* **websocket:** added in base url to help with ssl stuff and iis ([daf9e8a](https://git.tuffraid.net/cowch/logistics_support_tool/commit/daf9e8a966fd440723b1aec932a02873a5e27eb7))
### 📝 Testing Code
* **iis:** wrapper test for ws ([75c17d2](https://git.tuffraid.net/cowch/logistics_support_tool/commit/75c17d20659dcc5a762e00928709c4d3dd277284))
### 📈 Project changes
* **hotreload:** added in air for hot reloading ([78be07c](https://git.tuffraid.net/cowch/logistics_support_tool/commit/78be07c8bbf5acbcdac65351f693941f47be4cb5))
## [0.0.1-alpha.5](https://git.tuffraid.net/cowch/logistics_support_tool/compare/v0.0.1-alpha.4...v0.0.1-alpha.5) (2025-07-21)
### 🌟 Enhancements
* **backend:** env added ([67b5976](https://git.tuffraid.net/cowch/logistics_support_tool/commit/67b59761769350951bc6b52ef715b592b5d4a862))
* **backend:** set the static path to the docs ([5a9636c](https://git.tuffraid.net/cowch/logistics_support_tool/commit/5a9636cdc15f164ed3547f544e34858683b38241))
* **docs:** added in the new docs to build alongside the backend ([3fdcc11](https://git.tuffraid.net/cowch/logistics_support_tool/commit/3fdcc110e3b4d7356af1fb025070bdf7413a8e88))
* **env-example:** added in an example env ([a23b6a6](https://git.tuffraid.net/cowch/logistics_support_tool/commit/a23b6a6e9eef4dbeb2f84c325ca8dca178ab3ff3))
* **env:** new env introduced to handle dev and prodution ([f41a1b3](https://git.tuffraid.net/cowch/logistics_support_tool/commit/f41a1b336389d6100e42681f53d9f618c8726f25))
* **services:** just a 1 to 1 from lstv2 ([8e03903](https://git.tuffraid.net/cowch/logistics_support_tool/commit/8e039037a9b40994b9e77f59680a3ce1b6ebc3a0))
* **update server:** new update server added with iis stop and old version included ([05e31f4](https://git.tuffraid.net/cowch/logistics_support_tool/commit/05e31f4b9e20799257da244b237420fa2b6435f8))
### 🐛 Bug fixes
* **backend:** ignored docs this should be built before running by the user ([fdf14b0](https://git.tuffraid.net/cowch/logistics_support_tool/commit/fdf14b06c88d3057f31184e03fb592bd9a959847))
* **builds:** added in the gets/installs for our go project and node portons ([528d7af](https://git.tuffraid.net/cowch/logistics_support_tool/commit/528d7af0312cb3de43ddc93d8af22bde6aadea52))
* **docs:** added a copy script to cp the build to the backend with ps1 ([0fb2ec5](https://git.tuffraid.net/cowch/logistics_support_tool/commit/0fb2ec52739def8294a57bbc6c497ba6531568a6))
### 📚 Documentation
* **env:** changes to have more clear info on the example doc ([5f9d495](https://git.tuffraid.net/cowch/logistics_support_tool/commit/5f9d49561a7c22a4d2cb85bd06bbbdb9fa952224))
* **iiscontrol:** added in an example how to run i t ([ccddef8](https://git.tuffraid.net/cowch/logistics_support_tool/commit/ccddef8ba8f64774db6d99fba3ea7c7c54bea1a5))
### 🛠️ Code Refactor
* **backend:** changes to convert the backend to strictly the app ([1e649d1](https://git.tuffraid.net/cowch/logistics_support_tool/commit/1e649d1f23c404252754d746254810386bb0f233))
* **build:** changes to remove the build name as it was not really realvent ([392a9ef](https://git.tuffraid.net/cowch/logistics_support_tool/commit/392a9ef407d4e64f573cbfc9109c8a81f55c14d5))
* **config:** changes to autoformat go files ([4ef2d90](https://git.tuffraid.net/cowch/logistics_support_tool/commit/4ef2d90aa8595d5e3d18a289c012320bdf0dcc4a))
* **createzip:** added in verbage to be clear what was done at the end ([452bdbe](https://git.tuffraid.net/cowch/logistics_support_tool/commit/452bdbedb48cde7fa0ef246fb61e304127c49e58))
* **createzip:** changes to the way the app looks for better understanding ([d2578b8](https://git.tuffraid.net/cowch/logistics_support_tool/commit/d2578b885029ca98b750f4c6996e567053b2e517))
* **createzip:** renamed the backend to app now that everything is in one ([0c4465c](https://git.tuffraid.net/cowch/logistics_support_tool/commit/0c4465c91a40cbea73048617952df26b476d01f4))
* **docker:** removed frontend from being built ([9acfd1c](https://git.tuffraid.net/cowch/logistics_support_tool/commit/9acfd1ccd0b9019d5acc01153ddad15b82b6c74e))
* **env-example:** changes to the env example to include the dev stuff ([2d23372](https://git.tuffraid.net/cowch/logistics_support_tool/commit/2d2337257f2b31c740f9eb7064be010528e14c7c))
* **frontend:** changes to no longer use server side and only static files ([39d23f4](https://git.tuffraid.net/cowch/logistics_support_tool/commit/39d23f4a8a8af6b1acb87b913e5cd1929fe144e4))
* **lstv2build:** changes to not always build the old app if we dont need too ([ed40f4c](https://git.tuffraid.net/cowch/logistics_support_tool/commit/ed40f4c77e9d81e36292f38c342e9c2b062f84b9))
* **lstv2:** moved the loc to .env file ([7539b16](https://git.tuffraid.net/cowch/logistics_support_tool/commit/7539b1653d7a48dbe248847ed321ab065e58efa0))
* **wrapper:** changes to handle docs and frontned now ([253d998](https://git.tuffraid.net/cowch/logistics_support_tool/commit/253d998b68b5808d6fd2d9731255616238fcdb71))
### 🚀 Performance
* **docs:** changes to stop the server from opening a browser when it started up ([f0bcea0](https://git.tuffraid.net/cowch/logistics_support_tool/commit/f0bcea0405db364e1e15471b5db74fc9d8f93788))
### 📝 Testing Code
* **app:** added production into the build so we dont fill logs up ([7e9401d](https://git.tuffraid.net/cowch/logistics_support_tool/commit/7e9401d8bb12589de6ce1517e4502d784788ab0f))
* **docker:** changes to make all latest now instead of 2 apps ([5a95bf3](https://git.tuffraid.net/cowch/logistics_support_tool/commit/5a95bf3ef0f4d4b125f8e777d57cc96b3d3a894d))
### 📈 Project changes
* **wrapper:** changes to clean the publish folder ([5cc1fbe](https://git.tuffraid.net/cowch/logistics_support_tool/commit/5cc1fbe919a8c3fa46617f7b9ed2830559b8978f))
### 📈 Project Builds
* **docs:** added building the docs into the build script ([6072afc](https://git.tuffraid.net/cowch/logistics_support_tool/commit/6072afc8c01a4fe4980e0e17cfe41d5a9cd524ef))
## [0.0.1-alpha.4](https://git.tuffraid.net/cowch/logistics_support_tool/compare/v0.0.1-alpha.3...v0.0.1-alpha.4) (2025-07-16) ## [0.0.1-alpha.4](https://git.tuffraid.net/cowch/logistics_support_tool/compare/v0.0.1-alpha.3...v0.0.1-alpha.4) (2025-07-16)
### 📈 Project changes ### 📈 Project changes

View File

@@ -1,47 +1,96 @@
using System;
using System.IO;
using System.Net;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.Net.Http;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// to build the binary dotnet publish -c Release -o ./publish
// Go backend
builder.Services.AddHttpClient("GoBackend", client => builder.Services.AddHttpClient("GoBackend", client =>
{ {
client.BaseAddress = new Uri("http://localhost:8080"); client.BaseAddress = new Uri("http://localhost:8080");
client.Timeout = TimeSpan.FromSeconds(30);
});
// Node frontend
builder.Services.AddHttpClient("NodeFrontend", client =>
{
client.BaseAddress = new Uri("http://localhost:3000");
client.Timeout = TimeSpan.FromSeconds(30);
}); });
var app = builder.Build(); var app = builder.Build();
app.UseStaticFiles(); // Enable WebSocket support
app.UseWebSockets();
app.Use((Func<HttpContext, Func<Task>, Task>)(async (context, next) => // Logging method
void LogToFile(string message)
{ {
var clientFactory = context.RequestServices.GetRequiredService<IHttpClientFactory>(); try
{
string logDir = Path.Combine(AppContext.BaseDirectory, "logs");
Directory.CreateDirectory(logDir);
string logFilePath = Path.Combine(logDir, "proxy_log.txt");
File.AppendAllText(logFilePath, $"{DateTime.UtcNow}: {message}{Environment.NewLine}");
}
catch (Exception ex)
{
// Handle potential errors writing to log file
Console.WriteLine($"Logging error: {ex.Message}");
}
}
var isApiRequest = // Middleware to handle WebSocket requests
context.Request.Path.StartsWithSegments("/api") || app.Use(async (context, next) =>
context.Request.Path.StartsWithSegments("/graphql") || {
context.Request.Path.StartsWithSegments("/auth") || if (context.WebSockets.IsWebSocketRequest && context.Request.Path.StartsWithSegments("/ws"))
!context.Request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase); {
// LogToFile($"WebSocket request received for path: {context.Request.Path}");
var client = clientFactory.CreateClient(isApiRequest ? "GoBackend" : "NodeFrontend"); try
{
var backendUri = new UriBuilder("ws", "localhost", 8080)
{
Path = context.Request.Path,
Query = context.Request.QueryString.ToString()
}.Uri;
using var backendSocket = new ClientWebSocket();
await backendSocket.ConnectAsync(backendUri, context.RequestAborted);
using var frontendSocket = await context.WebSockets.AcceptWebSocketAsync();
var cts = new CancellationTokenSource();
// WebSocket forwarding tasks
var forwardToBackend = ForwardWebSocketAsync(frontendSocket, backendSocket, cts.Token);
var forwardToFrontend = ForwardWebSocketAsync(backendSocket, frontendSocket, cts.Token);
await Task.WhenAny(forwardToBackend, forwardToFrontend);
cts.Cancel();
}
catch (Exception ex)
{
//LogToFile($"WebSocket proxy error: {ex.Message}");
context.Response.StatusCode = (int)HttpStatusCode.BadGateway;
await context.Response.WriteAsync($"WebSocket proxy error: {ex.Message}");
}
}
else
{
await next();
}
});
// Middleware to handle HTTP requests
app.Use(async (context, next) =>
{
if (context.WebSockets.IsWebSocketRequest)
{
await next();
return;
}
var client = context.RequestServices.GetRequiredService<IHttpClientFactory>().CreateClient("GoBackend");
try try
{ {
var requestUri = context.Request.Path + context.Request.QueryString; var request = new HttpRequestMessage(new HttpMethod(context.Request.Method),
context.Request.Path + context.Request.QueryString);
var request = new HttpRequestMessage(
new HttpMethod(context.Request.Method),
requestUri);
foreach (var header in context.Request.Headers) foreach (var header in context.Request.Headers)
{ {
@@ -52,13 +101,12 @@ app.Use((Func<HttpContext, Func<Task>, Task>)(async (context, next) =>
} }
} }
if (context.Request.ContentLength > 0 || context.Request.Headers.ContainsKey("Transfer-Encoding")) 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, HttpCompletionOption.ResponseHeadersRead, context.RequestAborted); 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)
@@ -72,14 +120,39 @@ app.Use((Func<HttpContext, Func<Task>, Task>)(async (context, next) =>
} }
context.Response.Headers.Remove("transfer-encoding"); context.Response.Headers.Remove("transfer-encoding");
await response.Content.CopyToAsync(context.Response.Body); await response.Content.CopyToAsync(context.Response.Body);
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)
{ {
context.Response.StatusCode = isApiRequest ? 503 : 502; LogToFile($"HTTP proxy error: {ex.Message}");
await context.Response.WriteAsync($"{(isApiRequest ? "Go API" : "Frontend")} unavailable: {ex.Message}"); 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 ex)
{
LogToFile($"WebSocket forwarding error: {ex.Message}");
await destination.CloseOutputAsync(WebSocketCloseStatus.InternalServerError, "Error", cancellationToken);
}
}
app.Run(); app.Run();

View File

@@ -1,15 +1,36 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<location path="." inheritInChildApplications="false"> <system.webServer>
<system.webServer> <!-- Enable WebSockets -->
<handlers> <webSocket enabled="true" receiveBufferLimit="4194304" pingInterval="00:01:00" />
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers> <rewrite>
<aspNetCore processPath="dotnet" <rules>
arguments=".\LstWrapper.dll" <!-- Proxy all requests starting with /lst/ to the .NET wrapper (port 4000) -->
stdoutLogEnabled="true" <rule name="Proxy to Wrapper" stopProcessing="true">
stdoutLogFile=".\logs\stdout" <match url="^lst/(.*)" />
hostingModel="inprocess" /> <conditions>
</system.webServer> <!-- Skip this rule if it's a WebSocket request -->
</location> <add input="{HTTP_UPGRADE}" pattern="^WebSocket$" negate="true" />
</configuration> </conditions>
<action type="Rewrite" url="http://localhost:8080/{R:1}" />
</rule>
</rules>
</rewrite>
<staticContent>
<mimeMap fileExtension=".js" mimeType="application/javascript" />
<mimeMap fileExtension=".mjs" mimeType="application/javascript" />
<mimeMap fileExtension=".css" mimeType="text/css" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
</staticContent>
<handlers>
<!-- Let AspNetCoreModule handle all requests -->
<remove name="WebSocketHandler" />
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="dotnet" arguments=".\LstWrapper.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
</system.webServer>
</configuration>

View File

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

0
backend/.air.toml Normal file
View File

2
backend/.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
lst_backend.exe
lst.net.exe

View File

@@ -3,19 +3,25 @@ FROM golang:1.24.4-alpine3.22 AS builder
WORKDIR /app WORKDIR /app
COPY go.mod go.sum ./ COPY go.mod go.sum ./
# COPY VERSION ./VERSION COPY docs /app/docs/
COPY frontend /app/frontend/
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o lst_go ./main.go RUN CGO_ENABLED=0 GOOS=linux go build -o lst_go ./main.go
FROM alpine:latest FROM alpine:latest
WORKDIR /root/ WORKDIR /root/
# Copy only the binary (no need for source files)
RUN mkdir -p ./docs ./frontend
COPY --from=builder /app/lst_go . COPY --from=builder /app/lst_go .
# COPY --from=builder /app/VERSION ./ COPY --from=builder /app/docs ./docs/
COPY --from=builder /app/frontend ./frontend/
# create the volume paths # create the volume paths
RUN mkdir -p /data RUN mkdir -p /data

View File

@@ -2,33 +2,52 @@ module lst.net
go 1.24.3 go 1.24.3
require github.com/gin-gonic/gin v1.10.1 require (
github.com/bensch777/discord-webhook-golang v0.0.6
github.com/gin-contrib/cors v1.7.6
github.com/gin-gonic/gin v1.10.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/rs/zerolog v1.34.0
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.1
)
require ( require (
github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic v1.13.3 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect github.com/cloudwego/iasm v0.2.0 // 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-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.26.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.5 // indirect
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/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.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.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.18.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/net v0.41.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/text v0.15.0 // indirect golang.org/x/sys v0.34.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect golang.org/x/text v0.27.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
) )

51
backend/internal/db/db.go Normal file
View File

@@ -0,0 +1,51 @@
package db
import (
"fmt"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"lst.net/internal/models"
)
var DB *gorm.DB
type DBConfig struct {
DB *gorm.DB
DSN string
}
func InitDB() (*DBConfig, 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 nil, 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(&models.Log{}, &models.Settings{}) // &ClientRecord{}, &Servers{}
if err != nil {
return nil, fmt.Errorf("failed to auto-migrate models: %v", err)
}
fmt.Println("✅ Database migration completed successfully")
return &DBConfig{
DB: DB,
DSN: dsn,
}, nil
}

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

@@ -0,0 +1,21 @@
package models
import (
"time"
"github.com/google/uuid"
"lst.net/pkg"
)
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:"column:last_heartbeat"`
Channels pkg.JSONB `gorm:"type:jsonb"`
CreatedAt time.Time
UpdatedAt time.Time
DisconnectedAt *time.Time `gorm:"column:disconnected_at"`
}

View File

@@ -0,0 +1,179 @@
package ws
import (
"encoding/json"
"log"
"strings"
"sync"
"lst.net/pkg/logger"
)
type Channel struct {
Name string
Clients map[*Client]bool
Register chan *Client
Unregister chan *Client
Broadcast chan []byte
lock sync.RWMutex
}
var (
channels = make(map[string]*Channel)
channelsMu sync.RWMutex
)
// InitializeChannels creates and returns all channels
func InitializeChannels() {
channelsMu.Lock()
defer channelsMu.Unlock()
channels["logServices"] = NewChannel("logServices")
channels["labels"] = NewChannel("labels")
// Add more channels here as needed
}
func NewChannel(name string) *Channel {
return &Channel{
Name: name,
Clients: make(map[*Client]bool),
Register: make(chan *Client),
Unregister: make(chan *Client),
Broadcast: make(chan []byte),
}
}
func GetChannel(name string) (*Channel, bool) {
channelsMu.RLock()
defer channelsMu.RUnlock()
ch, exists := channels[name]
return ch, exists
}
func GetAllChannels() map[string]*Channel {
channelsMu.RLock()
defer channelsMu.RUnlock()
chs := make(map[string]*Channel)
for k, v := range channels {
chs[k] = v
}
return chs
}
func StartAllChannels() {
channelsMu.RLock()
defer channelsMu.RUnlock()
for _, ch := range channels {
go ch.RunChannel()
}
}
func CleanupChannels() {
channelsMu.Lock()
defer channelsMu.Unlock()
for _, ch := range channels {
close(ch.Broadcast)
// Add any other cleanup needed
}
channels = make(map[string]*Channel)
}
func StartBroadcasting(broadcaster chan logger.Message, channels map[string]*Channel) {
logger := logger.New()
go func() {
for msg := range broadcaster {
switch msg.Channel {
case "logServices":
// Just forward the message - filtering happens in RunChannel()
messageBytes, err := json.Marshal(msg)
if err != nil {
logger.Error("Error marshaling message", "websocket", map[string]interface{}{
"errors": err,
})
continue
}
channels["logServices"].Broadcast <- messageBytes
case "labels":
// Future labels handling
messageBytes, err := json.Marshal(msg)
if err != nil {
logger.Error("Error marshaling message", "websocket", map[string]interface{}{
"errors": err,
})
continue
}
channels["labels"].Broadcast <- messageBytes
default:
log.Printf("Received message for unknown channel: %s", msg.Channel)
}
}
}()
}
func contains(slice []string, item string) bool {
// Empty filter slice means "match all"
if len(slice) == 0 {
return true
}
// Case-insensitive comparison
item = strings.ToLower(item)
for _, s := range slice {
if strings.ToLower(s) == item {
return true
}
}
return false
}
// Updated Channel.RunChannel() for logServices filtering
func (ch *Channel) RunChannel() {
for {
select {
case client := <-ch.Register:
ch.lock.Lock()
ch.Clients[client] = true
ch.lock.Unlock()
case client := <-ch.Unregister:
ch.lock.Lock()
delete(ch.Clients, client)
ch.lock.Unlock()
case message := <-ch.Broadcast:
var msg logger.Message
if err := json.Unmarshal(message, &msg); err != nil {
continue
}
ch.lock.RLock()
for client := range ch.Clients {
// Special filtering for logServices
if ch.Name == "logServices" {
logLevel, _ := msg.Meta["level"].(string)
logService, _ := msg.Meta["service"].(string)
levelMatch := len(client.LogLevels) == 0 || contains(client.LogLevels, logLevel)
serviceMatch := len(client.Services) == 0 || contains(client.Services, logService)
if !levelMatch || !serviceMatch {
continue
}
}
select {
case client.Send <- message:
default:
ch.Unregister <- client
}
}
ch.lock.RUnlock()
}
}
}

View File

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

View File

@@ -0,0 +1,229 @@
package ws
import (
"encoding/json"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"lst.net/pkg/logger"
)
type JoinPayload struct {
Channel string `json:"channel"`
APIKey string `json:"apiKey"`
Services []string `json:"services,omitempty"`
Levels []string `json:"levels,omitempty"`
Labels []string `json:"labels,omitempty"`
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, // allow all origins; customize for prod
HandshakeTimeout: 15 * time.Second,
ReadBufferSize: 1024,
WriteBufferSize: 1024,
EnableCompression: true,
}
func SocketHandler(c *gin.Context, channels map[string]*Channel, log *logger.CustomLogger, db *gorm.DB) {
// Upgrade HTTP to WebSocket
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Error("WebSocket upgrade failed", "websocket", map[string]interface{}{"error": err})
return
}
//defer conn.Close()
// Create new client
client := &Client{
Conn: conn,
APIKey: "exampleAPIKey",
Send: make(chan []byte, 256), // Buffered channel
Channels: make(map[string]bool),
IPAddress: c.ClientIP(),
UserAgent: c.Request.UserAgent(),
done: make(chan struct{}),
}
client.isAlive.Store(true)
// Add to global clients map
clientsMu.Lock()
clients[client] = true
clientsMu.Unlock()
// Save initial connection to DB
client.SaveToDB(log, db)
// Save initial connection to DB
// if err := client.SaveToDB(); err != nil {
// log.Println("Failed to save client to DB:", err)
// conn.Close()
// return
// }
// Set handlers
conn.SetPingHandler(func(string) error {
return nil // Auto-responds with pong
})
conn.SetPongHandler(func(string) error {
now := time.Now()
client.markActive() // Track last pong time
client.lastActive = now
client.updateHeartbeat(log, db)
return nil
})
// Start server-side ping ticker
go client.startServerPings(log, db)
defer func() {
// Unregister from all channels
for channelName := range client.Channels {
if ch, exists := channels[channelName]; exists {
ch.Unregister <- client
}
}
// Remove from global clients map
clientsMu.Lock()
delete(clients, client)
clientsMu.Unlock()
// Mark disconnected in DB
client.MarkDisconnected(log, db)
// Close connection
conn.Close()
log.Info("Client disconnected", "websocket", map[string]interface{}{
"client": client.ClientID,
})
}()
// Send welcome message immediately
welcomeMsg := map[string]string{
"status": "connected",
"message": "Welcome to the WebSocket server. Send subscription request to begin.",
}
if err := conn.WriteJSON(welcomeMsg); err != nil {
log.Error("Failed to send welcome message", "websocket", map[string]interface{}{"error": err})
return
}
// Message handling goroutine
go func() {
defer func() {
// Cleanup on disconnect
for channelName := range client.Channels {
if ch, exists := channels[channelName]; exists {
ch.Unregister <- client
}
}
close(client.Send)
client.MarkDisconnected(log, db)
}()
for {
_, msg, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
log.Error("Client disconnected unexpectedl", "websocket", map[string]interface{}{"error": err})
}
break
}
var payload struct {
Channel string `json:"channel"`
APIKey string `json:"apiKey"`
Services []string `json:"services,omitempty"`
Levels []string `json:"levels,omitempty"`
Labels []string `json:"labels,omitempty"`
}
if err := json.Unmarshal(msg, &payload); err != nil {
conn.WriteJSON(map[string]string{"error": "invalid payload format"})
continue
}
// Validate API key (implement your own validateAPIKey function)
// if payload.APIKey == "" || !validateAPIKey(payload.APIKey) {
// conn.WriteJSON(map[string]string{"error": "invalid or missing API key"})
// continue
// }
if payload.APIKey == "" {
conn.WriteMessage(websocket.TextMessage, []byte("Missing API Key"))
continue
}
client.APIKey = payload.APIKey
// Handle channel subscription
switch payload.Channel {
case "logServices":
// Unregister from other channels if needed
if client.Channels["labels"] {
channels["labels"].Unregister <- client
delete(client.Channels, "labels")
}
// Update client filters
client.Services = payload.Services
client.LogLevels = payload.Levels
// Register to channel
channels["logServices"].Register <- client
client.Channels["logServices"] = true
conn.WriteJSON(map[string]string{
"message": "You are now subscribed to the the service channel",
"status": "subscribed",
"channel": "logServices",
})
case "labels":
// Unregister from other channels if needed
if client.Channels["logServices"] {
channels["logServices"].Unregister <- client
delete(client.Channels, "logServices")
}
// Set label filters if provided
if payload.Labels != nil {
client.Labels = payload.Labels
}
// Register to channel
channels["labels"].Register <- client
client.Channels["labels"] = true
// Update DB record
client.SaveToDB(log, db)
// if err := client.SaveToDB(); err != nil {
// log.Println("Failed to update client labels:", err)
// }
conn.WriteJSON(map[string]interface{}{
"message": "You are now subscribed to the label channel",
"status": "subscribed",
"channel": "labels",
"filters": client.Labels,
})
default:
conn.WriteJSON(map[string]string{
"error": "invalid channel",
"available_channels": "logServices, labels",
})
}
}
}()
// Send messages to client
for message := range client.Send {
if err := conn.WriteMessage(websocket.TextMessage, message); err != nil {
log.Error("Write erro", "websocket", map[string]interface{}{"error": err})
break
}
}
}

View File

@@ -0,0 +1,79 @@
package ws
// 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();
import (
"encoding/json"
"fmt"
"os"
"time"
"github.com/lib/pq"
"lst.net/pkg/logger"
)
func LogServices(broadcaster chan logger.Message, log *logger.CustomLogger) {
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",
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(),
})
}
log.Info("Listening for all logs through single logServices channel...", "wbsocker", map[string]interface{}{})
for {
select {
case notify := <-listener.Notify:
if notify != nil {
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
}
// Always send to logServices channel
broadcaster <- logger.Message{
Channel: "logServices",
Data: logData,
Meta: map[string]interface{}{
"level": logData["level"],
"service": logData["service"],
},
}
}
case <-time.After(90 * time.Second):
go func() {
listener.Ping()
}()
}
}
}

View File

@@ -0,0 +1,56 @@
package ws
import (
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"lst.net/pkg/logger"
)
var (
broadcaster = make(chan logger.Message)
)
func RegisterSocketRoutes(r *gin.Engine, base_url string, log *logger.CustomLogger, db *gorm.DB) {
// Initialize all channels
InitializeChannels()
// Start channel processors
StartAllChannels()
// Start background services
go LogServices(broadcaster, log)
go StartBroadcasting(broadcaster, channels)
// WebSocket route
r.GET(base_url+"/ws", func(c *gin.Context) {
SocketHandler(c, channels, log, db)
})
r.GET(base_url+"/ws/clients", AdminAuthMiddleware(), handleGetClients)
}
func handleGetClients(c *gin.Context) {
channel := c.Query("channel")
var clientList []*Client
if channel != "" {
clientList = GetClientsByChannel(channel)
} else {
clientList = GetAllClients()
}
c.JSON(http.StatusOK, gin.H{
"count": len(clientList),
"clients": clientList,
})
}
func AdminAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// Implement your admin authentication logic
// Example: Check API key or JWT token
c.Next()
}
}

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
package servers
import (
"reflect"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"lst.net/internal/models"
"lst.net/pkg/logger"
)
func getServers(c *gin.Context, log *logger.CustomLogger, db *gorm.DB) {
servers, err := GetServers(log, db)
log.Info("Current Settings", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
})
if err != nil {
log.Error("Current Settings", "system", map[string]interface{}{
"endpoint": "/api/v1/settings",
"client_ip": c.ClientIP(),
"user_agent": c.Request.UserAgent(),
"error": err,
})
c.JSON(500, gin.H{"message": "There was an error getting the settings", "error": err})
return
}
c.JSON(200, gin.H{"message": "Current settings", "data": servers})
}
func GetServers(log *logger.CustomLogger, db *gorm.DB) ([]map[string]interface{}, error) {
var servers []models.Servers
res := db.Find(&servers)
if res.Error != nil {
return nil, res.Error
}
toLowercase := func(s models.Servers) map[string]interface{} {
t := reflect.TypeOf(s)
v := reflect.ValueOf(s)
data := make(map[string]interface{})
for i := 0; i < t.NumField(); i++ {
field := strings.ToLower(t.Field(i).Name)
data[field] = v.Field(i).Interface()
}
return data
}
var lowercaseServers []map[string]interface{}
for _, server := range servers {
lowercaseServers = append(lowercaseServers, toLowercase(server))
}
return lowercaseServers, nil
}

View File

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

View File

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

View File

@@ -0,0 +1,59 @@
package servers
// import (
// "encoding/json"
// "github.com/gin-gonic/gin"
// "lst.net/internal/db"
// "lst.net/pkg/logger"
// )
// func updateSettingById(c *gin.Context) {
// log := logger.New()
// settingID := c.Param("id")
// if settingID == "" {
// c.JSON(500, gin.H{"message": "Invalid data"})
// log.Error("Invalid data", "system", map[string]interface{}{
// "endpoint": "/api/v1/settings",
// "client_ip": c.ClientIP(),
// "user_agent": c.Request.UserAgent(),
// })
// return
// }
// var setting SettingUpdateInput
// //err := c.ShouldBindBodyWithJSON(&setting)
// decoder := json.NewDecoder(c.Request.Body) // more strict and will force us to have correct data
// decoder.DisallowUnknownFields()
// if err := decoder.Decode(&setting); err != nil {
// c.JSON(400, gin.H{"message": "Invalid request body", "error": err.Error()})
// log.Error("Invalid request body", "system", map[string]interface{}{
// "endpoint": "/api/v1/settings",
// "client_ip": c.ClientIP(),
// "user_agent": c.Request.UserAgent(),
// "error": err,
// })
// return
// }
// if err := UpdateServer(db.DB, settingID, setting); err != nil {
// c.JSON(500, gin.H{"message": "Failed to update setting", "error": err.Error()})
// log.Error("Failed to update setting", "system", map[string]interface{}{
// "endpoint": "/api/v1/settings",
// "client_ip": c.ClientIP(),
// "user_agent": c.Request.UserAgent(),
// "error": err,
// })
// return
// }
// c.JSON(200, gin.H{"message": "Setting was just updated", "data": setting})
// }
// func UpdateServer() (string, error) {
// return "Server was just updated", nil
// }

View File

@@ -0,0 +1,39 @@
package settings
import (
"gorm.io/gorm"
)
func GetAllSettings(db *gorm.DB) ([]map[string]interface{}, error) {
// var settings []models.Settings
// result := db.Find(&settings)
// if result.Error != nil {
// return nil, result.Error
// }
// // Function to convert struct to map with lowercase keys
// toLowercase := func(s models.Settings) map[string]interface{} {
// t := reflect.TypeOf(s)
// v := reflect.ValueOf(s)
// data := make(map[string]interface{})
// for i := 0; i < t.NumField(); i++ {
// field := strings.ToLower(t.Field(i).Name)
// data[field] = v.Field(i).Interface()
// }
// return data
// }
// // Convert each struct in settings slice to a map with lowercase keys
// var lowercaseSettings []map[string]interface{}
// for _, setting := range settings {
// lowercaseSettings = append(lowercaseSettings, toLowercase(setting))
// }
convertedSettings := GetMap()
return convertedSettings, nil
}

View File

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

View File

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

View File

@@ -0,0 +1,128 @@
package settings
import (
"errors"
"fmt"
"gorm.io/gorm"
"lst.net/internal/models"
"lst.net/pkg/logger"
)
var seedConfigData = []models.Settings{
{Name: "serverPort", Description: "The port the server will listen on if not running in docker", Value: "4000", Enabled: true, AppService: "server"},
{Name: "server", Description: "The server we will use when connecting to the alplaprod sql", Value: "usmcd1vms006", Enabled: true, AppService: "server"},
{Name: "timezone", Value: "America/Chicago", Description: "What time zone is the server in this is used for cronjobs and some other time stuff", AppService: "server", Enabled: true},
{Name: "dbUser", Value: "alplaprod", Description: "What is the db userName", AppService: "server", Enabled: true},
{Name: "dbPass", Value: "b2JlbGl4", Description: "What is the db password", AppService: "server", Enabled: true},
{Name: "tcpPort", Value: "2222", Description: "TCP port for printers to connect send data and the zedra cameras", AppService: "server", Enabled: true},
{Name: "prolinkCheck", Value: "1", Description: "Will prolink be considered to check if matches, maninly used in plants that do not fully utilize prolink + ocp", AppService: "production", Enabled: true},
{Name: "bookin", Value: "1", Description: "do we want to book in after a label is printed", AppService: "ocp", Enabled: true},
{Name: "dbServer", Value: "usmcd1vms036", Description: "What server is the prod db on?", AppService: "server", Enabled: true},
{Name: "printDelay", Value: "90", Description: "How long in seconds between prints", AppService: "ocp", Enabled: true},
{Name: "plantToken", Value: "test3", Description: "What is the plant token", AppService: "server", Enabled: true},
{Name: "dualPrinting", Value: "0", Description: "Dose the plant have 2 machines that go to 1?", AppService: "ocp", Enabled: true},
{Name: "ocmeService", Value: "0", Description: "Is the ocme service enabled. this is gernerally only for Dayton.", AppService: "ocme", Enabled: true},
{Name: "fifoCheck", Value: "45", Description: "How far back do we want to check for fifo default 45, putting 0 will ignore.", AppService: "ocme", Enabled: true},
{Name: "dayCheck", Value: "3", Description: "how many days +/- to check for shipments in alplaprod", AppService: "ocme", Enabled: true},
{Name: "maxLotPerTruck", Value: "3", Description: "How mant lots can we have per truck?", AppService: "ocme", Enabled: true},
{Name: "monitorAddress", Value: "8", Description: "What address is monitored to be limited to the amount of lots that can be added to a truck.", AppService: "ocme", Enabled: true},
{Name: "ocmeCycleCount", Value: "1", Description: "Are we allowing ocme cycle counts?", AppService: "ocme", Enabled: true},
{Name: "devDir", Value: "", Description: "This is the dev dir and strictly only for updating the servers.", AppService: "server", Enabled: true},
{Name: "demandMGTActivated", Value: "0", Description: "Do we allow for new fake edi?", AppService: "logistics", Enabled: true},
{Name: "qualityRequest", Value: "0", Description: "quality request module?", AppService: "quality", Enabled: true},
{Name: "ocpLogsCheck", Value: "4", Description: "How long do we want to allow logs to show that have not been cleared?", AppService: "ocp", Enabled: true},
{Name: "inhouseDelivery", Value: "0", Description: "Are we doing auto inhouse delivery?", AppService: "ocp", Enabled: true},
// dyco settings
{Name: "dycoConnect", Value: "0", Description: "Are we running the dyco system?", AppService: "dycp", Enabled: true},
{Name: "dycoPrint", Value: "0", Description: "Are we using the dyco to get the labels or the rfid?", AppService: "dyco", Enabled: true},
{Name: "strapperCheck", Value: "1", Description: "Are we monitoring the strapper for faults?", AppService: "dyco", Enabled: true},
// ocp
{Name: "ocpActive", Value: `1`, Description: "Are we pritning on demand?", AppService: "ocp", Enabled: true},
{Name: "ocpCycleDelay", Value: `10`, Description: "How long between printer cycles do we want to monitor.", AppService: "ocp", Enabled: true},
{Name: "pNgAddress", Value: `139`, Description: "What is the address for p&g so we can make sure we have the correct fake edi forcast going in.", AppService: "logisitcs", Enabled: true},
{Name: "scannerID", Value: `500`, Description: "What scanner id will we be using for the app", AppService: "logistics", Enabled: true},
{Name: "scannerPort", Value: `50002`, Description: "What port instance will we be using?", AppService: "logistics", Enabled: true},
{Name: "stagingReturnLocations", Value: `30125,31523`, Description: "What are the staging location IDs we will use to select from. seperated by commas", AppService: "logistics", Enabled: true},
{Name: "testingApiFunction", Value: `1`, Description: "This is a test to validate if we set to 0 it will actaully not allow the route", AppService: "logistics", Enabled: true},
}
func SeedSettings(db *gorm.DB, log *logger.CustomLogger) error {
for _, cfg := range seedConfigData {
var existing models.Settings
if err := db.Unscoped().Where("name = ?", cfg.Name).First(&existing).Error; err == nil {
if existing.DeletedAt.Valid {
// Undelete by setting DeletedAt to NULL
if err := db.Unscoped().Model(&existing).Update("DeletedAt", gorm.DeletedAt{}).Error; err != nil {
log.Error("Failed to undelete settings", "settings", map[string]interface{}{
"name": cfg.Name,
"error": err,
})
return nil
}
}
if errors.Is(err, gorm.ErrRecordNotFound) {
if err := db.Create(&cfg).Error; err != nil {
log.Error("Failed to seed settings", "settings", map[string]interface{}{
"name": cfg.Name,
"error": err,
})
}
}
// // Try to find config by unique Name
// result := db.Where("Name =?", cfg.Name).First(&existing)
// if result.Error != nil {
// if result.Error == gorm.ErrRecordNotFound && cfg.Enabled {
// // not here lets add it
// if err := db.Create(&cfg).Error; err != nil && !existing.DeletedAt.Valid {
// log.Error("Failed to seed settings", "settings", map[string]interface{}{
// "name": cfg.Name,
// "error": err,
// })
// }
// //log.Printf("Seeded new config: %s", cfg.Name)
// } else {
// // Some other error
// return result.Error
// }
} else {
// remove the setting if we change to false this will help with future proofing our seeder in the event we need to add it back
if cfg.Enabled {
existing.Description = cfg.Description
existing.Name = cfg.Name
existing.AppService = cfg.AppService
if err := db.Save(&existing).Error; err != nil {
log.Error("Failed to update ettings.", "settings", map[string]interface{}{
"name": cfg.Name,
"error": err,
})
return err
}
} else {
// we delete the setting so its no longer there
if err := db.Delete(&existing).Error; err != nil {
log.Error("Failed to delete ettings.", "settings", map[string]interface{}{
"name": cfg.Name,
"error": err,
})
return err
}
settingDelete := fmt.Sprintf("Updated existing config: %s", cfg.Name)
log.Info(settingDelete, "settings", map[string]interface{}{})
}
//log.Printf("Updated existing config: %s", cfg.Name)
}
}
log.Info("All settings added or updated.", "settings", map[string]interface{}{})
return nil
}

View File

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

View File

@@ -0,0 +1,56 @@
package settings
import (
"gorm.io/gorm"
"lst.net/internal/models"
"lst.net/pkg/logger"
)
func UpdateSetting(log *logger.CustomLogger, db *gorm.DB, id string, input SettingUpdateInput) error {
var cfg models.Settings
if err := db.Where("setting_id =?", id).First(&cfg).Error; err != nil {
return err
}
updates := map[string]interface{}{}
if input.Description != nil {
updates["description"] = *input.Description
}
if input.Value != nil {
updates["value"] = *input.Value
}
if input.Enabled != nil {
updates["enabled"] = *input.Enabled
}
if input.AppService != nil {
updates["app_service"] = *input.AppService
}
if len(updates) == 0 {
return nil // nothing to update
}
settingUpdate := db.Model(&cfg).Updates(updates)
if settingUpdate.Error != nil {
log.Error("There was an error updating the setting", "settings", map[string]interface{}{
"error": settingUpdate.Error,
})
return settingUpdate.Error
}
if err := Refresh(); err != nil {
log.Error("There was an error refreshing the settings after a setting update", "settings", map[string]interface{}{
"error": err,
})
}
log.Info("The setting was just updated", "settings", map[string]interface{}{
"id": id,
"name": cfg.Name,
"updated": updates,
})
return nil
}

View File

@@ -1,29 +1,79 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"net/http" "os"
"github.com/gin-gonic/gin" "github.com/joho/godotenv"
"lst.net/internal/db"
"lst.net/internal/router"
"lst.net/internal/system/settings"
"lst.net/pkg/logger"
) )
func main() { func main() {
fmt.Println("Welcome to lst backend where all the fun happens.") log := logger.New()
r := gin.Default() if os.Getenv("RUNNING_IN_DOCKER") != "true" {
err := godotenv.Load("../.env")
if err != nil {
log := logger.New()
log.Info("Warning: .env file not found (ok in Docker/production)", "system", map[string]interface{}{})
}
}
// Initialize DB
if _, err := db.InitDB(); err != nil {
r.GET("/api/ping", func(c *gin.Context) { log.Panic("Database intialize failed, please check the server asap.", "db", map[string]interface{}{
c.JSON(200, gin.H{"message": "pong"}) "error": err.Error(),
}) "cause": errors.Unwrap(err),
"timeout": "30s",
"details": fmt.Sprintf("%+v", err), // Full stack trace if available
})
}
defer func() {
if r := recover(); r != nil {
r.Any("/api", errorApiLoc) sqlDB, _ := db.DB.DB()
r.Any("/", errorLoc) sqlDB.Close()
r.Run(":8080") log.Error("Recovered from panic during DB shutdown", "db", map[string]interface{}{
} "panic": r,
})
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) { if err := settings.Initialize(db.DB); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"message": "looks like you have encountered an api route that dose not exist"}) 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
basePath := "/"
if os.Getenv("APP_ENV") != "production" {
basePath = "/lst" // Dev only
}
log.Info("Welcome to lst backend where all the fun happens.", "system", map[string]interface{}{})
// Init Gin router and pass DB to services
r := router.Setup(db.DB, basePath, log)
// get the server port
port := "8080"
if os.Getenv("VITE_SERVER_PORT") != "" {
port = os.Getenv("VITE_SERVER_PORT")
}
if err := r.Run(":" + port); err != nil {
log.Panic("Server failed to start", "system", map[string]interface{}{
"error": err,
})
}
} }

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

@@ -0,0 +1,117 @@
package logger
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
type CustomLogger struct {
consoleLogger zerolog.Logger
}
type Message struct {
Channel string `json:"channel"`
Data map[string]interface{} `json:"data"`
Meta map[string]interface{} `json:"meta,omitempty"`
}
// 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 := 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 := 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")
}
CreateDiscordMsg(message)
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,27 +1,26 @@
--- ---
services: services:
lst_backend: lst_backend:
# build: . # Tell Docker Compose to build the image using the Dockerfile in the current directory
build: build:
context: . context: .
dockerfile: ./backend/Dockerfile dockerfile: ./backend/Dockerfile
image: git.tuffraid.net/cowch/logistics_support_tool:backend-latest no_cache: true
container_name: lst_backend # A friendly name for your running container image: git.tuffraid.net/cowch/logistics_support_tool:latest
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
lst_frontend: networks:
# build: . # Tell Docker Compose to build the image using the Dockerfile in the current directory docker-network:
build: external: true
context: .
dockerfile: ./frontend/Dockerfile
image: git.tuffraid.net/cowch/logistics_support_tool:frontend-latest
container_name: lst_frontend # A friendly name for your running container
volumes:
- /path/to/frontend/data:/data
ports:
- "3120:3000"
restart: unless-stopped

24
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,29 +0,0 @@
# Build Stage
FROM node:24-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm install
# Build the Next.js app
FROM node:24-alpine AS builder
WORKDIR /app
COPY . ./
COPY --from=deps /app/node_modules ./node_modules
# Run other commands like prisma or drizzle
RUN npm run build
# if more commands are needed after here do the same
# Final stage
FROM node:24-alpine
WORKDIR /app
COPY --from=builder /app/.nitro /app/.nitro
COPY --from=builder /app/.output /app/.output
COPY --from=builder /app/.tanstack /app/.tanstack
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

69
frontend/README.md Normal file
View File

@@ -0,0 +1,69 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
...tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
...tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
...tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

23
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,27 +1,29 @@
{ {
"name": "frontend", "name": "frontend",
"version": "1.0.0", "private": true,
"description": "", "version": "0.0.0",
"main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite",
"build": "vite build" "build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
}, },
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": { "dependencies": {
"@tanstack/react-router": "^1.127.3",
"@tanstack/react-start": "^1.127.4",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0"
"vite": "^7.0.4"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.30.1",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"typescript": "^5.8.3", "@vitejs/plugin-react-swc": "^3.10.2",
"vite-tsconfig-paths": "^5.1.4" "eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4"
} }
} }

1
frontend/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

42
frontend/src/App.css Normal file
View File

@@ -0,0 +1,42 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

41
frontend/src/App.tsx Normal file
View File

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

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

68
frontend/src/index.css Normal file
View File

@@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

10
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -1,59 +0,0 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

View File

@@ -1,19 +0,0 @@
// src/router.tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
export function createRouter() {
const router = createTanStackRouter({
basepath: '/lst',
routeTree,
scrollRestoration: true,
})
return router
}
declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
}
}

View File

@@ -1,48 +0,0 @@
/// <reference types="vite/client" />
import type { ReactNode } from 'react'
import {
Outlet,
createRootRoute,
HeadContent,
Scripts,
} from '@tanstack/react-router'
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'TanStack Start Starter',
},
],
}),
component: RootComponent,
})
function RootComponent() {
return (
<RootDocument>
<Outlet />
</RootDocument>
)
}
function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
)
}

View File

@@ -1,51 +0,0 @@
// src/routes/index.tsx
import * as fs from "node:fs";
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
const filePath = "count.txt";
async function readCount() {
return parseInt(
await fs.promises.readFile(filePath, "utf-8").catch(() => "0")
);
}
const getCount = createServerFn({
method: "GET",
}).handler(() => {
return readCount();
});
const updateCount = createServerFn({ method: "POST" })
.validator((d: number) => d)
.handler(async ({ data }) => {
const count = await readCount();
await fs.promises.writeFile(filePath, `${count + data}`);
});
export const Route = createFileRoute("/")({
component: Home,
loader: async () => await getCount(),
});
function Home() {
const router = useRouter();
const state = Route.useLoaderData();
return (
<div>
<p>This is just something to put in here</p>
<button
type="button"
onClick={() => {
updateCount({ data: 1 }).then(() => {
router.invalidate();
});
}}
>
Add 1 to {state}?
</button>
</div>
);
}

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -1,10 +1,7 @@
{ {
"compilerOptions": { "files": [],
"jsx": "react-jsx", "references": [
"moduleResolution": "Bundler", { "path": "./tsconfig.app.json" },
"module": "ESNext", { "path": "./tsconfig.node.json" }
"target": "ES2022", ]
"skipLibCheck": true, }
"strictNullChecks": true
}
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,11 +1,40 @@
import { defineConfig } from 'vite' import { defineConfig } from "vite";
import tsConfigPaths from 'vite-tsconfig-paths' import react from "@vitejs/plugin-react-swc";
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
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/
export default defineConfig({ export default defineConfig({
base: '/lst/', plugins: [react()],
server: { base: "/lst/app/",
port: 3000, build: {
}, outDir: "../backend/frontend",
plugins: [tsConfigPaths(), tanstackStart({ target: 'node-server' })], assetsDir: "assets",
}) 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/, ""),
},
},
},
});

20
lst-docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

41
lst-docs/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Website
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
## Installation
```bash
yarn
```
## Local Development
```bash
yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
## Build
```bash
yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
## Deployment
Using SSH:
```bash
USE_SSH=true yarn deploy
```
Not using SSH:
```bash
GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

View File

@@ -0,0 +1,12 @@
---
slug: first-blog-post
title: First Blog Post
authors: [slorber, yangshun]
tags: [hola, docusaurus]
---
Lorem ipsum dolor sit amet...
<!-- truncate -->
...consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

View File

@@ -0,0 +1,44 @@
---
slug: long-blog-post
title: Long Blog Post
authors: yangshun
tags: [hello, docusaurus]
---
This is the summary of a very long blog post,
Use a `<!--` `truncate` `-->` comment to limit blog post size in the list view.
<!-- truncate -->
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

View File

@@ -0,0 +1,24 @@
---
slug: mdx-blog-post
title: MDX Blog Post
authors: [slorber]
tags: [docusaurus]
---
Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).
:::tip
Use the power of React to create interactive blog posts.
:::
{/* truncate */}
For example, use JSX to create an interactive button:
```js
<button onClick={() => alert('button clicked!')}>Click me!</button>
```
<button onClick={() => alert('button clicked!')}>Click me!</button>

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -0,0 +1,29 @@
---
slug: welcome
title: Welcome
authors: [slorber, yangshun]
tags: [facebook, hello, docusaurus]
---
[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).
Here are a few tips you might find useful.
<!-- truncate -->
Simply add Markdown files (or folders) to the `blog` directory.
Regular blog authors can be added to `authors.yml`.
The blog post date can be extracted from filenames, such as:
- `2019-05-30-welcome.md`
- `2019-05-30-welcome/index.md`
A blog post folder can be convenient to co-locate blog post images:
![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg)
The blog supports tags as well!
**And if you don't want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config.

25
lst-docs/blog/authors.yml Normal file
View File

@@ -0,0 +1,25 @@
yangshun:
name: Yangshun Tay
title: Ex-Meta Staff Engineer, Co-founder GreatFrontEnd
url: https://linkedin.com/in/yangshun
image_url: https://github.com/yangshun.png
page: true
socials:
x: yangshunz
linkedin: yangshun
github: yangshun
newsletter: https://www.greatfrontend.com
slorber:
name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
page:
# customize the url of the author page at /blog/authors/<permalink>
permalink: '/all-sebastien-lorber-articles'
socials:
x: sebastienlorber
linkedin: sebastienlorber
github: slorber
newsletter: https://thisweekinreact.com

19
lst-docs/blog/tags.yml Normal file
View File

@@ -0,0 +1,19 @@
facebook:
label: Facebook
permalink: /facebook
description: Facebook tag description
hello:
label: Hello
permalink: /hello
description: Hello tag description
docusaurus:
label: Docusaurus
permalink: /docusaurus
description: Docusaurus tag description
hola:
label: Hola
permalink: /hola
description: Hola tag description

2
lst-docs/copy.ps1 Normal file
View File

@@ -0,0 +1,2 @@
Write-Host "Copy files to the backend"
robocopy build ..\backend\docs /E /NFL /NDL

47
lst-docs/docs/intro.md Normal file
View File

@@ -0,0 +1,47 @@
---
sidebar_position: 1
---
# Tutorial Intro
Let's discover **Docusaurus in less than 5 minutes**.
## Getting Started
Get started by **creating a new site**.
Or **try Docusaurus immediately** with **[docusaurus.new](https://docusaurus.new)**.
### What you'll need
- [Node.js](https://nodejs.org/en/download/) version 18.0 or above:
- When installing Node.js, you are recommended to check all checkboxes related to dependencies.
## Generate a new site
Generate a new Docusaurus site using the **classic template**.
The classic template will automatically be added to your project after you run the command:
```bash
npm init docusaurus@latest my-website classic
```
You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor.
The command also installs all necessary dependencies you need to run Docusaurus.
## Start your site
Run the development server:
```bash
cd my-website
npm run start
```
The `cd` command changes the directory you're working with. In order to work with your newly created Docusaurus site, you'll need to navigate the terminal there.
The `npm run start` command builds your website locally and serves it through a development server, ready for you to view at http://localhost:3000/.
Open `docs/intro.md` (this page) and edit some lines: the site **reloads automatically** and displays your changes.

View File

@@ -0,0 +1,8 @@
{
"label": "Tutorial - Basics",
"position": 2,
"link": {
"type": "generated-index",
"description": "5 minutes to learn the most important Docusaurus concepts."
}
}

View File

@@ -0,0 +1,23 @@
---
sidebar_position: 6
---
# Congratulations!
You have just learned the **basics of Docusaurus** and made some changes to the **initial template**.
Docusaurus has **much more to offer**!
Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**.
Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610)
## What's next?
- Read the [official documentation](https://docusaurus.io/)
- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config)
- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration)
- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout)
- Add a [search bar](https://docusaurus.io/docs/search)
- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase)
- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support)

View File

@@ -0,0 +1,34 @@
---
sidebar_position: 3
---
# Create a Blog Post
Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed...
## Create your first Post
Create a file at `blog/2021-02-28-greetings.md`:
```md title="blog/2021-02-28-greetings.md"
---
slug: greetings
title: Greetings!
authors:
- name: Joel Marcey
title: Co-creator of Docusaurus 1
url: https://github.com/JoelMarcey
image_url: https://github.com/JoelMarcey.png
- name: Sébastien Lorber
title: Docusaurus maintainer
url: https://sebastienlorber.com
image_url: https://github.com/slorber.png
tags: [greetings]
---
Congratulations, you have made your first post!
Feel free to play around and edit this post as much as you like.
```
A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings).

View File

@@ -0,0 +1,57 @@
---
sidebar_position: 2
---
# Create a Document
Documents are **groups of pages** connected through:
- a **sidebar**
- **previous/next navigation**
- **versioning**
## Create your first Doc
Create a Markdown file at `docs/hello.md`:
```md title="docs/hello.md"
# Hello
This is my **first Docusaurus document**!
```
A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello).
## Configure the Sidebar
Docusaurus automatically **creates a sidebar** from the `docs` folder.
Add metadata to customize the sidebar label and position:
```md title="docs/hello.md" {1-4}
---
sidebar_label: 'Hi!'
sidebar_position: 3
---
# Hello
This is my **first Docusaurus document**!
```
It is also possible to create your sidebar explicitly in `sidebars.js`:
```js title="sidebars.js"
export default {
tutorialSidebar: [
'intro',
// highlight-next-line
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
};
```

View File

@@ -0,0 +1,43 @@
---
sidebar_position: 1
---
# Create a Page
Add **Markdown or React** files to `src/pages` to create a **standalone page**:
- `src/pages/index.js``localhost:3000/`
- `src/pages/foo.md``localhost:3000/foo`
- `src/pages/foo/bar.js``localhost:3000/foo/bar`
## Create your first React Page
Create a file at `src/pages/my-react-page.js`:
```jsx title="src/pages/my-react-page.js"
import React from 'react';
import Layout from '@theme/Layout';
export default function MyReactPage() {
return (
<Layout>
<h1>My React page</h1>
<p>This is a React page</p>
</Layout>
);
}
```
A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page).
## Create your first Markdown Page
Create a file at `src/pages/my-markdown-page.md`:
```mdx title="src/pages/my-markdown-page.md"
# My Markdown page
This is a Markdown page
```
A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page).

View File

@@ -0,0 +1,31 @@
---
sidebar_position: 5
---
# Deploy your site
Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**).
It builds your site as simple **static HTML, JavaScript and CSS files**.
## Build your site
Build your site **for production**:
```bash
npm run build
```
The static files are generated in the `build` folder.
## Deploy your site
Test your production build locally:
```bash
npm run serve
```
The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/).
You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**).

View File

@@ -0,0 +1,152 @@
---
sidebar_position: 4
---
# Markdown Features
Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**.
## Front Matter
Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/):
```text title="my-doc.md"
// highlight-start
---
id: my-doc-id
title: My document title
description: My document description
slug: /my-custom-url
---
// highlight-end
## Markdown heading
Markdown text with [links](./hello.md)
```
## Links
Regular Markdown links are supported, using url paths or relative file paths.
```md
Let's see how to [Create a page](/create-a-page).
```
```md
Let's see how to [Create a page](./create-a-page.md).
```
**Result:** Let's see how to [Create a page](./create-a-page.md).
## Images
Regular Markdown images are supported.
You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`):
```md
![Docusaurus logo](/img/docusaurus.png)
```
![Docusaurus logo](/img/docusaurus.png)
You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them:
```md
![Docusaurus logo](./img/docusaurus.png)
```
## Code Blocks
Markdown code blocks are supported with Syntax highlighting.
````md
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return <h1>Hello, Docusaurus!</h1>;
}
```
````
```jsx title="src/components/HelloDocusaurus.js"
function HelloDocusaurus() {
return <h1>Hello, Docusaurus!</h1>;
}
```
## Admonitions
Docusaurus has a special syntax to create admonitions and callouts:
```md
:::tip My tip
Use this awesome feature option
:::
:::danger Take care
This action is dangerous
:::
```
:::tip My tip
Use this awesome feature option
:::
:::danger Take care
This action is dangerous
:::
## MDX and React Components
[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**:
```jsx
export const Highlight = ({children, color}) => (
<span
style={{
backgroundColor: color,
borderRadius: '20px',
color: '#fff',
padding: '10px',
cursor: 'pointer',
}}
onClick={() => {
alert(`You clicked the color ${color} with label ${children}`)
}}>
{children}
</span>
);
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
This is <Highlight color="#1877F2">Facebook blue</Highlight> !
```
export const Highlight = ({children, color}) => (
<span
style={{
backgroundColor: color,
borderRadius: '20px',
color: '#fff',
padding: '10px',
cursor: 'pointer',
}}
onClick={() => {
alert(`You clicked the color ${color} with label ${children}`);
}}>
{children}
</span>
);
This is <Highlight color="#25c2a0">Docusaurus green</Highlight> !
This is <Highlight color="#1877F2">Facebook blue</Highlight> !

View File

@@ -0,0 +1,7 @@
{
"label": "Tutorial - Extras",
"position": 3,
"link": {
"type": "generated-index"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,55 @@
---
sidebar_position: 1
---
# Manage Docs Versions
Docusaurus can manage multiple versions of your docs.
## Create a docs version
Release a version 1.0 of your project:
```bash
npm run docusaurus docs:version 1.0
```
The `docs` folder is copied into `versioned_docs/version-1.0` and `versions.json` is created.
Your docs now have 2 versions:
- `1.0` at `http://localhost:3000/docs/` for the version 1.0 docs
- `current` at `http://localhost:3000/docs/next/` for the **upcoming, unreleased docs**
## Add a Version Dropdown
To navigate seamlessly across versions, add a version dropdown.
Modify the `docusaurus.config.js` file:
```js title="docusaurus.config.js"
export default {
themeConfig: {
navbar: {
items: [
// highlight-start
{
type: 'docsVersionDropdown',
},
// highlight-end
],
},
},
};
```
The docs version dropdown appears in your navbar:
![Docs Version Dropdown](./img/docsVersionDropdown.png)
## Update an existing version
It is possible to edit versioned docs in their respective folder:
- `versioned_docs/version-1.0/hello.md` updates `http://localhost:3000/docs/hello`
- `docs/hello.md` updates `http://localhost:3000/docs/next/hello`

View File

@@ -0,0 +1,88 @@
---
sidebar_position: 2
---
# Translate your site
Let's translate `docs/intro.md` to French.
## Configure i18n
Modify `docusaurus.config.js` to add support for the `fr` locale:
```js title="docusaurus.config.js"
export default {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
};
```
## Translate a doc
Copy the `docs/intro.md` file to the `i18n/fr` folder:
```bash
mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/
cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md
```
Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French.
## Start your localized site
Start your site on the French locale:
```bash
npm run start -- --locale fr
```
Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated.
:::caution
In development, you can only use one locale at a time.
:::
## Add a Locale Dropdown
To navigate seamlessly across languages, add a locale dropdown.
Modify the `docusaurus.config.js` file:
```js title="docusaurus.config.js"
export default {
themeConfig: {
navbar: {
items: [
// highlight-start
{
type: 'localeDropdown',
},
// highlight-end
],
},
},
};
```
The locale dropdown now appears in your navbar:
![Locale Dropdown](./img/localeDropdown.png)
## Build your localized site
Build your site for a specific locale:
```bash
npm run build -- --locale fr
```
Or build your site to include all the locales at once:
```bash
npm run build
```

View File

@@ -0,0 +1,153 @@
import { themes as prismThemes } from "prism-react-renderer";
import type { Config } from "@docusaurus/types";
import type * as Preset from "@docusaurus/preset-classic";
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
const config: Config = {
title: "Logistics Support TOol",
tagline: "lst",
favicon: "img/favicon.ico",
// Future flags, see https://docusaurus.io/docs/api/docusaurus-config#future
future: {
v4: true, // Improve compatibility with the upcoming Docusaurus v4
},
// Set the production url of your site here
url: "http://localhost:8080",
// Set the /<baseUrl>/ pathname under which your site is served
// For GitHub pages deployment, it is often '/<projectName>/'
baseUrl: "/lst/docs/",
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: "facebook", // Usually your GitHub org/user name.
projectName: "docusaurus", // Usually your repo name.
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
// Even if you don't use internationalization, you can use this field to set
// useful metadata like html lang. For example, if your site is Chinese, you
// may want to replace "en" with "zh-Hans".
i18n: {
defaultLocale: "en",
locales: ["en"],
},
presets: [
[
"classic",
{
docs: {
sidebarPath: "./sidebars.ts",
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/",
},
blog: {
showReadingTime: true,
feedOptions: {
type: ["rss", "atom"],
xslt: true,
},
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/",
// Useful options to enforce blogging best practices
onInlineTags: "warn",
onInlineAuthors: "warn",
onUntruncatedBlogPosts: "warn",
},
theme: {
customCss: "./src/css/custom.css",
},
} satisfies Preset.Options,
],
],
themeConfig: {
// Replace with your project's social card
image: "img/docusaurus-social-card.jpg",
navbar: {
title: "Logistics Support Tool",
logo: {
alt: "lst",
src: "img/logo.svg",
},
items: [
{
type: "docSidebar",
sidebarId: "tutorialSidebar",
position: "left",
label: "Tutorial",
},
{ to: "/blog", label: "Blog", position: "left" },
{
href: "https://github.com/facebook/docusaurus",
label: "GitHub",
position: "right",
},
],
},
footer: {
style: "dark",
links: [
{
title: "Docs",
items: [
{
label: "Tutorial",
to: "/docs/intro",
},
],
},
{
title: "Community",
items: [
{
label: "Stack Overflow",
href: "https://stackoverflow.com/questions/tagged/docusaurus",
},
{
label: "Discord",
href: "https://discordapp.com/invite/docusaurus",
},
{
label: "X",
href: "https://x.com/docusaurus",
},
],
},
{
title: "More",
items: [
{
label: "Blog",
to: "/blog",
},
{
label: "GitHub",
href: "https://github.com/facebook/docusaurus",
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
},
colorMode: {
defaultMode: "dark", // Set dark as default
disableSwitch: true, // Allow users to toggle
respectPrefersColorScheme: false, // Ignore OS preference
},
} satisfies Preset.ThemeConfig,
};
export default config;

17474
lst-docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
lst-docs/package.json Normal file
View File

@@ -0,0 +1,48 @@
{
"name": "lst-docs",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start --no-open",
"build": "rimraf build && docusaurus build && npm run build:copy",
"build:copy": "powershell -File copy.ps1",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "3.8.1",
"@docusaurus/preset-classic": "3.8.1",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "3.8.1",
"@docusaurus/tsconfig": "3.8.1",
"@docusaurus/types": "3.8.1",
"typescript": "~5.6.2"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 3 chrome version",
"last 3 firefox version",
"last 5 safari version"
]
},
"engines": {
"node": ">=18.0"
}
}

33
lst-docs/sidebars.ts Normal file
View File

@@ -0,0 +1,33 @@
import type {SidebarsConfig} from '@docusaurus/plugin-content-docs';
// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...)
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
const sidebars: SidebarsConfig = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
'intro',
'hello',
{
type: 'category',
label: 'Tutorial',
items: ['tutorial-basics/create-a-document'],
},
],
*/
};
export default sidebars;

View File

@@ -0,0 +1,71 @@
import type {ReactNode} from 'react';
import clsx from 'clsx';
import Heading from '@theme/Heading';
import styles from './styles.module.css';
type FeatureItem = {
title: string;
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
description: ReactNode;
};
const FeatureList: FeatureItem[] = [
{
title: 'Easy to Use',
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
description: (
<>
Docusaurus was designed from the ground up to be easily installed and
used to get your website up and running quickly.
</>
),
},
{
title: 'Focus on What Matters',
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
description: (
<>
Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go
ahead and move your docs into the <code>docs</code> directory.
</>
),
},
{
title: 'Powered by React',
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
description: (
<>
Extend or customize your website layout by reusing React. Docusaurus can
be extended while reusing the same header and footer.
</>
),
},
];
function Feature({title, Svg, description}: FeatureItem) {
return (
<div className={clsx('col col--4')}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<Heading as="h3">{title}</Heading>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures(): ReactNode {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,11 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}

View File

@@ -0,0 +1,30 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #2e8555;
--ifm-color-primary-dark: #29784c;
--ifm-color-primary-darker: #277148;
--ifm-color-primary-darkest: #205d3b;
--ifm-color-primary-light: #33925d;
--ifm-color-primary-lighter: #359962;
--ifm-color-primary-lightest: #3cad6e;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme='dark'] {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: #21af90;
--ifm-color-primary-darker: #1fa588;
--ifm-color-primary-darkest: #1a8870;
--ifm-color-primary-light: #29d5b0;
--ifm-color-primary-lighter: #32d8b4;
--ifm-color-primary-lightest: #4fddbf;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}

View File

@@ -0,0 +1,23 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,44 @@
import type {ReactNode} from 'react';
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';
import HomepageFeatures from '@site/src/components/HomepageFeatures';
import Heading from '@theme/Heading';
import styles from './index.module.css';
function HomepageHeader() {
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<Heading as="h1" className="hero__title">
{siteConfig.title}
</Heading>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/intro">
Docusaurus Tutorial - 5min
</Link>
</div>
</div>
</header>
);
}
export default function Home(): ReactNode {
const {siteConfig} = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />">
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}

View File

@@ -0,0 +1,7 @@
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

Some files were not shown because too many files have changed in this diff Show More