Compare commits

...

4 commits

Author SHA1 Message Date
a67a3fdc87
fix: MYSQL_USERNAME -> MYSQL_USER
feat: add client
2026-02-25 09:30:22 +01:00
7e934d16f7
feat: add api 2026-02-24 22:12:46 +01:00
55ad3c2aed
feat: add host and hosts pages 2026-02-24 20:11:32 +01:00
f2e2f45a19
init 2026-02-24 09:21:41 +01:00
36 changed files with 1289 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/borgmatic-monitor
/env.sh
/example.json

5
Dockerfile Normal file
View file

@ -0,0 +1,5 @@
FROM alpine
COPY ./build/bgm-server /usr/bin/
ENTRYPOINT ["/usr/bin/bgm-server"]

18
Makefile Normal file
View file

@ -0,0 +1,18 @@
swagger:
swag init -d "cmd/server/,pkg/web/" -g main.go
watch-server: swagger
MYSQL_DATABASE=borgmaticmon gowatch -p ./cmd/server/
watch-client:
gowatch -p ./cmd/client/ -args="-e,http://localhost:1323/api/v1/borgmatic/infos,-h,apps,-c,env.sh"
.PHONY:
bins:
mkdir -p build/
CGO_ENABLED=0 go build -o ./build/bgm-server ./cmd/server
CGO_ENABLED=0 go build -o ./build/bgm-client ./cmd/client
.PHONY:
docker: bins
docker buildx build -t gitnet.fr/deblan/bgm-server --push .

BIN
build/bgm-client Executable file

Binary file not shown.

BIN
build/bgm-server Executable file

Binary file not shown.

92
cmd/client/main.go Normal file
View file

@ -0,0 +1,92 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"log"
"net/http"
"os"
"os/exec"
"github.com/urfave/cli/v3"
"gitnet.fr/deblan/borgmatic-monitor/pkg/web/api"
)
func main() {
cmd := &cli.Command{
Flags: []cli.Flag{
&cli.StringFlag{
Name: "endpoint",
Aliases: []string{"e"},
Value: "",
Usage: "Endpoint of the monitor",
},
&cli.StringFlag{
Name: "hostname",
Aliases: []string{"h"},
Value: "",
Usage: "Hostname",
},
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Value: "",
Usage: "Borgmatic config file",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
var buff bytes.Buffer
c := exec.Command(
"borgmatic",
"info",
"-c", cmd.String("config"),
"--json",
)
c.Stderr = os.Stderr
c.Stdout = &buff
err := c.Run()
if err != nil {
return err
}
input := api.BorgmaticInfos{
Hostname: cmd.String("hostname"),
Infos: buff.String(),
}
j, err := json.Marshal(input)
if err != nil {
return err
}
req, err := http.NewRequest("POST", cmd.String("endpoint"), bytes.NewBuffer(j))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return errors.New("Invalid response code")
}
return nil
},
}
if err := cmd.Run(context.Background(), os.Args); err != nil {
log.Fatal(err)
}
}

38
cmd/server/main.go Normal file
View file

@ -0,0 +1,38 @@
package main
import (
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
echoSwagger "github.com/swaggo/echo-swagger"
"gitnet.fr/deblan/borgmatic-monitor/docs"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database/model"
"gitnet.fr/deblan/borgmatic-monitor/pkg/web"
)
func main() {
database.Init()
db := database.GetDb()
db.AutoMigrate(model.Host{})
db.AutoMigrate(model.Info{})
e := echo.New()
e.Use(middleware.RequestLogger())
e.GET("/", web.Hosts)
e.GET("/host/:id", web.Host)
e.POST("/api/v1/borgmatic/infos", web.ApiBorgmaticInfos)
docs.SwaggerInfo.Title = "Borgmatic monitor"
docs.SwaggerInfo.Description = "API"
docs.SwaggerInfo.Version = "1.0"
docs.SwaggerInfo.BasePath = "/"
docs.SwaggerInfo.Schemes = []string{"http", "https"}
e.GET("/swagger/*", echoSwagger.WrapHandler)
if err := e.Start(":1323"); err != nil {
e.Logger.Error("failed to start server", "error", err)
}
}

80
docs/docs.go Normal file
View file

@ -0,0 +1,80 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/api/v1/borgmatic/infos": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Create a borgmatic infos",
"parameters": [
{
"description": "Object",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.BorgmaticInfos"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
}
}
}
}
},
"definitions": {
"api.BorgmaticInfos": {
"type": "object",
"properties": {
"hostname": {
"type": "string"
},
"infos": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "",
Host: "",
BasePath: "",
Schemes: []string{},
Title: "",
Description: "",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

51
docs/swagger.json Normal file
View file

@ -0,0 +1,51 @@
{
"swagger": "2.0",
"info": {
"contact": {}
},
"paths": {
"/api/v1/borgmatic/infos": {
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"summary": "Create a borgmatic infos",
"parameters": [
{
"description": "Object",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/api.BorgmaticInfos"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
}
}
}
}
},
"definitions": {
"api.BorgmaticInfos": {
"type": "object",
"properties": {
"hostname": {
"type": "string"
},
"infos": {
"type": "string"
}
}
}
}
}

31
docs/swagger.yaml Normal file
View file

@ -0,0 +1,31 @@
definitions:
api.BorgmaticInfos:
properties:
hostname:
type: string
infos:
type: string
type: object
info:
contact: {}
paths:
/api/v1/borgmatic/infos:
post:
consumes:
- application/json
parameters:
- description: Object
in: body
name: request
required: true
schema:
$ref: '#/definitions/api.BorgmaticInfos'
produces:
- application/json
responses:
"200":
description: OK
"400":
description: Bad Request
summary: Create a borgmatic infos
swagger: "2.0"

51
go.mod
View file

@ -1,3 +1,54 @@
module gitnet.fr/deblan/borgmatic-monitor
go 1.25.1
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/labstack/echo/v5 v5.0.4 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/sv-tools/openapi v0.4.0 // indirect
github.com/swaggo/echo-swagger v1.5.0 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
github.com/swaggo/swag v1.16.6 // indirect
github.com/swaggo/swag/v2 v2.0.0-rc5 // indirect
github.com/urfave/cli/v2 v2.27.5 // indirect
github.com/urfave/cli/v3 v3.6.2 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.42.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.6.0 // indirect
gorm.io/gorm v1.31.1 // indirect
maragu.dev/gomponents v1.2.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)

150
go.sum Normal file
View file

@ -0,0 +1,150 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo/v5 v5.0.4 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc=
github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/sv-tools/openapi v0.4.0 h1:UhD9DVnGox1hfTePNclpUzUFgos57FvzT2jmcAuTOJ4=
github.com/sv-tools/openapi v0.4.0/go.mod h1:kD/dG+KP0+Fom1r6nvcj/ORtLus8d8enXT6dyRZDirE=
github.com/swaggo/echo-swagger v1.5.0 h1:nkHxOaBy0SkbJMtMeXZC64KHSa0mJdZFQhVqwEcMres=
github.com/swaggo/echo-swagger v1.5.0/go.mod h1:TzO363X1ZG/MSbjrG2IX6m65Yd3/zpqh5KM6lPctAhk=
github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU=
github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/swaggo/swag/v2 v2.0.0-rc5 h1:fK7d6ET9rrEsdB8IyuwXREWMcyQN3N7gawGFbbrjgHk=
github.com/swaggo/swag/v2 v2.0.0-rc5/go.mod h1:kCL8Fu4Zl8d5tB2Bgj96b8wRowwrwk175bZHXfuGVFI=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
maragu.dev/gomponents v1.2.0 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc=
maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

15
pkg/borgmatic/archive.go Normal file
View file

@ -0,0 +1,15 @@
package borgmatic
type Archive struct {
CommandLine []string `json:"command_line"`
Comment string `json:"comment"`
Duration float64 `json:"duration"`
Start *Time `json:"start"`
End *Time `json:"end"`
Hostname string `json:"hostname"`
Id string `json:"id"`
Limits ArchiveLimits `json:"limits"`
Stats ArchiveStats `json:"stats"`
Name string `json:"name"`
Username string `json:"username"`
}

View file

@ -0,0 +1,5 @@
package borgmatic
type ArchiveLimits struct {
MaxArchiveSize float64 `json:"max_archive_size"`
}

View file

@ -0,0 +1,8 @@
package borgmatic
type ArchiveStats struct {
CompressedSize int64 `json:"compressed_size"`
DeduplicatedSize int64 `json:"deduplicated_size"`
Nfiles int64 `json:"nfiles"`
OriginalSize int64 `json:"original_size"`
}

6
pkg/borgmatic/cache.go Normal file
View file

@ -0,0 +1,6 @@
package borgmatic
type Cache struct {
Path string `json:"path"`
Stats CacheStats `json:"stats"`
}

View file

@ -0,0 +1,10 @@
package borgmatic
type CacheStats struct {
TotalChunks float64 `json:"total_chunks"`
TotalCsize float64 `json:"total_csize"`
TotalSize float64 `json:"total_size"`
TotalUniqueChunks float64 `json:"total_unique_chunks"`
UniqueCsize float64 `json:"unique_csize"`
UniqueSize float64 `json:"unique_size"`
}

View file

@ -0,0 +1,5 @@
package borgmatic
type Encryption struct {
Mode string `json:"mode"`
}

8
pkg/borgmatic/info.go Normal file
View file

@ -0,0 +1,8 @@
package borgmatic
type Info struct {
Archives []Archive `json:"archives"`
Cache Cache `json:"cache"`
Encryption Encryption `json:"encryption"`
Repository Repository `json:"repository"`
}

3
pkg/borgmatic/infos.go Normal file
View file

@ -0,0 +1,3 @@
package borgmatic
type Infos []Info

View file

@ -0,0 +1,8 @@
package borgmatic
type Repository struct {
Id string `json:"id"`
LastModified *Time `json:"last_modified"`
Location string `json:"location"`
Label string `json:"label"`
}

24
pkg/borgmatic/time.go Normal file
View file

@ -0,0 +1,24 @@
package borgmatic
import (
"strings"
"time"
)
type Time struct {
time.Time
}
func (t *Time) UnmarshalJSON(b []byte) (err error) {
s := strings.Trim(string(b), "\"")
if s == "null" {
t.Time = time.Time{}
return
}
t.Time, err = time.Parse("2006-01-02T15:04:05.000000", s)
return
}

68
pkg/database/database.go Normal file
View file

@ -0,0 +1,68 @@
package database
import (
"fmt"
"os"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var (
db *gorm.DB
config mysql.Config
)
func Init() {
config = mysql.Config{
DSN: fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local",
fallback(os.Getenv("MYSQL_USER"), "root"),
fallback(os.Getenv("MYSQL_PASSWORD"), "root"),
fallback(os.Getenv("MYSQL_HOST"), "127.0.0.1"),
fallback(os.Getenv("MYSQL_PORT"), "3306"),
fallback(os.Getenv("MYSQL_DATABASE"), "app"),
),
DefaultStringSize: 256,
DisableDatetimePrecision: true,
DontSupportRenameIndex: true,
DontSupportRenameColumn: true,
SkipInitializeWithVersion: false,
}
}
func GetDb() *gorm.DB {
var err error
if db == nil {
db, err = gorm.Open(mysql.New(config))
if err != nil {
panic(err)
}
}
gdb, err := db.DB()
if err != nil {
db = nil
return GetDb()
}
if err = gdb.Ping(); err != nil {
db = nil
return GetDb()
}
return db
}
func fallback(value string, fb string) string {
if value == "" {
return fb
}
return value
}

80
pkg/database/host.go Normal file
View file

@ -0,0 +1,80 @@
package database
import (
"math"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database/model"
"gorm.io/gorm"
)
func Hosts() []model.Host {
var hosts []model.Host
GetDb().
Model(model.Host{}).
Preload("Infos", func(db *gorm.DB) *gorm.DB {
return db.Order("infos.id DESC")
}).
Order("name ASC").
Find(&hosts)
return hosts
}
func Host(id int) *model.Host {
var host model.Host
GetDb().
Model(model.Host{}).
Order("name ASC").
Where("id = ?", id).
First(&host)
if host.ID > 0 {
return &host
}
return nil
}
func HostByName(label string) *model.Host {
var host model.Host
GetDb().
Model(model.Host{}).
Order("name ASC").
Where("name = ?", label).
First(&host)
if host.ID > 0 {
return &host
}
return nil
}
func HostInfos(host *model.Host, page, itemPerPage int) *[]model.Info {
var infos []model.Info
GetDb().
Model(model.Info{}).
Order("infos.id DESC").
Offset((page-1)*itemPerPage).
Limit(itemPerPage).
Where("host_id = ?", host.ID).
Find(&infos)
return &infos
}
func HostInfosMaxPages(host *model.Host, itemPerPage int) int {
var count int64
GetDb().
Model(model.Info{}).
Order("infos.id DESC").
Where("host_id = ?", host.ID).
Count(&count)
return int(math.Ceil(float64(count) / float64(itemPerPage)))
}

View file

@ -0,0 +1,35 @@
package model
import (
"time"
"gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic"
"gorm.io/gorm"
)
type Host struct {
gorm.Model
Name string `gorm:"not null"`
Token string `gorm:"not null"`
Infos []Info `gorm:"one2many:info;not null"`
}
func (h *Host) LastArchive() *borgmatic.Archive {
var last *borgmatic.Archive
var lastDate *time.Time
for _, item := range h.Infos {
if infos := *item.Infos(); infos != nil {
for _, info := range infos {
for _, a := range info.Archives {
if lastDate == nil || a.End.After(*lastDate) {
last = &a
}
}
}
}
}
return last
}

View file

@ -0,0 +1,28 @@
package model
import (
"encoding/json"
"gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic"
"gorm.io/gorm"
)
type Info struct {
gorm.Model
HostID uint
Host Host `gorm:"many2one:info"`
Data string `gorm:"type:text;not null"`
}
func (i *Info) Infos() *borgmatic.Infos {
var infos borgmatic.Infos
err := json.Unmarshal([]byte(i.Data), &infos)
if err != nil {
return nil
}
return &infos
}

View file

@ -0,0 +1,6 @@
package api
type BorgmaticInfos struct {
Hostname string `json:"hostname"`
Infos string `json:"infos"`
}

View file

@ -0,0 +1,49 @@
package web
import (
"encoding/json"
"net/http"
"github.com/labstack/echo/v5"
"gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database/model"
"gitnet.fr/deblan/borgmatic-monitor/pkg/web/api"
)
// @Summary Create a borgmatic infos
// @Accept json
// @Produce json
// @Success 200
// @Success 400
// @Router /api/v1/borgmatic/infos [post]
// @Param request body api.BorgmaticInfos true "Object"
func ApiBorgmaticInfos(c *echo.Context) error {
value := new(api.BorgmaticInfos)
if err := c.Bind(value); err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
host := database.HostByName(value.Hostname)
if host == nil {
return c.JSON(http.StatusBadRequest, "Host not found")
}
var infos borgmatic.Infos
err := json.Unmarshal([]byte(value.Infos), &infos)
if err != nil {
return c.JSON(http.StatusBadRequest, err.Error())
}
info := new(model.Info)
info.HostID = host.ID
info.Data = value.Infos
database.GetDb().Save(info)
return c.JSON(200, "ok")
}

83
pkg/web/host.go Normal file
View file

@ -0,0 +1,83 @@
package web
import (
"fmt"
"math"
"net/http"
"github.com/labstack/echo/v5"
"github.com/spf13/cast"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database"
"gitnet.fr/deblan/borgmatic-monitor/pkg/web/partial"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func Host(c *echo.Context) error {
id := cast.ToInt(c.Param("id"))
if id == 0 {
return c.HTML(http.StatusNotFound, "Not found.")
}
host := database.Host(id)
if host == nil {
return c.HTML(http.StatusNotFound, "Not found.")
}
type params struct {
Page float64 `query:"page"`
}
var p params
maxPerPages := 2
maxPages := database.HostInfosMaxPages(host, maxPerPages)
if err := c.Bind(&p); err != nil {
p.Page = 1
}
page := int(math.Min(math.Max(p.Page, 1), float64(maxPages)))
infos := database.HostInfos(host, page, maxPerPages)
var nodes []Node
if len(*infos) == 0 {
nodes = append(
nodes,
Div(
Class("text-muted"),
Text("Nothing yet!"),
),
)
} else {
for _, item := range *infos {
if infos := *item.Infos(); infos != nil {
for i := len(infos) - 1; i >= 0; i-- {
nodes = append(nodes, partial.BorgmaticInfo(infos[i]))
}
}
}
}
return c.HTML(http.StatusOK, Page(
fmt.Sprintf("host_%d", host.ID),
host.Name,
Div(
Class("container-fluid"),
Div(
Class("d-flex justify-content-end"),
partial.Paginate(fmt.Sprintf("/host/%d?page=%%page%%", host.ID), page, maxPages),
),
Div(
Class("row"),
Map(nodes, func(node Node) Node {
return node
}),
),
),
))
}

31
pkg/web/hosts.go Normal file
View file

@ -0,0 +1,31 @@
package web
import (
"net/http"
"github.com/labstack/echo/v5"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database"
"gitnet.fr/deblan/borgmatic-monitor/pkg/web/partial"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func Hosts(c *echo.Context) error {
var nodes []Node
for _, host := range database.Hosts() {
nodes = append(nodes, partial.Host(host))
}
return c.HTML(http.StatusOK, Page(
"home",
"Hosts",
Div(
Class("container-fluid"),
Div(
Class("row"),
Group(nodes),
),
),
))
}

50
pkg/web/page.go Normal file
View file

@ -0,0 +1,50 @@
package web
import (
"bytes"
"gitnet.fr/deblan/borgmatic-monitor/pkg/web/partial"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/components"
. "maragu.dev/gomponents/html"
)
func Page(currentNode string, title string, children ...Node) string {
page := HTML5(HTML5Props{
Title: title,
Language: "en",
Head: []Node{
Link(
Href("https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"),
Rel("stylesheet"),
Integrity("sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"),
CrossOrigin("anonymous"),
),
Link(
Href("https://cdn.jsdelivr.net/npm/remixicon@4.9.0/fonts/remixicon.css"),
Rel("stylesheet"),
),
},
Body: []Node{
Main(
Class("d-flex flex-nowrap"),
partial.NavMenu(currentNode),
Div(
Class("flex-fill p-3"),
Style("max-height: 100vh; overflow: auto"),
Group(children),
),
),
Script(
Src("https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"),
Integrity("sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"),
),
},
})
var buf bytes.Buffer
page.Render(&buf)
return buf.String()
}

View file

@ -0,0 +1,47 @@
package partial
import (
"fmt"
"github.com/dustin/go-humanize"
"gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func Archive(archive borgmatic.Archive) Node {
return Div(
Class("row"),
Div(
Class("col-12 col-md-4"),
I(Class("ri-store-3-fill me-1")),
Text(archive.Name),
),
Div(
Class("text-end col-12 col-md-4"),
Text(fmt.Sprintf("%2.fs", archive.Duration)),
I(Class("ri-time-fill mx-2")),
Text(fmt.Sprintf("%d", archive.Stats.Nfiles)),
I(Class("ri-file-fill ms-2")),
),
Div(
Class("text-end col-12 col-md-4"),
Div(
Class("row"),
Div(
Class("col-4 text-secondary"),
Text(humanize.Bytes(uint64(archive.Stats.OriginalSize))),
),
Div(
Class("col-4 text-primary"),
Text(humanize.Bytes(uint64(archive.Stats.DeduplicatedSize))),
),
Div(
Class("col-4 text-success"),
Text(humanize.Bytes(uint64(archive.Stats.CompressedSize))),
I(Class("ri-file-zip-fill ms-2")),
),
),
),
)
}

View file

@ -0,0 +1,39 @@
package partial
import (
"gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func BorgmaticInfo(info borgmatic.Info) Node {
return Div(
Class("col-12"),
Div(
Class("card mb-3"),
Div(
Class("card-header"),
Text(info.Repository.LastModified.Format("2006-01-02 15:04:05")),
),
Div(
Class("card-body"),
Div(
I(Class("ri-price-tag-3-line me-1")),
Text(info.Repository.Label),
),
Div(
I(Class("ri-map-pin-line me-1")),
Text(info.Repository.Location),
),
Div(
Class("mb-3"),
I(Class("ri-lock-line me-1")),
Text(info.Encryption.Mode),
),
Div(
Map(info.Archives, Archive),
),
),
),
)
}

64
pkg/web/partial/host.go Normal file
View file

@ -0,0 +1,64 @@
package partial
import (
"fmt"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database/model"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func Host(host model.Host) Node {
lastArchive := host.LastArchive()
var body Node
if lastArchive != nil {
pushes := len(host.Infos)
body = Div(
Div(
Class("fw-bold"),
I(Class("ri-archive-stack-line me-1")),
If(pushes <= 1, Text(fmt.Sprintf("%d push", pushes))),
If(pushes > 1, Text(fmt.Sprintf("%d pushes", pushes))),
),
Div(
I(Class("ri-calendar-line me-1")),
Text(fmt.Sprintf(
"Last update on %s",
lastArchive.End.Format("2006-01-02 15:04:05"),
)),
),
Div(
Class("mt-3"),
A(
Class("btn btn-primary"),
Href(fmt.Sprintf("/host/%d", host.ID)),
Text("Show"),
),
),
)
} else {
body = Div(
Class("text-danger"),
I(Class("ri-error-warning-line me-1")),
Text("Nothing yet!"),
)
}
return Div(
Class("col-6"),
Div(
Class("card mb-3"),
Div(
Class("card-header"),
Text(host.Name),
),
Div(
Class("card-body"),
body,
),
),
)
}

56
pkg/web/partial/page.go Normal file
View file

@ -0,0 +1,56 @@
package partial
import (
"fmt"
"gitnet.fr/deblan/borgmatic-monitor/pkg/database"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func NavMenu(currentNode string) Node {
items := [][3]string{
[3]string{"home", "/", "All"},
}
for _, host := range database.Hosts() {
items = append(
items,
[3]string{
fmt.Sprintf("host_%d", host.ID),
fmt.Sprintf("/host/%d", host.ID),
host.Name,
},
)
}
return Div(
Class("d-flex flex-column flex-shrink-0 p-3 text-bg-dark"),
Style("width: 280px; height: 100vh"),
Ul(
Class("nav nav-pills flex-column mb-auto"),
Map(items, func(item [3]string) Node {
return NavMenuItem(item, currentNode == item[0])
}),
),
)
}
func NavMenuItem(item [3]string, isActive bool) Node {
class := "nav-link"
if isActive {
class += " active"
} else {
class += " text-white"
}
return Li(
Class("nav-item"),
A(
Class(class),
Href(item[1]),
Text(item[2]),
),
)
}

42
pkg/web/partial/pager.go Normal file
View file

@ -0,0 +1,42 @@
package partial
import (
"fmt"
"strings"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func Paginate(path string, currentPage int, pages int) Node {
return Nav(
Attr("arial-label", "Pagination"),
Ul(
Class("pagination"),
If(
currentPage > 1,
Li(
Class("page-item"),
A(
Class("page-link"),
Attr("arial-label", "Previous"),
Href(strings.Replace(path, "%page%", fmt.Sprintf("%d", currentPage-1), -1)),
Span(Attr("arial-hidden", "true"), Raw("&laquo;")),
),
),
),
If(
currentPage < pages,
Li(
Class("page-item"),
A(
Class("page-link"),
Attr("arial-label", "Next"),
Href(strings.Replace(path, "%page%", fmt.Sprintf("%d", currentPage+1), -1)),
Span(Attr("arial-hidden", "true"), Raw("&raquo;")),
),
),
),
),
)
}