This commit is contained in:
谷口 遥人 2026-01-05 13:05:20 +00:00 committed by GitHub
commit b3b49cb5fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 89 additions and 43 deletions

View file

@ -90,6 +90,7 @@ Configuration can be done through config file, GUI or environment variables. Var
| TZ | Set your timezone for correct time | | | TZ | Set your timezone for correct time | |
| HOST | Listen address | 0.0.0.0 | | HOST | Listen address | 0.0.0.0 |
| PORT | Port for web GUI | 8840 | | PORT | Port for web GUI | 8840 |
| BASE_PATH | Optional web path prefix for UI and API (no trailing slash) | |
| THEME | Any theme name from https://bootswatch.com in lowcase or [additional](https://github.com/aceberg/aceberg-bootswatch-fork) | sand | | THEME | Any theme name from https://bootswatch.com in lowcase or [additional](https://github.com/aceberg/aceberg-bootswatch-fork) | sand |
| COLOR | Background color: light or dark | dark | | COLOR | Background color: light or dark | dark |
| NODEPATH | Path to local node modules | | | NODEPATH | Path to local node modules | |
@ -138,6 +139,7 @@ Config file name is `config_v2.yaml`. Example:
```yaml ```yaml
arp_args: "" arp_args: ""
base_path: ""
color: dark color: dark
host: 0.0.0.0 host: 0.0.0.0
ifaces: enp4s0 ifaces: enp4s0

View file

@ -3,6 +3,7 @@ package api
import ( import (
"net/http" "net/http"
"strconv" "strconv"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -20,6 +21,9 @@ func saveConfigHandler(c *gin.Context) {
conf.AppConfig.NodePath = c.PostForm("node") conf.AppConfig.NodePath = c.PostForm("node")
conf.AppConfig.ShoutURL = c.PostForm("shout") conf.AppConfig.ShoutURL = c.PostForm("shout")
basePath := c.PostForm("basepath")
conf.AppConfig.BasePath = strings.TrimRight(basePath, "/")
conf.Write(conf.AppConfig) conf.Write(conf.AppConfig)
c.Redirect(http.StatusFound, c.Request.Referer()) c.Redirect(http.StatusFound, c.Request.Referer())

View file

@ -8,7 +8,7 @@ import (
) )
// Routes - start API routes // Routes - start API routes
func Routes(router *gin.Engine) { func Routes(router gin.IRouter) {
r0 := router.Group("/api") r0 := router.Group("/api")
{ {

View file

@ -13,6 +13,7 @@ func read(path string) (config models.Conf) {
viper.SetDefault("HOST", "0.0.0.0") viper.SetDefault("HOST", "0.0.0.0")
viper.SetDefault("PORT", "8840") viper.SetDefault("PORT", "8840")
viper.SetDefault("BASE_PATH", "")
viper.SetDefault("THEME", "sand") viper.SetDefault("THEME", "sand")
viper.SetDefault("COLOR", "dark") viper.SetDefault("COLOR", "dark")
viper.SetDefault("NODEPATH", "") viper.SetDefault("NODEPATH", "")
@ -40,6 +41,7 @@ func read(path string) (config models.Conf) {
config.Host = viper.Get("HOST").(string) config.Host = viper.Get("HOST").(string)
config.Port = viper.Get("PORT").(string) config.Port = viper.Get("PORT").(string)
config.BasePath = strings.Trim(viper.GetString("BASE_PATH"), "/")
config.Theme = viper.Get("THEME").(string) config.Theme = viper.Get("THEME").(string)
config.Color = viper.Get("COLOR").(string) config.Color = viper.Get("COLOR").(string)
config.NodePath = viper.Get("NODEPATH").(string) config.NodePath = viper.Get("NODEPATH").(string)

View file

@ -19,6 +19,7 @@ func Write(config models.Conf) {
viper.Set("HOST", config.Host) viper.Set("HOST", config.Host)
viper.Set("PORT", config.Port) viper.Set("PORT", config.Port)
viper.Set("BASE_PATH", "") // Can be set only with ENV
viper.Set("THEME", config.Theme) viper.Set("THEME", config.Theme)
viper.Set("COLOR", config.Color) viper.Set("COLOR", config.Color)
viper.Set("NODEPATH", config.NodePath) viper.Set("NODEPATH", config.NodePath)

View file

@ -4,6 +4,7 @@ package models
type Conf struct { type Conf struct {
Host string Host string
Port string Port string
BasePath string
Theme string Theme string
Color string Color string
DirPath string DirPath string

View file

@ -3,10 +3,19 @@ package web
import ( import (
"net/http" "net/http"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func indexHandler(c *gin.Context) { func indexHandler(c *gin.Context) {
basePath := ""
if conf.AppConfig.BasePath != "" {
basePath = "/" + conf.AppConfig.BasePath
}
c.HTML(http.StatusOK, "index.html", true) data := gin.H{
"BasePath": basePath,
}
c.HTML(http.StatusOK, "index.html", data)
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -5,11 +5,17 @@
<title>WatchYourLAN</title> <title>WatchYourLAN</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Template data injection -->
<script>
window.appConfig = {
basePath: "{{ .BasePath }}",
};
</script>
<!--Favicon--> <!--Favicon-->
<link rel="icon" type="image/x-icon" href="/fs/public/favicon.png"> <link rel="icon" type="image/x-icon" href="{{ .BasePath }}/fs/public/favicon.png">
<!-- JS & CSS --> <!-- JS & CSS -->
<script type="module" crossorigin src="/fs/public/assets/index.js"></script> <script type="module" crossorigin src="{{ .BasePath }}/fs/public/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/fs/public/assets/index.css"> <link rel="stylesheet" crossorigin href="{{ .BasePath }}/fs/public/assets/index.css">
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View file

@ -41,7 +41,7 @@ func Gui() {
"\n Config dir: " + conf.AppConfig.DirPath + "\n Config dir: " + conf.AppConfig.DirPath +
"\n Default DB: " + conf.AppConfig.UseDB + "\n Default DB: " + conf.AppConfig.UseDB +
"\n Log level: " + conf.AppConfig.LogLevel + "\n Log level: " + conf.AppConfig.LogLevel +
"\n Web GUI: http://" + address + "\n Web GUI: http://" + address + "/" + conf.AppConfig.BasePath +
"\n=================================== " + colorReset) "\n=================================== " + colorReset)
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
@ -51,15 +51,21 @@ func Gui() {
templ := template.Must(template.New("").ParseFS(templFS, "templates/*")) templ := template.Must(template.New("").ParseFS(templFS, "templates/*"))
router.SetHTMLTemplate(templ) // templates router.SetHTMLTemplate(templ) // templates
router.StaticFS("/fs/", http.FS(pubFS)) // public var routerGroup gin.IRouter = router
router.GET("/", indexHandler) // index.go if conf.AppConfig.BasePath != "" {
router.GET("/config", indexHandler) // index.go routerGroup = router.Group(conf.AppConfig.BasePath)
router.GET("/history", indexHandler) // index.go }
router.GET("/host/*any", indexHandler) // index.go
router.GET("/metrics", prometheus.Handler())
api.Routes(router) routerGroup.StaticFS("/fs/", http.FS(pubFS))
routerGroup.GET("/", indexHandler)
routerGroup.GET("/config", indexHandler)
routerGroup.GET("/history", indexHandler)
routerGroup.GET("/host/*any", indexHandler)
routerGroup.GET("/metrics", prometheus.Handler())
api.Routes(routerGroup)
err = router.Run(address) err = router.Run(address)
check.IfError(err) check.IfError(err)

View file

@ -11,6 +11,7 @@ services:
IFACES: "enp4s0 wlxf4ec3892dd51" # required: 1 or more interface IFACES: "enp4s0 wlxf4ec3892dd51" # required: 1 or more interface
HOST: "0.0.0.0" # optional, default: 0.0.0.0 HOST: "0.0.0.0" # optional, default: 0.0.0.0
PORT: "8840" # optional, default: 8840 PORT: "8840" # optional, default: 8840
BASE_PATH: "" # optional
TIMEOUT: "120" # optional, time in seconds, default: 120 TIMEOUT: "120" # optional, time in seconds, default: 120
SHOUTRRR_URL: "" # optional, set url to notify SHOUTRRR_URL: "" # optional, set url to notify
THEME: "sand" # optional THEME: "sand" # optional

View file

@ -22,6 +22,7 @@ services:
IFACES: "enp4s0 wlxf4ec3892dd51" # required: 1 or more interface IFACES: "enp4s0 wlxf4ec3892dd51" # required: 1 or more interface
# HOST: "0.0.0.0" # optional, default: 0.0.0.0 # HOST: "0.0.0.0" # optional, default: 0.0.0.0
# PORT: "8840" # optional, default: 8840 # PORT: "8840" # optional, default: 8840
# BASE_PATH: "" # optional, default: "" (empty)
# TIMEOUT: "120" # optional, time in seconds, default: 120 # TIMEOUT: "120" # optional, time in seconds, default: 120
# SHOUTRRR_URL: "" # optional, set url to notify # SHOUTRRR_URL: "" # optional, set url to notify
# THEME: "sand" # optional # THEME: "sand" # optional

View file

@ -1,64 +1,68 @@
## API ## API
```http ```http
GET /api/all GET {BASE_PATH}/api/all
``` ```
Returns all hosts in `json`. Returns all hosts in `json`.
```http ```http
GET /api/history GET {BASE_PATH}/api/history
``` ```
Returns all History. Not recommended, the output can be a lot. Returns all History. Not recommended, the output can be a lot.
```http ```http
GET /api/history/:mac/:date GET {BASE_PATH}/api/history/:mac/:date
``` ```
Returns only history of a device with this `mac` filtered by `date`. `date` format can be anything from `2` to `2025-07` to `2025-07-26`. Returns only history of a device with this `mac` filtered by `date`. `date` format can be anything from `2` to `2025-07` to `2025-07-26`.
```http ```http
GET /api/history/:mac?num=20 GET {BASE_PATH}/api/history/:mac?num=20
``` ```
Returns only last 20 lines of history of a device with this `mac`. Returns only last 20 lines of history of a device with this `mac`.
```http ```http
GET /api/host/:id GET {BASE_PATH}/api/host/:id
``` ```
Returns host with this `id` in `json`. Returns host with this `id` in `json`.
```http ```http
GET /api/port/:addr/:port GET {BASE_PATH}/api/port/:addr/:port
``` ```
Gets state of one `port` of `addr`. Returns `true` if port is open or `false` otherwise. Gets state of one `port` of `addr`. Returns `true` if port is open or `false` otherwise.
<details> <details>
<summary>Request example</summary> <summary>Request example</summary>
```bash ```bash
curl http://0.0.0.0:8840/api/port/192.168.2.2/8844 # No base path
curl "http://0.0.0.0:8840/api/port/192.168.2.2/8844"
# Generic form
curl "http://0.0.0.0:8840/{BASE_PATH}/api/port/192.168.2.2/8844"
``` ```
</details><br> </details><br>
```http ```http
GET /api/edit/:id/:name/*known GET {BASE_PATH}/api/edit/:id/:name/*known
``` ```
Edit host with ID `id`. Can change `name`. `known` is optional, when set to `toggle` will change Known state. Edit host with ID `id`. Can change `name`. `known` is optional, when set to `toggle` will change Known state.
```http ```http
GET /api/host/del/:id GET {BASE_PATH}/api/host/del/:id
``` ```
Remove host with ID `id`. Remove host with ID `id`.
```http ```http
GET /api/notify_test GET {BASE_PATH}/api/notify_test
``` ```
Send test notification. Send test notification.
```http ```http
GET /api/status/*iface GET {BASE_PATH}/api/status/*iface
``` ```
Show status (Total number of hosts, online/offline, known/unknown). The `iface` parameter is optional and shows status for one interface only. For all interfaces just call `/api/status/`. Show status (Total number of hosts, online/offline, known/unknown). The `iface` parameter is optional and shows status for one interface only. For all interfaces just call `{BASE_PATH}/api/status/`.

View file

@ -1,12 +1,12 @@
{ {
"name": "watchyourlan", "name": "watchyourlan",
"version": "2.1.3", "version": "2.1.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "watchyourlan", "name": "watchyourlan",
"version": "2.1.3", "version": "2.1.4",
"dependencies": { "dependencies": {
"@solid-primitives/scheduled": "^1.5.2", "@solid-primitives/scheduled": "^1.5.2",
"@solidjs/router": "^0.15.3", "@solidjs/router": "^0.15.3",

View file

@ -22,7 +22,7 @@ function App() {
<div class="container-lg"> <div class="container-lg">
<div class="row"> <div class="row">
<div class="col-md mt-4 mb-4"> <div class="col-md mt-4 mb-4">
<Router> <Router base={window.appConfig?.basePath || ""}>
<Route path="/" component={Body}/> <Route path="/" component={Body}/>
<Route path="/config" component={Config}/> <Route path="/config" component={Config}/>
<Route path="/history" component={History}/> <Route path="/history" component={History}/>

View file

@ -35,17 +35,17 @@ function Header() {
<nav class="navbar navbar-expand-md navbar-dark bg-primary"> <nav class="navbar navbar-expand-md navbar-dark bg-primary">
<div class="container-lg"> <div class="container-lg">
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="/">
<img src="/fs/public/favicon.png" style="width: 2em"/> <img src={window.appConfig?.basePath + "/fs/public/favicon.png"} style="width: 2em"/>
</a> </a>
<ul class="navbar-nav me-auto mb-2 mb-md-0"> <ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="/" title="Home">Home</a> <a class="nav-link active" href={window.appConfig?.basePath + "/"} title="Home">Home</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="/config/" title="Config">Config</a> <a class="nav-link active" href={window.appConfig?.basePath + "/config/"} title="Config">Config</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" href="/history/" title="History">History</a> <a class="nav-link active" href={window.appConfig?.basePath + "/history/"} title="History">History</a>
</li> </li>
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">

View file

@ -1,4 +1,4 @@
export const apiPath = 'http://0.0.0.0:8840'; export const apiPath = 'http://0.0.0.0:8840'+(window.appConfig?.basePath ?? '');
export const apiGetAllHosts = async () => { export const apiGetAllHosts = async () => {
const url = apiPath+'/api/all'; const url = apiPath+'/api/all';

View file

@ -1 +1,10 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
export {};
declare global {
interface Window {
appConfig: {
basePath: string;
};
}
}