Merge pull request #1 from yaoresugi/feature/basepath-support

ベースパスを指定できるようにした
This commit is contained in:
谷口 遥人 2026-01-05 19:51:16 +09:00 committed by GitHub
commit 71ce54c0ad
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 | |
| HOST | Listen address | 0.0.0.0 |
| 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 |
| COLOR | Background color: light or dark | dark |
| NODEPATH | Path to local node modules | |
@ -138,6 +139,7 @@ Config file name is `config_v2.yaml`. Example:
```yaml
arp_args: ""
base_path: ""
color: dark
host: 0.0.0.0
ifaces: enp4s0

View file

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

View file

@ -8,7 +8,7 @@ import (
)
// Routes - start API routes
func Routes(router *gin.Engine) {
func Routes(router gin.IRouter) {
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("PORT", "8840")
viper.SetDefault("BASE_PATH", "")
viper.SetDefault("THEME", "sand")
viper.SetDefault("COLOR", "dark")
viper.SetDefault("NODEPATH", "")
@ -40,6 +41,7 @@ func read(path string) (config models.Conf) {
config.Host = viper.Get("HOST").(string)
config.Port = viper.Get("PORT").(string)
config.BasePath = strings.Trim(viper.GetString("BASE_PATH"), "/")
config.Theme = viper.Get("THEME").(string)
config.Color = viper.Get("COLOR").(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("PORT", config.Port)
viper.Set("BASE_PATH", "") // Can be set only with ENV
viper.Set("THEME", config.Theme)
viper.Set("COLOR", config.Color)
viper.Set("NODEPATH", config.NodePath)

View file

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

View file

@ -3,10 +3,19 @@ package web
import (
"net/http"
"github.com/aceberg/WatchYourLAN/internal/conf"
"github.com/gin-gonic/gin"
)
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>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Template data injection -->
<script>
window.appConfig = {
basePath: "{{ .BasePath }}",
};
</script>
<!--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 -->
<script type="module" crossorigin src="/fs/public/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="/fs/public/assets/index.css">
<script type="module" crossorigin src="{{ .BasePath }}/fs/public/assets/index.js"></script>
<link rel="stylesheet" crossorigin href="{{ .BasePath }}/fs/public/assets/index.css">
</head>
<body>
<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 Default DB: " + conf.AppConfig.UseDB +
"\n Log level: " + conf.AppConfig.LogLevel +
"\n Web GUI: http://" + address +
"\n Web GUI: http://" + address + "/" + conf.AppConfig.BasePath +
"\n=================================== " + colorReset)
gin.SetMode(gin.ReleaseMode)
@ -51,15 +51,21 @@ func Gui() {
templ := template.Must(template.New("").ParseFS(templFS, "templates/*"))
router.SetHTMLTemplate(templ) // templates
router.StaticFS("/fs/", http.FS(pubFS)) // public
var routerGroup gin.IRouter = router
router.GET("/", indexHandler) // index.go
router.GET("/config", indexHandler) // index.go
router.GET("/history", indexHandler) // index.go
router.GET("/host/*any", indexHandler) // index.go
router.GET("/metrics", prometheus.Handler())
if conf.AppConfig.BasePath != "" {
routerGroup = router.Group(conf.AppConfig.BasePath)
}
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)
check.IfError(err)

View file

@ -2,7 +2,7 @@ version: "3"
services:
wyl:
image: aceberg/watchyourlan
network_mode: "host"
network_mode: "host"
restart: unless-stopped
volumes:
- ~/.dockerdata/wyl:/data/WatchYourLAN
@ -11,6 +11,7 @@ services:
IFACES: "enp4s0 wlxf4ec3892dd51" # required: 1 or more interface
HOST: "0.0.0.0" # optional, default: 0.0.0.0
PORT: "8840" # optional, default: 8840
BASE_PATH: "" # optional
TIMEOUT: "120" # optional, time in seconds, default: 120
SHOUTRRR_URL: "" # optional, set url to notify
THEME: "sand" # optional
@ -19,7 +20,7 @@ services:
# WARNING! WYL needs 'host' network mode to work. So, WYL port will be exposed in this setup. You need to limit access to it with firewall or other measures
forauth:
image: aceberg/forauth
image: aceberg/forauth
restart: unless-stopped
ports:
- 8800:8800 # Proxy port
@ -28,10 +29,10 @@ services:
- ~/.dockerdata/forauth:/data/ForAuth
environment:
TZ: Asia/Novosibirsk # required: needs your TZ for correct time
FA_TARGET: "YOUR_IP:8840" # optional: path to wyl host:port
FA_TARGET: "YOUR_IP:8840" # optional: path to wyl host:port
FA_AUTH: "true" # optional: true - enabled, default: false
FA_AUTH_EXPIRE: 7d # optional: expiration time, default: 7d
FA_AUTH_PASSWORD: "$$2a$$10$$wGLUHXh2cRN1257uGg1s5eZvYgnjw8wB9vAcfcHqqqrxm5hvBqAzK"
FA_AUTH_PASSWORD: "$$2a$$10$$wGLUHXh2cRN1257uGg1s5eZvYgnjw8wB9vAcfcHqqqrxm5hvBqAzK"
# WARNING! If password is set as environment variable, every '$' character must be escaped with another '$', like this '$$'
# optional: password encrypted with bcrypt, how-to: https://github.com/aceberg/ForAuth/blob/main/docs/BCRYPT.md (In this example FA_AUTH_PASSWORD=pw)
FA_AUTH_USER: user # optional: username

View file

@ -9,7 +9,7 @@ services:
wyl:
image: aceberg/watchyourlan # dockerhub
# image: ghcr.io/aceberg/watchyourlan # or github
network_mode: "host"
network_mode: "host"
restart: unless-stopped
# uncomment those if you are using local node-bootstrap:
# command: "-n http://YOUR_IP:8850" # put your server IP or DNS name here
@ -22,6 +22,7 @@ services:
IFACES: "enp4s0 wlxf4ec3892dd51" # required: 1 or more interface
# HOST: "0.0.0.0" # optional, default: 0.0.0.0
# PORT: "8840" # optional, default: 8840
# BASE_PATH: "" # optional, default: "" (empty)
# TIMEOUT: "120" # optional, time in seconds, default: 120
# SHOUTRRR_URL: "" # optional, set url to notify
# THEME: "sand" # optional

View file

@ -1,64 +1,68 @@
## API
```http
GET /api/all
GET {BASE_PATH}/api/all
```
Returns all hosts in `json`.
```http
GET /api/history
GET {BASE_PATH}/api/history
```
Returns all History. Not recommended, the output can be a lot.
```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`.
```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`.
```http
GET /api/host/:id
GET {BASE_PATH}/api/host/:id
```
Returns host with this `id` in `json`.
```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.
<details>
<summary>Request example</summary>
```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>
```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.
```http
GET /api/host/del/:id
GET {BASE_PATH}/api/host/del/:id
```
Remove host with ID `id`.
```http
GET /api/notify_test
GET {BASE_PATH}/api/notify_test
```
Send test notification.
```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",
"version": "2.1.3",
"version": "2.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "watchyourlan",
"version": "2.1.3",
"version": "2.1.4",
"dependencies": {
"@solid-primitives/scheduled": "^1.5.2",
"@solidjs/router": "^0.15.3",

View file

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

View file

@ -6,13 +6,13 @@ function Header() {
const [themePath, setThemePath] = createSignal('');
const [iconsPath, setIconsPath] = createSignal('');
const setCurrentTheme = async () => {
setAppConfig(await apiGetConfig());
const theme = appConfig().Theme?appConfig().Theme:"sand";
const color = appConfig().Color?appConfig().Color:"dark";
if (appConfig().NodePath == '') {
setThemePath("https://cdn.jsdelivr.net/npm/aceberg-bootswatch-fork@v5.3.3-2/dist/"+theme+"/bootstrap.min.css");
setIconsPath("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css");
@ -35,17 +35,17 @@ function Header() {
<nav class="navbar navbar-expand-md navbar-dark bg-primary">
<div class="container-lg">
<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>
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<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 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 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>
</ul>
<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 () => {
const url = apiPath+'/api/all';

View file

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