Attempt 2
@@ -25,7 +25,7 @@ WORKDIR /root/
|
||||
# Get
|
||||
COPY --from=builder /app-server/build ./build
|
||||
COPY ./certs ./certs
|
||||
COPY ./*.env ./build/
|
||||
COPY ./*.env ./
|
||||
|
||||
# Run the app
|
||||
CMD ["./build/app_server"]
|
||||
|
||||
@@ -9,6 +9,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/chirpstack/chirpstack/api/go/v4 v4.14.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/crypto v0.39.0
|
||||
google.golang.org/grpc v1.75.0
|
||||
google.golang.org/protobuf v1.36.8
|
||||
|
||||
@@ -25,6 +25,8 @@ github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
serder "server/serder"
|
||||
|
||||
chirp_api "github.com/chirpstack/chirpstack/api/go/v4/api"
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -282,6 +283,8 @@ func main() {
|
||||
ExitSig: quit_signal,
|
||||
}
|
||||
|
||||
godotenv.Load("./.env")
|
||||
|
||||
// === Logger settings
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lmsgprefix | log.Lshortfile)
|
||||
log.SetPrefix(FindEnv("LOG_PREFIX") + " ")
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
[network]
|
||||
|
||||
# Network identifier (NetID, 3 bytes) encoded as HEX (e.g. 010203).
|
||||
net_id="10204"
|
||||
net_id="102040"
|
||||
|
||||
# Enabled regions.
|
||||
#
|
||||
|
||||
@@ -26,10 +26,8 @@ RUN apk add --no-cache ca-certificates
|
||||
## Final build
|
||||
# Get
|
||||
COPY --from=builder /web-server/build ./build/
|
||||
COPY ./build/stylesheet.css ./layouts/
|
||||
COPY ./src/layouts ./layouts/
|
||||
COPY ./config ./config/
|
||||
COPY ./shared.env ./config/shared.env
|
||||
COPY ./src/layouts ./layouts
|
||||
COPY ./.env ./config/.env
|
||||
|
||||
# Run
|
||||
CMD ["./build/web_server"]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Variables
|
||||
CSS_INPUT:=./src/layouts/styles/global.css
|
||||
CSS_INPUT:=./src/layouts/static/styles/global.css
|
||||
CSS_OUTPUT:=./build/stylesheet.css
|
||||
NAME:=web-app
|
||||
|
||||
all: css
|
||||
|
||||
# Build Tailwind
|
||||
css: ./src/layouts/**/*.css ./src/layouts/**/*.tmpl
|
||||
css: $(CSS_INPUT) ./src/layouts/**/*.tmpl
|
||||
npx tailwindcss -i $(CSS_INPUT) -o $(CSS_OUTPUT) --minify
|
||||
|
||||
# Build App
|
||||
|
||||
@@ -7,6 +7,8 @@ services:
|
||||
networks:
|
||||
pagerino_net:
|
||||
restart: on-failure:3
|
||||
ports:
|
||||
- "8222:8222"
|
||||
|
||||
networks:
|
||||
pagerino_net:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{{ define "meta" -}}
|
||||
<title>{{ .meta.Title }}</title>
|
||||
<link rel="icon" type="image/x-icon" href="/static/{{ if .meta.icon }}{{ .meta.icon }}{{ else }}favicon.ico{{ end }}">
|
||||
<link rel="icon" type="image/x-icon" href="/static/{{ if .meta.Icon }}{{ .meta.Icon }}{{ else }}favicon.ico{{ end }}">
|
||||
|
||||
<meta name="author" content="Olek" />
|
||||
<meta name="description"
|
||||
content={{ .meta.description }} />
|
||||
content={{ .meta.Description }} />
|
||||
{{- end }}
|
||||
@@ -1,17 +1,32 @@
|
||||
{{ define "navbar" }}
|
||||
<nav class="fixed top-0 w-full bg-[var(--primary-bg)] shadow-md z-50 transition-all duration-300">
|
||||
<nav class="sticky top-0 w-full bg-[var(--primary-bg)] shadow-md z-50 transition-all duration-300">
|
||||
<div id="navbar-inner" class="max-w-7xl mx-auto flex items-center justify-between px-4 py-3 md:py-4 transition-all duration-300">
|
||||
<!-- Logo -->
|
||||
<a href="/" class="flex items-center transition-all duration-300" id="logo-container">
|
||||
<img src="/static/logo.png" alt="Logo" class="h-12 md:h-16 transition-all duration-300" id="logo-img"/>
|
||||
<span class="ml-2 text-[var(--accent)] font-bold text-xl md:text-2xl">MyApp</span>
|
||||
<a href="/" class="flex items-start transition-all duration-300" id="logo-container">
|
||||
<img src="/static/GORAKLOGO.png" alt="Logo" class="h-12 md:h-16 transition-all duration-300" id="logo-img"/>
|
||||
<span class="ml-2 text-[var(--accent)] font-bold text-xl md:text-2xl">Pagerino</span>
|
||||
</a>
|
||||
|
||||
<!-- Nav Links -->
|
||||
<ul class="hidden md:flex space-x-6 text-[var(--text-primary)] font-medium">
|
||||
<li><a href="/network" class="hover:text-[var(--accent)] transition-colors">Network</a></li>
|
||||
<li><a href="/map" class="hover:text-[var(--accent)] transition-colors">Map</a></li>
|
||||
<li><a href="/analytics" class="hover:text-[var(--accent)] transition-colors">Analytics</a></li>
|
||||
<ul class="hidden md:flex space-x-10 text-[var(--text-primary)] font-semibold">
|
||||
<li>
|
||||
<a href="/network"
|
||||
class="px-4 py-2 rounded-lg hover:bg-[var(--accent)] hover:text-white transition-colors duration-200">
|
||||
Network
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/map"
|
||||
class="px-4 py-2 rounded-lg hover:bg-[var(--accent)] hover:text-white transition-colors duration-200">
|
||||
Map
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/analytics"
|
||||
class="px-4 py-2 rounded-lg hover:bg-[var(--accent)] hover:text-white transition-colors duration-200">
|
||||
Analytics
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- User / Login -->
|
||||
|
||||
29
WebApp/src/layouts/components/websocket.tmpl
Normal file
@@ -0,0 +1,29 @@
|
||||
{{ define "websocket" -}}
|
||||
<script>
|
||||
let socket; // Global socket variable
|
||||
|
||||
function initWebSocket() { //TODO Make connection persistent across all pages
|
||||
if (socket) return; // Single connection
|
||||
|
||||
socket = new WebSocket("ws://" + window.location.host + "/ws");
|
||||
|
||||
socket.onopen = function() {
|
||||
console.log("Connected to server");
|
||||
};
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("Message from server:", data);
|
||||
};
|
||||
|
||||
let retries = 0
|
||||
socket.onclose = function() {
|
||||
console.log("Disconnected");
|
||||
socket = null; // For reconnection
|
||||
setTimeout(initWebSocket, 1000)
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("load", initWebSocket);
|
||||
</script>
|
||||
{{- end }}
|
||||
@@ -1,51 +0,0 @@
|
||||
{{ define "layout" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link href="../../build/stylesheet.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
|
||||
{{ template "meta" . }}
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
{{ template "navbar" . }}
|
||||
<main>
|
||||
{{ template "content" . }}
|
||||
</main>
|
||||
<footer>
|
||||
{{ template "footer" . }}
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
let socket; // Global socket variable
|
||||
|
||||
function initWebSocket() { //TODO Make connection persistent across all pages
|
||||
if (socket) return; // Single connection
|
||||
|
||||
socket = new WebSocket("ws://" + window.location.host + "/ws");
|
||||
|
||||
socket.onopen = function() {
|
||||
console.log("Connected to server");
|
||||
};
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("Message from server:", data);
|
||||
};
|
||||
|
||||
let retries = 0
|
||||
socket.onclose = function() {
|
||||
console.log("Disconnected");
|
||||
socket = null; // For reconnection
|
||||
setTimeout(initWebSocket, 1000)
|
||||
};
|
||||
}
|
||||
|
||||
window.addEventListener("load", initWebSocket);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
||||
@@ -1,4 +1,4 @@
|
||||
{{ define "content" -}}
|
||||
{{ define "home" -}}
|
||||
<h1>
|
||||
Hello, Pagerino User!
|
||||
</h1>
|
||||
|
||||
27
WebApp/src/layouts/pages/index.tmpl
Normal file
@@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link href="/static/styles/stylesheet.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||
<link rel="manifest" href="/static/site.webmanifest" />
|
||||
|
||||
{{ template "meta" . }}
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
{{ template "navbar" . }}
|
||||
<main>
|
||||
{{ if eq .Page "login" }}
|
||||
{{ template "login" . }}
|
||||
{{ else if eq .Page "home" }}
|
||||
{{ template "home" . }}
|
||||
{{ end }}
|
||||
</main>
|
||||
<footer>
|
||||
{{ template "footer" . }}
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +1,35 @@
|
||||
{{ define "content" -}}
|
||||
<form method="POST" action="/login">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" required />
|
||||
{{ define "login" -}}
|
||||
<div class="flex items-center justify-center min-h-screen bg-gray-100">
|
||||
<form method="POST" action="/login"
|
||||
class="bg-white shadow-lg rounded-2xl p-8 w-full max-w-md space-y-6">
|
||||
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" required />
|
||||
<h2 class="text-2xl font-bold text-center text-gray-800">Login</h2>
|
||||
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
<!-- Username -->
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-600">Username</label>
|
||||
<input type="text" id="username" name="username" required
|
||||
class="mt-1 w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none" />
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-600">Password</label>
|
||||
<input type="password" id="password" name="password" required
|
||||
class="mt-1 w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none" />
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-600 text-white py-2 rounded-lg font-medium hover:bg-blue-700 transition-colors">
|
||||
Login
|
||||
</button>
|
||||
|
||||
<!-- Optional link -->
|
||||
<p class="text-sm text-center text-gray-500">
|
||||
Don’t have an account?
|
||||
<a href="/register" class="text-blue-600 hover:underline">Sign up</a>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
{{- end }}
|
||||
BIN
WebApp/src/layouts/static/GORAKLOGO.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
WebApp/src/layouts/static/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
WebApp/src/layouts/static/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
WebApp/src/layouts/static/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
WebApp/src/layouts/static/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 376 B |
BIN
WebApp/src/layouts/static/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 845 B |
BIN
WebApp/src/layouts/static/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
WebApp/src/layouts/static/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
2
WebApp/src/layouts/static/styles/stylesheet.css
Normal file
@@ -58,7 +58,7 @@ type UserData struct {
|
||||
type PageMeta struct {
|
||||
Title string
|
||||
Description string
|
||||
IconFile string
|
||||
Icon string
|
||||
}
|
||||
|
||||
// === Utilities ===
|
||||
@@ -176,7 +176,7 @@ func main() {
|
||||
signal.Notify(Data.ExitSig, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
// = Load configs
|
||||
godotenv.Load("./config/app_config.env", "./config/shared.env", "./config/socket_config.env")
|
||||
godotenv.Load("./config/.env")
|
||||
|
||||
// = Setup logger
|
||||
log.SetFlags(log.Lmsgprefix | log.LUTC | log.Lshortfile)
|
||||
@@ -186,7 +186,7 @@ func main() {
|
||||
router := gin.Default()
|
||||
|
||||
// Load assets and HTML templates
|
||||
router.Static("./layouts/static", "./layouts/static")
|
||||
router.Static("/static", "./layouts/static")
|
||||
router.LoadHTMLGlob("./layouts/**/*.tmpl")
|
||||
|
||||
HOME_NAME := os.Getenv("HOME_NAME")
|
||||
@@ -215,7 +215,7 @@ func main() {
|
||||
dial_opts...,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("Failed to connect to gRPC API")
|
||||
log.Println("Failed to connect to gRPC API: " + err.Error())
|
||||
return
|
||||
}
|
||||
Data.ApiUserClient = api_user.NewUserServiceClient(grpc_client)
|
||||
@@ -232,21 +232,25 @@ func main() {
|
||||
title = strings.ToTitle(HOME_NAME)
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, "home.tmpl", gin.H{
|
||||
ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"meta": PageMeta{
|
||||
Title: title,
|
||||
Description: "Pagerino system home page",
|
||||
//Icon: "",
|
||||
},
|
||||
"Page": "home",
|
||||
})
|
||||
})
|
||||
|
||||
// Login page (escape auth handler)
|
||||
router.GET("/login", func(ctx *gin.Context) {
|
||||
ctx.HTML(http.StatusOK, "login.tmpl", gin.H{
|
||||
ctx.HTML(http.StatusOK, "index.tmpl", gin.H{
|
||||
"meta": PageMeta{
|
||||
Title: "Login",
|
||||
Description: "Pagerino system login page",
|
||||
//Icon: "",
|
||||
},
|
||||
"Page": "login",
|
||||
})
|
||||
})
|
||||
|
||||
@@ -257,7 +261,7 @@ func main() {
|
||||
|
||||
// Run server in another thread for graceful shutdown
|
||||
go func() {
|
||||
if err := router.Run("localhost:" + Data.FindEnv("WEB_PORT")); err != nil && err != http.ErrServerClosed {
|
||||
if err := router.Run(":" + Data.FindEnv("WEB_PORT")); err != nil && err != http.ErrServerClosed {
|
||||
log.Println("HTTP Server error: " + err.Error())
|
||||
}
|
||||
log.Println("HTTP server shutdown.")
|
||||
|
||||