From 40f4bd069ed2c82215dc0dd6eb24d5f557c8a108 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Tue, 21 Mar 2023 10:52:17 -0800 Subject: [PATCH 1/6] fix potential memory leak with time.After --- backend/app/api/app.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/app/api/app.go b/backend/app/api/app.go index a84b9ae1..0f442974 100644 --- a/backend/app/api/app.go +++ b/backend/app/api/app.go @@ -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 } } From f1089833e0eef59bd5526a9863f534500862d600 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Tue, 21 Mar 2023 10:52:44 -0800 Subject: [PATCH 2/6] add new background service to manage scheduled notifications --- backend/app/api/main.go | 14 ++++ backend/internal/core/services/all.go | 8 +- .../core/services/service_background.go | 81 +++++++++++++++++++ .../data/ent/schema/maintenance_entry.go | 2 + backend/internal/data/repo/repo_group.go | 62 ++++++++------ .../data/repo/repo_maintenance_entry.go | 21 +++++ backend/internal/data/repo/repo_notifier.go | 10 +++ backend/internal/data/repo/repos_all.go | 2 +- 8 files changed, 170 insertions(+), 30 deletions(-) create mode 100644 backend/internal/core/services/service_background.go diff --git a/backend/app/api/main.go b/backend/app/api/main.go index c92572a4..5ea43734 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -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 { diff --git a/backend/internal/core/services/all.go b/backend/internal/core/services/all.go index dab59ef0..8cbe8965 100644 --- a/backend/internal/core/services/all.go +++ b/backend/internal/core/services/all.go @@ -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}, } } diff --git a/backend/internal/core/services/service_background.go b/backend/internal/core/services/service_background.go new file mode 100644 index 00000000..21ae4c36 --- /dev/null +++ b/backend/internal/core/services/service_background.go @@ -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 +} diff --git a/backend/internal/data/ent/schema/maintenance_entry.go b/backend/internal/data/ent/schema/maintenance_entry.go index 1c623cf0..52b905ba 100644 --- a/backend/internal/data/ent/schema/maintenance_entry.go +++ b/backend/internal/data/ent/schema/maintenance_entry.go @@ -33,6 +33,8 @@ func (MaintenanceEntry) Fields() []ent.Field { Optional(), field.Float("cost"). Default(0.0), + field.Bool("reminders_enabled"). + Default(false), } } diff --git a/backend/internal/data/repo/repo_group.go b/backend/internal/data/repo/repo_group.go index 2b740719..fab543c2 100644 --- a/backend/internal/data/repo/repo_group.go +++ b/backend/internal/data/repo/repo_group.go @@ -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)) diff --git a/backend/internal/data/repo/repo_maintenance_entry.go b/backend/internal/data/repo/repo_maintenance_entry.go index daf78876..b699d09a 100644 --- a/backend/internal/data/repo/repo_maintenance_entry.go +++ b/backend/internal/data/repo/repo_maintenance_entry.go @@ -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). diff --git a/backend/internal/data/repo/repo_notifier.go b/backend/internal/data/repo/repo_notifier.go index c99cad2b..2ea27eb6 100644 --- a/backend/internal/data/repo/repo_notifier.go +++ b/backend/internal/data/repo/repo_notifier.go @@ -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) } diff --git a/backend/internal/data/repo/repos_all.go b/backend/internal/data/repo/repos_all.go index 9a6d9c55..2a3cf276 100644 --- a/backend/internal/data/repo/repos_all.go +++ b/backend/internal/data/repo/repos_all.go @@ -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}, From 62cd43e50a674922406297a1710c699081f23d63 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Tue, 21 Mar 2023 11:05:55 -0800 Subject: [PATCH 3/6] update docs --- docs/docs/index.md | 2 +- docs/docs/tips-tricks.md | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/docs/index.md b/docs/docs/index.md index cc15a02e..708e33a0 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -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 diff --git a/docs/docs/tips-tricks.md b/docs/docs/tips-tricks.md index f7f47d9d..d5c1207f 100644 --- a/docs/docs/tips-tricks.md +++ b/docs/docs/tips-tricks.md @@ -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) \ No newline at end of file +[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. \ No newline at end of file From 0eed528c69c9eac76a2d3a674fc24a848279edf4 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Tue, 21 Mar 2023 11:21:08 -0800 Subject: [PATCH 4/6] remove old js reference --- docs/mkdocs.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6ba89150..65bd2e1c 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -33,8 +33,6 @@ plugins: extra_css: - assets/stylesheets/extras.css -extra_javascript: - - assets/js/redoc.js markdown_extensions: - pymdownx.emoji: From 05ab3ad7963324d652484fe1d19a717fca0916c1 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Tue, 21 Mar 2023 11:26:25 -0800 Subject: [PATCH 5/6] closes #278 --- README.md | 11 +++++++---- docs/docs/quick-start.md | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 691504fe..611a5303 100644 --- a/README.md +++ b/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 diff --git a/docs/docs/quick-start.md b/docs/docs/quick-start.md index 3a5fad36..e0ad87b2 100644 --- a/docs/docs/quick-start.md +++ b/docs/docs/quick-start.md @@ -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 From 02c46fc75ea1564abb1e6f2b80dc16c2265da0cc Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Tue, 21 Mar 2023 11:28:32 -0800 Subject: [PATCH 6/6] tidy --- backend/go.mod | 1 - backend/go.sum | 7 ------- 2 files changed, 8 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index 5871a444..44db1e23 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -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 diff --git a/backend/go.sum b/backend/go.sum index be22b376..0bad26e0 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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=