Add host from API, trigger rescan

This commit is contained in:
aceberg 2025-09-07 03:33:42 +07:00
parent 1271b7b42e
commit 24ee0acb45
20 changed files with 255 additions and 24 deletions

View file

@ -4,12 +4,10 @@ All notable changes to this project will be documented in this file.
## [v2.1.4] - 2025-09
### Added
- Swagger API docs
- Add host from API
- Delete selected hosts
- Wake-on-LAN [#135](https://github.com/aceberg/WatchYourLAN/issues/135), [#196](https://github.com/aceberg/WatchYourLAN/issues/196)
### Fixed
- Names not fully saved while editing with GUI
## [v2.1.3] - 2025-07-26
### Fixed
- Memory leak bug [#149](https://github.com/aceberg/WatchYourLAN/issues/149)

View file

@ -207,6 +207,53 @@ const docTemplate = `{
}
}
},
"/host/add/{mac}": {
"get": {
"description": "Add host by MAC, with optional Name, IP, Hardware\nReturns ` + "`" + `models.Host` + "`" + ` with this MAC form DB, either just added or existing",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Add host manually",
"parameters": [
{
"type": "string",
"description": "Host MAC",
"name": "mac",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "IP",
"name": "ip",
"in": "query"
},
{
"type": "string",
"description": "Hardware",
"name": "hw",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Host"
}
}
}
}
},
"/host/del/{id}": {
"get": {
"description": "Remove a host from the database by its unique ID",
@ -321,6 +368,26 @@ const docTemplate = `{
}
}
},
"/rescan": {
"get": {
"description": "Manually trigger rescan",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Rescan all interfaces now",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/status/{iface}": {
"get": {
"description": "Retrieve summary statistics of hosts, optionally filtered by interface",

View file

@ -200,6 +200,53 @@
}
}
},
"/host/add/{mac}": {
"get": {
"description": "Add host by MAC, with optional Name, IP, Hardware\nReturns `models.Host` with this MAC form DB, either just added or existing",
"produces": [
"application/json"
],
"tags": [
"hosts"
],
"summary": "Add host manually",
"parameters": [
{
"type": "string",
"description": "Host MAC",
"name": "mac",
"in": "path",
"required": true
},
{
"type": "string",
"description": "Name",
"name": "name",
"in": "query"
},
{
"type": "string",
"description": "IP",
"name": "ip",
"in": "query"
},
{
"type": "string",
"description": "Hardware",
"name": "hw",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.Host"
}
}
}
}
},
"/host/del/{id}": {
"get": {
"description": "Remove a host from the database by its unique ID",
@ -314,6 +361,26 @@
}
}
},
"/rescan": {
"get": {
"description": "Manually trigger rescan",
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "Rescan all interfaces now",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "string"
}
}
}
}
},
"/status/{iface}": {
"get": {
"description": "Retrieve summary statistics of hosts, optionally filtered by interface",

View file

@ -254,6 +254,39 @@ paths:
summary: Get host by ID
tags:
- hosts
/host/add/{mac}:
get:
description: |-
Add host by MAC, with optional Name, IP, Hardware
Returns `models.Host` with this MAC form DB, either just added or existing
parameters:
- description: Host MAC
in: path
name: mac
required: true
type: string
- description: Name
in: query
name: name
type: string
- description: IP
in: query
name: ip
type: string
- description: Hardware
in: query
name: hw
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.Host'
summary: Add host manually
tags:
- hosts
/host/del/{id}:
get:
description: Remove a host from the database by its unique ID
@ -310,6 +343,19 @@ paths:
summary: Check port state
tags:
- network
/rescan:
get:
description: Manually trigger rescan
produces:
- application/json
responses:
"200":
description: OK
schema:
type: string
summary: Rescan all interfaces now
tags:
- system
/status/{iface}:
get:
description: Retrieve summary statistics of hosts, optionally filtered by interface

View file

@ -8,6 +8,7 @@ import (
"github.com/aceberg/WatchYourLAN/internal/check"
"github.com/aceberg/WatchYourLAN/internal/gdb"
"github.com/aceberg/WatchYourLAN/internal/models"
)
// getAllHosts godoc
@ -53,6 +54,42 @@ func delHost(c *gin.Context) {
c.IndentedJSON(http.StatusOK, "OK")
}
// addHost godoc
// @Summary Add host manually
// @Description Add host by MAC, with optional Name, IP, Hardware
// @Description Returns `models.Host` with this MAC form DB, either just added or existing
// @Tags hosts
// @Produce json
// @Param mac path string true "Host MAC"
// @Param name query string false "Name"
// @Param ip query string false "IP"
// @Param hw query string false "Hardware"
// @Success 200 {object} models.Host
// @Router /host/add/{mac} [get]
func addHost(c *gin.Context) {
mac := c.Param("mac")
hosts := gdb.SelectByMAC("now", mac)
if len(hosts) > 0 {
slog.Warn("Host with this MAC already exists", "host", hosts[0])
} else {
var host models.Host
host.Mac = mac
host.Name = c.Query("name")
host.IP = c.Query("ip")
host.Hw = c.Query("hw")
gdb.Update("now", host)
hosts = gdb.SelectByMAC("now", mac)
slog.Info("Added host to DB", "host", hosts[0])
}
c.IndentedJSON(http.StatusOK, hosts[0])
}
// editHost godoc
// @Summary Edit host
// @Description Update a host's name and optionally toggle its "known" status

View file

@ -9,6 +9,7 @@ import (
"github.com/aceberg/WatchYourLAN/internal/gdb"
"github.com/aceberg/WatchYourLAN/internal/models"
"github.com/aceberg/WatchYourLAN/internal/notify"
"github.com/aceberg/WatchYourLAN/internal/routines"
)
// getVersion godoc
@ -22,6 +23,18 @@ func getVersion(c *gin.Context) {
c.IndentedJSON(http.StatusOK, conf.AppConfig.Version)
}
// triggerRescan godoc
// @Summary Rescan all interfaces now
// @Description Manually trigger rescan
// @Tags system
// @Produce json
// @Success 200 {string} string "OK"
// @Router /rescan [get]
func triggerRescan(c *gin.Context) {
routines.ScanRestart()
c.Status(http.StatusOK)
}
// getConfig godoc
// @Summary Get application configuration
// @Description Returns the current configuration used by the app

View file

@ -16,11 +16,13 @@ func Routes(router *gin.Engine) {
r0.GET("/edit/:id/:name/*known", editHost) // api-hosts.go
r0.GET("/host/:id", getHost) // api-hosts.go
r0.GET("/host/del/:id", delHost) // api-hosts.go
r0.GET("/host/add/:mac", addHost) // api-hosts.go
r0.GET("/config", getConfig) // api-system.go
r0.GET("/notify_test", notifyTest) // api-system.go
r0.GET("/status/*iface", getStatus) // api-system.go
r0.GET("/version", getVersion) // api-system.go
r0.GET("/rescan", triggerRescan) // api-system.go
r0.GET("/history", getHistory) // api-history.go
r0.GET("/history/:mac", getHistoryByMAC) // api-history.go

View file

@ -24,9 +24,9 @@ func SelectByID(id int) (host models.Host) {
}
// SelectByMAC - get all hosts by MAC
func SelectByMAC(mac string) (hosts []models.Host) {
func SelectByMAC(table, mac string) (hosts []models.Host) {
tab := db.Table("history")
tab := db.Table(table)
tab.Where("\"MAC\" = ?", mac).Find(&hosts)
return hosts

View file

@ -17,7 +17,6 @@ func Unknown(host models.Host) {
msg := fmt.Sprintf("Unknown host found. Name: '%s', IP: '%s', MAC: '%s', Hw: '%s', Iface: '%s'", host.DNS, host.IP, host.Mac, host.Hw, host.Iface)
slog.Warn(msg)
shout(msg)
}
@ -25,6 +24,7 @@ func Unknown(host models.Host) {
func Test() {
msg := "test notification"
slog.Info("Sending " + msg)
shout(msg)
}

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
import{u as at,t as o,i as e,j as I,b as C,s as K,v as j,w as B,x as st,y as dt,h as N,c as m,e as p,F as ct,z as ot,o as z,k as $t,A as ut,B as bt,C as ht}from"./index.js";import{M as ft}from"./MacHistory.js";var gt=o('<div class="card border-primary"><div class=card-header>Host</div><div class="card-body table-responsive"><table class="table table-striped table-hover"><tbody><tr><td>ID</td><td></td></tr><tr><td>Name</td><td><input type=text class=form-control></td></tr><tr><td>DNS name</td><td></td></tr><tr><td>Iface</td><td></td></tr><tr><td>IP</td><td><a target=_blank></a></td></tr><tr><td>MAC</td><td></td></tr><tr><td>Hardware</td><td></td></tr><tr><td>Date</td><td></td></tr><tr><td>Known</td><td><div class="form-check form-switch"><input class=form-check-input type=checkbox></div></td></tr><tr><td>Online</td><td></td></tr></tbody></table><button type=button class="btn btn-outline-danger">Delete host'),_t=o('<i class="bi bi-check-circle-fill"style=color:var(--bs-success);>'),mt=o('<i class="bi bi-circle-fill"style=color:var(--bs-gray-500);>'),vt=o('<button type=button class="btn btn-outline-success">Wake-on-LAN');function St(t){let l="";const i=at(r=>{j(t.host.ID,r,""),B()},300),n=async r=>{l=r,i(r)},d=async()=>{l==""&&(l=t.host.Name),await j(t.host.ID,l,"toggle"),B()},_=async()=>{await dt(t.host.ID),window.location.href="/"},h=async()=>{await st(t.host.Mac)};return(()=>{var r=gt(),f=r.firstChild,v=f.nextSibling,$=v.firstChild,P=$.firstChild,a=P.firstChild,u=a.firstChild,g=u.nextSibling,s=a.nextSibling,y=s.firstChild,w=y.nextSibling,k=w.firstChild,S=s.nextSibling,c=S.firstChild,b=c.nextSibling,x=S.nextSibling,H=x.firstChild,G=H.nextSibling,M=x.nextSibling,R=M.firstChild,q=R.nextSibling,A=q.firstChild,L=M.nextSibling,J=L.firstChild,Q=J.nextSibling,E=L.nextSibling,U=E.firstChild,V=U.nextSibling,O=E.nextSibling,X=O.firstChild,Y=X.nextSibling,F=O.nextSibling,Z=F.firstChild,tt=Z.nextSibling,et=tt.firstChild,T=et.firstChild,lt=F.nextSibling,it=lt.firstChild,nt=it.nextSibling,rt=$.nextSibling;return e(g,()=>t.host.ID),k.$$input=D=>n(D.target.value),e(b,()=>t.host.DNS),e(G,()=>t.host.Iface),e(A,()=>t.host.IP),e(Q,()=>t.host.Mac),e(V,()=>t.host.Hw),e(Y,()=>t.host.Date),T.$$click=d,e(nt,(()=>{var D=I(()=>t.host.Now==1);return()=>D()?_t():[mt(),"   ",(()=>{var W=vt();return W.$$click=h,W})()]})()),rt.$$click=_,C(()=>K(A,"href","http://"+t.host.IP)),C(()=>k.value=t.host.Name),C(()=>T.checked=t.host.Known==1),r})()}N(["input","click"]);var xt=o('<div class="card border-primary"><div class=card-header>Port Scan</div><div class=card-body><form class=input-group><input type=text class=form-control placeholder=1><input type=text class=form-control placeholder=65535><button type=button class="btn btn-primary">Scan</button></form><div class=mt-2>'),Ct=o('<div class="d-flex justify-content-between mt-2"><button type=button class="btn btn-warning">Stop/Continue</button><div>Scanning port: '),pt=o("<a class=me-4 target=_blank>");function yt(t){let l=!1;const[i,n]=m(""),[d,_]=m(""),[h,r]=m(""),[f,v]=m([]),$=async()=>{l=!1;let a=Number(i());(Number.isNaN(a)||a<1||a>65535)&&(a=1);let u=Number(d());(Number.isNaN(u)||u<1||u>65535)&&(u=65535);let g;for(let s=a;s<=u&&!l;s++)r(s.toString()),g=await ot(t.IP,s),g&&v([...f(),s])},P=()=>{l?(n(h()),$()):l=!0};return(()=>{var a=xt(),u=a.firstChild,g=u.nextSibling,s=g.firstChild,y=s.firstChild,w=y.nextSibling,k=w.nextSibling,S=s.nextSibling;return y.$$input=c=>n(c.target.value),w.$$input=c=>_(c.target.value),k.$$click=$,e(g,(()=>{var c=I(()=>h()!="");return()=>c()?(()=>{var b=Ct(),x=b.firstChild,H=x.nextSibling;return H.firstChild,x.$$click=P,e(H,h,null),b})():[]})(),S),e(S,p(ct,{get each(){return f()},children:c=>(()=>{var b=pt();return e(b,c),C(()=>K(b,"href","http://"+t.IP+":"+c)),b})()})),a})()}N(["input","click"]);var wt=o('<div class="card border-primary"><div class=card-header><div class=input-group style=width:fit-content;><span class=input-group-text>Host History for</span><input type=date class=form-control></div></div><div class=card-body>');function kt(t){const[l,i]=m("");z(()=>{$t(15e3),i(new Date().toLocaleDateString("en-CA"))});const n=d=>{i(""),i(d)};return(()=>{var d=wt(),_=d.firstChild,h=_.firstChild,r=h.firstChild,f=r.nextSibling,v=_.nextSibling;return f.$$input=$=>n($.currentTarget.value),e(v,(()=>{var $=I(()=>t.mac!==""&&l()!=="");return()=>$()?p(ft,{get mac(){return t.mac},get date(){return l()}}):"Loading..."})()),C(()=>f.value=l()),d})()}N(["input"]);var Ht=o("<div class=row><div class=col-md></div><div class=col-md>"),Pt=o('<div class="row mt-4"><div class=col-md>');function Nt(){const[t,l]=m(ut);return z(async()=>{const i=bt(),n=await ht(i.id);l(n)}),[(()=>{var i=Ht(),n=i.firstChild,d=n.nextSibling;return e(n,p(St,{get host(){return t()}})),e(d,p(yt,{get IP(){return t().IP}})),i})(),(()=>{var i=Pt(),n=i.firstChild;return e(n,p(kt,{get mac(){return t().Mac}})),i})()]}export{Nt as default};
import{u as at,t as o,i as e,j as I,b as y,s as B,v as j,w as rt,x as st,h as N,c as m,e as C,F as dt,y as ct,o as K,k as ot,z as $t,A as ut,B as bt}from"./index.js";import{M as ht}from"./MacHistory.js";var ft=o('<div class="card border-primary"><div class=card-header>Host</div><div class="card-body table-responsive"><table class="table table-striped table-hover"><tbody><tr><td>ID</td><td></td></tr><tr><td>Name</td><td><input type=text class=form-control></td></tr><tr><td>DNS name</td><td></td></tr><tr><td>Iface</td><td></td></tr><tr><td>IP</td><td><a target=_blank></a></td></tr><tr><td>MAC</td><td></td></tr><tr><td>Hardware</td><td></td></tr><tr><td>Date</td><td></td></tr><tr><td>Known</td><td><div class="form-check form-switch"><input class=form-check-input type=checkbox></div></td></tr><tr><td>Online</td><td></td></tr></tbody></table><button type=button class="btn btn-outline-danger">Delete host'),gt=o('<i class="bi bi-check-circle-fill"style=color:var(--bs-success);>'),_t=o('<i class="bi bi-circle-fill"style=color:var(--bs-gray-500);>'),mt=o('<button type=button class="btn btn-outline-success">Wake-on-LAN');function vt(t){let l="";const i=at(async a=>{await j(t.host.ID,a,"")},300),n=async a=>{l=a,i(a)},d=async()=>{l==""&&(l=t.host.Name),await j(t.host.ID,l,"toggle")},_=async()=>{await st(t.host.ID),window.location.href="/"},h=async()=>{await rt(t.host.Mac)};return(()=>{var a=ft(),f=a.firstChild,v=f.nextSibling,$=v.firstChild,D=$.firstChild,r=D.firstChild,u=r.firstChild,g=u.nextSibling,s=r.nextSibling,p=s.firstChild,w=p.nextSibling,k=w.firstChild,S=s.nextSibling,c=S.firstChild,b=c.nextSibling,x=S.nextSibling,P=x.firstChild,z=P.nextSibling,M=x.nextSibling,G=M.firstChild,R=G.nextSibling,A=R.firstChild,L=M.nextSibling,q=L.firstChild,J=q.nextSibling,E=L.nextSibling,Q=E.firstChild,U=Q.nextSibling,O=E.nextSibling,V=O.firstChild,X=V.nextSibling,F=O.nextSibling,Y=F.firstChild,Z=Y.nextSibling,tt=Z.firstChild,T=tt.firstChild,et=F.nextSibling,lt=et.firstChild,it=lt.nextSibling,nt=$.nextSibling;return e(g,()=>t.host.ID),k.$$input=H=>n(H.target.value),e(b,()=>t.host.DNS),e(z,()=>t.host.Iface),e(A,()=>t.host.IP),e(J,()=>t.host.Mac),e(U,()=>t.host.Hw),e(X,()=>t.host.Date),T.$$click=d,e(it,(()=>{var H=I(()=>t.host.Now==1);return()=>H()?gt():[_t(),"   ",(()=>{var W=mt();return W.$$click=h,W})()]})()),nt.$$click=_,y(()=>B(A,"href","http://"+t.host.IP)),y(()=>k.value=t.host.Name),y(()=>T.checked=t.host.Known==1),a})()}N(["input","click"]);var St=o('<div class="card border-primary"><div class=card-header>Port Scan</div><div class=card-body><form class=input-group><input type=text class=form-control placeholder=1><input type=text class=form-control placeholder=65535><button type=button class="btn btn-primary">Scan</button></form><div class=mt-2>'),xt=o('<div class="d-flex justify-content-between mt-2"><button type=button class="btn btn-warning">Stop/Continue</button><div>Scanning port: '),yt=o("<a class=me-4 target=_blank>");function Ct(t){let l=!1;const[i,n]=m(""),[d,_]=m(""),[h,a]=m(""),[f,v]=m([]),$=async()=>{l=!1;let r=Number(i());(Number.isNaN(r)||r<1||r>65535)&&(r=1);let u=Number(d());(Number.isNaN(u)||u<1||u>65535)&&(u=65535);let g;for(let s=r;s<=u&&!l;s++)a(s.toString()),g=await ct(t.IP,s),g&&v([...f(),s])},D=()=>{l?(n(h()),$()):l=!0};return(()=>{var r=St(),u=r.firstChild,g=u.nextSibling,s=g.firstChild,p=s.firstChild,w=p.nextSibling,k=w.nextSibling,S=s.nextSibling;return p.$$input=c=>n(c.target.value),w.$$input=c=>_(c.target.value),k.$$click=$,e(g,(()=>{var c=I(()=>h()!="");return()=>c()?(()=>{var b=xt(),x=b.firstChild,P=x.nextSibling;return P.firstChild,x.$$click=D,e(P,h,null),b})():[]})(),S),e(S,C(dt,{get each(){return f()},children:c=>(()=>{var b=yt();return e(b,c),y(()=>B(b,"href","http://"+t.IP+":"+c)),b})()})),r})()}N(["input","click"]);var pt=o('<div class="card border-primary"><div class=card-header><div class=input-group style=width:fit-content;><span class=input-group-text>Host History for</span><input type=date class=form-control></div></div><div class=card-body>');function wt(t){const[l,i]=m("");K(()=>{ot(15e3),i(new Date().toLocaleDateString("en-CA"))});const n=d=>{i(""),i(d)};return(()=>{var d=pt(),_=d.firstChild,h=_.firstChild,a=h.firstChild,f=a.nextSibling,v=_.nextSibling;return f.$$input=$=>n($.currentTarget.value),e(v,(()=>{var $=I(()=>t.mac!==""&&l()!=="");return()=>$()?C(ht,{get mac(){return t.mac},get date(){return l()}}):"Loading..."})()),y(()=>f.value=l()),d})()}N(["input"]);var kt=o("<div class=row><div class=col-md></div><div class=col-md>"),Pt=o('<div class="row mt-4"><div class=col-md>');function It(){const[t,l]=m($t);return K(async()=>{const i=ut(),n=await bt(i.id);l(n)}),[(()=>{var i=kt(),n=i.firstChild,d=n.nextSibling;return e(n,C(vt,{get host(){return t()}})),e(d,C(Ct,{get IP(){return t().IP}})),i})(),(()=>{var i=Pt(),n=i.firstChild;return e(n,C(wt,{get mac(){return t().Mac}})),i})()]}export{It as default};

View file

@ -1,4 +1,4 @@
import{D as y,E as f,G as v,o as m,H,e as u,S as I,t as d,b as D,s as b,I as h,l as M,F as g}from"./index.js";async function w(a,r){let e=[];return r===""?e=await y(a):e=await f(a,r),e!=null?(e.sort((s,t)=>s.Date<t.Date?1:-1),e):[]}var x=d("<i>");function G(a){const[r,e]=v([]);let s;return m(async()=>{const t=await w(a.mac,a.date);e(t),s=setInterval(async()=>{const o=await w(a.mac,a.date);e(o)},6e4)}),H(()=>{clearInterval(s)}),u(g,{each:r,children:(t,o)=>u(I,{get when(){return o()<M()},get children(){var i=x();return D(n=>{var c="Date:"+t.Date+`
import{C as y,D as f,E as v,o as m,G as H,e as u,S as d,t as D,b as I,s as b,H as h,l as M,F as g}from"./index.js";async function w(a,r){let e=[];return r===""?e=await y(a):e=await f(a,r),e!=null?(e.sort((s,t)=>s.Date<t.Date?1:-1),e):[]}var x=D("<i>");function F(a){const[r,e]=v([]);let s;return m(async()=>{const t=await w(a.mac,a.date);e(t),s=setInterval(async()=>{const o=await w(a.mac,a.date);e(o)},6e4)}),H(()=>{clearInterval(s)}),u(g,{each:r,children:(t,o)=>u(d,{get when(){return o()<M()},get children(){var i=x();return I(n=>{var c="Date:"+t.Date+`
Iface:`+t.Iface+`
IP:`+t.IP+`
Known:`+t.Known,l=t.Now===0?"my-box-off":"my-box-on";return c!==n.e&&b(i,"title",n.e=c),l!==n.t&&h(i,n.t=l),n},{e:void 0,t:void 0}),i}})})}export{G as M};
Known:`+t.Known,l=t.Now===0?"my-box-off":"my-box-on";return c!==n.e&&b(i,"title",n.e=c),l!==n.t&&h(i,n.t=l),n},{e:void 0,t:void 0}),i}})})}export{F as M};

File diff suppressed because one or more lines are too long

View file

@ -38,7 +38,7 @@ function CardHead() {
when={editNames()}
fallback={<button class="btn btn-outline-primary" title="Toggle edit" onClick={[handleEditNames, true]}>Edit</button>}
>
<button type="button" onClick={handleDel} class="btn btn-outline-danger">Delete selected</button>
<button type="button" onClick={handleDel} title="Delete selected hosts" class="btn btn-outline-danger">Delete selected</button>
<button class="btn btn-primary" title="Toggle edit" onClick={[handleEditNames, false]}>Edit</button>
</Show>
</div>

View file

@ -36,7 +36,7 @@ function TableHead() {
title={"Sort by " + key}
></i></th>
}</For>
<th style="width: 2em;"></th>
<th style="width: 2em;" title="Edit"><i class="bi bi-pencil-fill"></i></th>
</tr>
</thead>
)

View file

@ -16,8 +16,8 @@ function TableRow(_props: any) {
let known:boolean;
_props.host.Known === 1 ? known = true : known = false;
const debouncedApi = debounce((val: string) => {
apiEditHost(_props.host.ID, val, "");
const debouncedApi = debounce(async (val: string) => {
await apiEditHost(_props.host.ID, val, "");
}, 300);
const handleInput = async (n: string) => {

View file

@ -17,7 +17,7 @@ function About() {
<div class="card-header">
About (<a href={link()} target="_blank">{version()}</a>)
</div>
<div class="card-body">
<div class="card-body table-responsive">
<table class="table table-striped"><tbody>
<tr>
<td><b>Swagger API docs</b></td>

View file

@ -71,7 +71,7 @@ function Scan() {
</tr>
<tr>
<td><button type="submit" class="btn btn-primary">Save</button></td>
<td></td>
<td class="text-muted">*Pressing <b>Save</b> button will trigger rescan</td>
</tr>
</tbody></table>
</form>

View file

@ -1,5 +1,4 @@
import { apiDelHost, apiEditHost, apiWOL } from "../../functions/api";
import { getHosts } from "../../functions/atstart";
import { debounce } from "@solid-primitives/scheduled";
@ -7,10 +6,8 @@ function HostCard(_props: any) {
let name:string = "";
const debouncedApi = debounce((val: string) => {
apiEditHost(_props.host.ID, val, "");
getHosts();
const debouncedApi = debounce(async (val: string) => {
await apiEditHost(_props.host.ID, val, "");
}, 300);
const handleInput = async (n: string) => {
@ -26,7 +23,6 @@ function HostCard(_props: any) {
}
await apiEditHost(_props.host.ID, name, 'toggle');
getHosts();
};
const handleDel = async () => {

View file

@ -1,13 +1,18 @@
import { For } from "solid-js";
import { For, onMount } from "solid-js";
import { allHosts } from "../functions/exports";
import TableRow from "../components/Body/TableRow";
import TableHead from "../components/Body/TableHead";
import CardHead from "../components/Body/CardHead";
import { getHosts } from "../functions/atstart";
function Body() {
onMount(() => {
getHosts();
});
return (
<div class="card border-primary">
<div class="card-header">