forked from mirrors/homebox
feat: use notifiers on schedule (#362)
* fix potential memory leak with time.After * add new background service to manage scheduled notifications * update docs * remove old js reference * closes #278 * tidy
This commit is contained in:
parent
975e636fb6
commit
840d220d4f
16 changed files with 201 additions and 52 deletions
11
README.md
11
README.md
|
@ -16,10 +16,13 @@
|
|||
[Configuration & Docker Compose](https://hay-kot.github.io/homebox/quick-start)
|
||||
|
||||
```bash
|
||||
docker run --name=homebox \
|
||||
--restart=always \
|
||||
--publish=3100:7745 \
|
||||
ghcr.io/hay-kot/homebox:latest
|
||||
docker run -d \
|
||||
--name homebox \
|
||||
--restart unless-stopped \
|
||||
--publish 3100:7745 \
|
||||
--env TZ=Europe/Bucharest \
|
||||
--volume /path/to/data/folder/:/data \
|
||||
ghcr.io/hay-kot/homebox:latest
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
|
|
@ -37,8 +37,11 @@ func new(conf *config.Config) *app {
|
|||
}
|
||||
|
||||
func (a *app) startBgTask(t time.Duration, fn func()) {
|
||||
timer := time.NewTimer(t)
|
||||
|
||||
for {
|
||||
timer.Reset(t)
|
||||
a.server.Background(fn)
|
||||
time.Sleep(t)
|
||||
<-timer.C
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -168,6 +169,19 @@ func run(cfg *config.Config) error {
|
|||
Msg("failed to purge expired invitations")
|
||||
}
|
||||
})
|
||||
go app.startBgTask(time.Duration(1)*time.Hour, func() {
|
||||
now := time.Now()
|
||||
|
||||
if now.Hour() == 8 {
|
||||
fmt.Println("run notifiers")
|
||||
err := app.services.BackgroundService.SendNotifiersToday(context.Background())
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Msg("failed to send notifiers")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: Remove through external API that does setup
|
||||
if cfg.Demo {
|
||||
|
|
|
@ -18,7 +18,6 @@ require (
|
|||
github.com/rs/zerolog v1.29.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/http-swagger/v2 v2.0.0
|
||||
github.com/swaggo/swag v1.8.11
|
||||
github.com/yeqown/go-qrcode/v2 v2.2.1
|
||||
github.com/yeqown/go-qrcode/writer/standard v1.2.1
|
||||
|
|
|
@ -183,8 +183,6 @@ cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuW
|
|||
cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
|
||||
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
entgo.io/ent v0.11.8 h1:M/M0QL1CYCUSdqGRXUrXhFYSDRJPsOOrr+RLEej/gyQ=
|
||||
entgo.io/ent v0.11.8/go.mod h1:ericBi6Q8l3wBH1wEIDfKxw7rcQEuRPyBfbIzjtxJ18=
|
||||
entgo.io/ent v0.11.10 h1:iqn32ybY5HRW3xSAyMNdNKpZhKgMf1Zunsej9yPKUI8=
|
||||
entgo.io/ent v0.11.10/go.mod h1:mzTZ0trE+jCQw/fnzijbm5Mck/l8Gbg7gC/+L1COyzM=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
|
@ -620,9 +618,6 @@ github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
|
|||
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
|
||||
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
|
||||
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
|
||||
github.com/swaggo/http-swagger/v2 v2.0.0/go.mod h1:XYhrQVIKz13CxuKD4p4kvpaRB4jJ1/MlfQXVOE+CX8Y=
|
||||
github.com/swaggo/swag v1.8.10 h1:eExW4bFa52WOjqRzRD58bgWsWfdFJso50lpbeTcmTfo=
|
||||
github.com/swaggo/swag v1.8.10/go.mod h1:ezQVUUhly8dludpVk+/PuwJWvLLanB13ygV5Pr9enSk=
|
||||
github.com/swaggo/swag v1.8.11 h1:Fp1dNNtDvbCf+8kvehZbHQnlF6AxHGjmw6H/xAMrZfY=
|
||||
github.com/swaggo/swag v1.8.11/go.mod h1:2GXgpNI9iy5OdsYWu8zXfRAGnOAPxYxTWTyM0XOTYZQ=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
|
@ -991,8 +986,6 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.6.1-0.20230222164832-25d2519c8696 h1:8985/C5IvACpd9DDXckSnjSBLKDgbxXiyODgi94zOPM=
|
||||
golang.org/x/tools v0.6.1-0.20230222164832-25d2519c8696/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
|
@ -5,9 +5,10 @@ import (
|
|||
)
|
||||
|
||||
type AllServices struct {
|
||||
User *UserService
|
||||
Group *GroupService
|
||||
Items *ItemService
|
||||
User *UserService
|
||||
Group *GroupService
|
||||
Items *ItemService
|
||||
BackgroundService *BackgroundService
|
||||
}
|
||||
|
||||
type OptionsFunc func(*options)
|
||||
|
@ -42,5 +43,6 @@ func New(repos *repo.AllRepos, opts ...OptionsFunc) *AllServices {
|
|||
repo: repos,
|
||||
autoIncrementAssetID: options.autoIncrementAssetID,
|
||||
},
|
||||
BackgroundService: &BackgroundService{repos},
|
||||
}
|
||||
}
|
||||
|
|
81
backend/internal/core/services/service_background.go
Normal file
81
backend/internal/core/services/service_background.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/repo"
|
||||
"github.com/hay-kot/homebox/backend/internal/data/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type BackgroundService struct {
|
||||
repos *repo.AllRepos
|
||||
}
|
||||
|
||||
func (svc *BackgroundService) SendNotifiersToday(ctx context.Context) error {
|
||||
// Get All Groups
|
||||
groups, err := svc.repos.Groups.GetAllGroups(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
today := types.DateFromTime(time.Now())
|
||||
|
||||
for i := range groups {
|
||||
group := groups[i]
|
||||
|
||||
entries, err := svc.repos.MaintEntry.GetScheduled(ctx, group.ID, today)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
log.Debug().
|
||||
Str("group_name", group.Name).
|
||||
Str("group_id", group.ID.String()).
|
||||
Msg("No scheduled maintenance for today")
|
||||
continue
|
||||
}
|
||||
|
||||
notifiers, err := svc.repos.Notifiers.GetByGroup(ctx, group.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
urls := make([]string, len(notifiers))
|
||||
for i := range notifiers {
|
||||
urls[i] = notifiers[i].URL
|
||||
}
|
||||
|
||||
bldr := strings.Builder{}
|
||||
|
||||
bldr.WriteString("Homebox Maintenance for (")
|
||||
bldr.WriteString(today.String())
|
||||
bldr.WriteString("):\n")
|
||||
|
||||
for i := range entries {
|
||||
entry := entries[i]
|
||||
bldr.WriteString(" - ")
|
||||
bldr.WriteString(entry.Name)
|
||||
bldr.WriteString("\n")
|
||||
}
|
||||
|
||||
var sendErrs []error
|
||||
for i := range urls {
|
||||
err := shoutrrr.Send(urls[i], bldr.String())
|
||||
|
||||
if err != nil {
|
||||
sendErrs = append(sendErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sendErrs) > 0 {
|
||||
return sendErrs[0]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -33,6 +33,8 @@ func (MaintenanceEntry) Fields() []ent.Field {
|
|||
Optional(),
|
||||
field.Float("cost").
|
||||
Default(0.0),
|
||||
field.Bool("reminders_enabled").
|
||||
Default(false),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,36 @@ import (
|
|||
)
|
||||
|
||||
type GroupRepository struct {
|
||||
db *ent.Client
|
||||
db *ent.Client
|
||||
groupMapper MapFunc[*ent.Group, Group]
|
||||
invitationMapper MapFunc[*ent.GroupInvitationToken, GroupInvitation]
|
||||
}
|
||||
|
||||
func NewGroupRepository(db *ent.Client) *GroupRepository {
|
||||
gmap := func(g *ent.Group) Group {
|
||||
return Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
Currency: strings.ToUpper(g.Currency.String()),
|
||||
}
|
||||
}
|
||||
|
||||
imap := func(i *ent.GroupInvitationToken) GroupInvitation {
|
||||
return GroupInvitation{
|
||||
ID: i.ID,
|
||||
ExpiresAt: i.ExpiresAt,
|
||||
Uses: i.Uses,
|
||||
Group: gmap(i.Edges.Group),
|
||||
}
|
||||
}
|
||||
|
||||
return &GroupRepository{
|
||||
db: db,
|
||||
groupMapper: gmap,
|
||||
invitationMapper: imap,
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
|
@ -76,27 +105,8 @@ type (
|
|||
}
|
||||
)
|
||||
|
||||
var mapToGroupErr = mapTErrFunc(mapToGroup)
|
||||
|
||||
func mapToGroup(g *ent.Group) Group {
|
||||
return Group{
|
||||
ID: g.ID,
|
||||
Name: g.Name,
|
||||
CreatedAt: g.CreatedAt,
|
||||
UpdatedAt: g.UpdatedAt,
|
||||
Currency: strings.ToUpper(g.Currency.String()),
|
||||
}
|
||||
}
|
||||
|
||||
var mapToGroupInvitationErr = mapTErrFunc(mapToGroupInvitation)
|
||||
|
||||
func mapToGroupInvitation(g *ent.GroupInvitationToken) GroupInvitation {
|
||||
return GroupInvitation{
|
||||
ID: g.ID,
|
||||
ExpiresAt: g.ExpiresAt,
|
||||
Uses: g.Uses,
|
||||
Group: mapToGroup(g.Edges.Group),
|
||||
}
|
||||
func (r *GroupRepository) GetAllGroups(ctx context.Context) ([]Group, error) {
|
||||
return r.groupMapper.MapEachErr(r.db.Group.Query().All(ctx))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) StatsLocationsByPurchasePrice(ctx context.Context, GID uuid.UUID) ([]TotalsByOrganizer, error) {
|
||||
|
@ -249,7 +259,7 @@ func (r *GroupRepository) StatsGroup(ctx context.Context, GID uuid.UUID) (GroupS
|
|||
}
|
||||
|
||||
func (r *GroupRepository) GroupCreate(ctx context.Context, name string) (Group, error) {
|
||||
return mapToGroupErr(r.db.Group.Create().
|
||||
return r.groupMapper.MapErr(r.db.Group.Create().
|
||||
SetName(name).
|
||||
Save(ctx))
|
||||
}
|
||||
|
@ -262,15 +272,15 @@ func (r *GroupRepository) GroupUpdate(ctx context.Context, ID uuid.UUID, data Gr
|
|||
SetCurrency(currency).
|
||||
Save(ctx)
|
||||
|
||||
return mapToGroupErr(entity, err)
|
||||
return r.groupMapper.MapErr(entity, err)
|
||||
}
|
||||
|
||||
func (r *GroupRepository) GroupByID(ctx context.Context, id uuid.UUID) (Group, error) {
|
||||
return mapToGroupErr(r.db.Group.Get(ctx, id))
|
||||
return r.groupMapper.MapErr(r.db.Group.Get(ctx, id))
|
||||
}
|
||||
|
||||
func (r *GroupRepository) InvitationGet(ctx context.Context, token []byte) (GroupInvitation, error) {
|
||||
return mapToGroupInvitationErr(r.db.GroupInvitationToken.Query().
|
||||
return r.invitationMapper.MapErr(r.db.GroupInvitationToken.Query().
|
||||
Where(groupinvitationtoken.Token(token)).
|
||||
WithGroup().
|
||||
Only(ctx))
|
||||
|
|
|
@ -84,6 +84,27 @@ func mapMaintenanceEntry(entry *ent.MaintenanceEntry) MaintenanceEntry {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) GetScheduled(ctx context.Context, GID uuid.UUID, dt types.Date) ([]MaintenanceEntry, error) {
|
||||
entries, err := r.db.MaintenanceEntry.Query().
|
||||
Where(
|
||||
maintenanceentry.HasItemWith(
|
||||
item.HasGroupWith(group.ID(GID)),
|
||||
),
|
||||
maintenanceentry.ScheduledDate(dt.Time()),
|
||||
maintenanceentry.Or(
|
||||
maintenanceentry.DateIsNil(),
|
||||
maintenanceentry.DateEQ(time.Time{}),
|
||||
),
|
||||
).
|
||||
All(ctx)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mapEachMaintenanceEntry(entries), nil
|
||||
}
|
||||
|
||||
func (r *MaintenanceEntryRepository) Create(ctx context.Context, itemID uuid.UUID, input MaintenanceEntryCreate) (MaintenanceEntry, error) {
|
||||
item, err := r.db.MaintenanceEntry.Create().
|
||||
SetItemID(itemID).
|
||||
|
|
|
@ -73,6 +73,16 @@ func (r *NotifierRepository) GetByGroup(ctx context.Context, groupID uuid.UUID)
|
|||
Where(notifier.GroupID(groupID)).
|
||||
Order(ent.Asc(notifier.FieldName)).
|
||||
All(ctx)
|
||||
|
||||
return r.mapper.MapEachErr(notifier, err)
|
||||
}
|
||||
|
||||
func (r *NotifierRepository) GetActiveByGroup(ctx context.Context, groupID uuid.UUID) ([]NotifierOut, error) {
|
||||
notifier, err := r.db.Notifier.Query().
|
||||
Where(notifier.GroupID(groupID), notifier.IsActive(true)).
|
||||
Order(ent.Asc(notifier.FieldName)).
|
||||
All(ctx)
|
||||
|
||||
return r.mapper.MapEachErr(notifier, err)
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ func New(db *ent.Client, root string) *AllRepos {
|
|||
return &AllRepos{
|
||||
Users: &UserRepository{db},
|
||||
AuthTokens: &TokenRepository{db},
|
||||
Groups: &GroupRepository{db},
|
||||
Groups: NewGroupRepository(db),
|
||||
Locations: &LocationRepository{db},
|
||||
Labels: &LabelRepository{db},
|
||||
Items: &ItemsRepository{db},
|
||||
|
|
|
@ -35,7 +35,7 @@ Homebox is currently in early-active development and is currently in **beta** st
|
|||
- Item Identifications (Serial, Model, etc)
|
||||
- Categorized Attachments (Images, Manuals, General)
|
||||
- Arbitrary/Custom Fields
|
||||
- Csv Import for quickly creating and managing items
|
||||
- CSV Import/Export for quickly creating and managing items
|
||||
- Custom Reporting
|
||||
- Bill of Materials Export
|
||||
- QR Code Label Generator
|
||||
|
|
|
@ -5,10 +5,13 @@
|
|||
Great for testing out the application, but not recommended for stable use. Checkout the docker-compose for the recommended deployment.
|
||||
|
||||
```sh
|
||||
docker run --name=homebox \
|
||||
--restart=always \
|
||||
--publish=3100:7745 \
|
||||
ghcr.io/hay-kot/homebox:latest
|
||||
docker run -d \
|
||||
--name homebox \
|
||||
--restart unless-stopped \
|
||||
--publish 3100:7745 \
|
||||
--env TZ=Europe/Bucharest \
|
||||
--volume /path/to/data/folder/:/data \
|
||||
ghcr.io/hay-kot/homebox:latest
|
||||
```
|
||||
|
||||
## Docker-Compose
|
||||
|
|
|
@ -41,8 +41,18 @@ Homebox has a built-in QR code generator that can be used to generate QR codes f
|
|||
|
||||
However, the API endpoint is available for generating QR codes on the fly for any item (or any other data) if you provide a valid API key in the query parameters. An example url would look like `/api/v1/qrcode?data=https://homebox.fly.dev/item/{uuid}&access_token={api_key}`. Currently the easiest way to get an API token is to use one from an existing URL of the QR Code in the API key, but this will be improved in the future.
|
||||
|
||||
:octicons-tag-24: 0.8.0
|
||||
:octicons-tag-24: v0.8.0
|
||||
|
||||
In version 0.8.0 We've added a custom label generation. On the tools page, there is now a link to the label-generator page where you can generate labels based on Asset ID for your inventory. These are still in early development, so please provide feedback. There's also more information on the implementation on the label generator page.
|
||||
|
||||
[Demo](https://homebox.fly.dev/reports/label-generator)
|
||||
|
||||
## Scheduled Maintenance Notifications
|
||||
|
||||
:octicons-tag-24: v0.9.0
|
||||
|
||||
Homebox uses [shoutrrr](https://containrrr.dev/shoutrrr/0.7/) to send notifications. This allows you to send notifications to a variety of services. On your profile page, you can add notification URLs to your profile which will be used to send notifications when a maintenance event is scheduled.
|
||||
|
||||
**Notifications are sent on the day the maintenance is scheduled at or around 8am.**
|
||||
|
||||
As of `v0.9.0` we have limited support for complex scheduling of maintenance events. If you have requests for extended functionality, please open an issue on GitHub or reach out on Discord. We're still gauging the demand for this feature.
|
|
@ -33,8 +33,6 @@ plugins:
|
|||
|
||||
extra_css:
|
||||
- assets/stylesheets/extras.css
|
||||
extra_javascript:
|
||||
- assets/js/redoc.js
|
||||
|
||||
markdown_extensions:
|
||||
- pymdownx.emoji:
|
||||
|
|
Loading…
Reference in a new issue