diff --git a/.gitignore b/.gitignore index ee9b3f5..bfe0fbe 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,7 @@ package.json node_modules/ # Database -zdravko.db* -zdravko_kv.db* -temporal.db* +store/ # Keys *.pem diff --git a/database/models/models.go b/database/models/models.go index fbb3dbd..cf4045e 100644 --- a/database/models/models.go +++ b/database/models/models.go @@ -45,16 +45,16 @@ type OAuth2State struct { ExpiresAt *Time `db:"expires_at"` } -type MonitorStatus string +type CheckStatus string const ( - MonitorSuccess MonitorStatus = "SUCCESS" - MonitorFailure MonitorStatus = "FAILURE" - MonitorError MonitorStatus = "ERROR" - MonitorUnknown MonitorStatus = "UNKNOWN" + CheckSuccess CheckStatus = "SUCCESS" + CheckFailure CheckStatus = "FAILURE" + CheckError CheckStatus = "ERROR" + CheckUnknown CheckStatus = "UNKNOWN" ) -type Monitor struct { +type Check struct { CreatedAt *Time `db:"created_at"` UpdatedAt *Time `db:"updated_at"` @@ -66,18 +66,18 @@ type Monitor struct { Script string `db:"script"` } -type MonitorWithWorkerGroups struct { - Monitor +type CheckWithWorkerGroups struct { + Check // List of worker group names WorkerGroups []string } -type MonitorHistory struct { +type CheckHistory struct { CreatedAt *Time `db:"created_at"` - MonitorId string `db:"monitor_id"` - Status MonitorStatus `db:"status"` + CheckId string `db:"check_id"` + Status CheckStatus `db:"status"` Note string `db:"note"` WorkerGroupId string `db:"worker_group_id"` @@ -92,11 +92,11 @@ type WorkerGroup struct { Name string `db:"name"` } -type WorkerGroupWithMonitors struct { +type WorkerGroupWithChecks struct { WorkerGroup // List of worker group names - Monitors []string + Checks []string } type TriggerStatus string diff --git a/database/sqlite/migrations/2024-02-27-initial.sql b/database/sqlite/migrations/2024-02-27-initial.sql index 496af72..6e0c8ac 100644 --- a/database/sqlite/migrations/2024-02-27-initial.sql +++ b/database/sqlite/migrations/2024-02-27-initial.sql @@ -6,7 +6,7 @@ CREATE TABLE oauth2_states ( PRIMARY KEY (state) ) STRICT; -CREATE TABLE monitors ( +CREATE TABLE checks ( id TEXT NOT NULL, name TEXT NOT NULL, "group" TEXT NOT NULL DEFAULT 'default', @@ -17,12 +17,12 @@ CREATE TABLE monitors ( updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), PRIMARY KEY (id), - CONSTRAINT unique_monitors_name UNIQUE (name) + CONSTRAINT unique_checks_name UNIQUE (name) ) STRICT; ---CREATE TRIGGER monitors_updated_timestamp AFTER UPDATE ON monitors BEGIN --- update monitors set updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') where id = new.id; +--CREATE TRIGGER checks_updated_timestamp AFTER UPDATE ON checks BEGIN +-- update checks set updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') where id = new.id; --END; CREATE TABLE worker_groups ( @@ -40,17 +40,17 @@ CREATE TABLE worker_groups ( -- update worker_groups set updated_at = strftime('%Y-%m-%dT%H:%M:%fZ') where id = new.id; --END; -CREATE TABLE monitor_worker_groups ( +CREATE TABLE check_worker_groups ( worker_group_id TEXT NOT NULL, - monitor_id TEXT NOT NULL, + check_id TEXT NOT NULL, - PRIMARY KEY (worker_group_id,monitor_id), - CONSTRAINT fk_monitor_worker_groups_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE, - CONSTRAINT fk_monitor_worker_groups_monitor FOREIGN KEY (monitor_id) REFERENCES monitors(id) ON DELETE CASCADE + PRIMARY KEY (worker_group_id,check_id), + CONSTRAINT fk_check_worker_groups_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE, + CONSTRAINT fk_check_worker_groups_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE ) STRICT; -CREATE TABLE monitor_histories ( - monitor_id TEXT NOT NULL, +CREATE TABLE check_histories ( + check_id TEXT NOT NULL, worker_group_id TEXT NOT NULL, status TEXT NOT NULL, @@ -58,14 +58,41 @@ CREATE TABLE monitor_histories ( created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), - PRIMARY KEY (monitor_id, worker_group_id, created_at), - CONSTRAINT fk_monitor_histories_monitor FOREIGN KEY (monitor_id) REFERENCES monitors(id) ON DELETE CASCADE, - CONSTRAINT fk_monitor_histories_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE + PRIMARY KEY (check_id, worker_group_id, created_at), + CONSTRAINT fk_check_histories_check FOREIGN KEY (check_id) REFERENCES checks(id) ON DELETE CASCADE, + CONSTRAINT fk_check_histories_worker_group FOREIGN KEY (worker_group_id) REFERENCES worker_groups(id) ON DELETE CASCADE +) STRICT; + +CREATE TABLE triggers ( + id TEXT NOT NULL, + name TEXT NOT NULL, + script TEXT NOT NULL, + status TEXT NOT NULL, + + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), + updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), + + PRIMARY KEY (id), + CONSTRAINT unique_triggers_name UNIQUE (name) +) STRICT; + +CREATE TABLE trigger_histories ( + trigger_id TEXT NOT NULL, + + status TEXT NOT NULL, + note TEXT NOT NULL, + + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), + + PRIMARY KEY (trigger_id, created_at), + CONSTRAINT fk_trigger_histories_trigger FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE ) STRICT; -- +migrate Down DROP TABLE oauth2_states; -DROP TABLE monitor_worker_groups; +DROP TABLE check_worker_groups; DROP TABLE worker_groups; -DROP TABLE monitor_histories; -DROP TABLE monitors; +DROP TABLE check_histories; +DROP TABLE checks; +DROP TABLE triggers; +DROP TABLE trigger_histories; diff --git a/database/sqlite/migrations/2024-04-28_triggers.sql b/database/sqlite/migrations/2024-04-28_triggers.sql deleted file mode 100644 index ff3c9de..0000000 --- a/database/sqlite/migrations/2024-04-28_triggers.sql +++ /dev/null @@ -1,29 +0,0 @@ --- +migrate Up -CREATE TABLE triggers ( - id TEXT NOT NULL, - name TEXT NOT NULL, - script TEXT NOT NULL, - status TEXT NOT NULL, - - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), - updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), - - PRIMARY KEY (id), - CONSTRAINT unique_triggers_name UNIQUE (name) -) STRICT; - -CREATE TABLE trigger_histories ( - trigger_id TEXT NOT NULL, - - status TEXT NOT NULL, - note TEXT NOT NULL, - - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ')), - - PRIMARY KEY (trigger_id, created_at), - CONSTRAINT fk_trigger_histories_trigger FOREIGN KEY (trigger_id) REFERENCES triggers(id) ON DELETE CASCADE -) STRICT; - --- +migrate Down -DROP TABLE triggers; -DROP TABLE trigger_histories; diff --git a/deploy/fly.toml b/deploy/fly.toml index c60dbb0..a0968ae 100644 --- a/deploy/fly.toml +++ b/deploy/fly.toml @@ -17,9 +17,9 @@ primary_region = 'waw' ROOT_URL = 'https://zdravko.mnts.dev' TEMPORAL_SERVER_HOST = 'server.process.zdravko.internal:7233' - TEMPORAL_DATABASE_PATH = '/data/temporal-9.db' - SQLITE_DATABASE_PATH = '/data/zdravko-9.db' - KEYVALUE_DATABASE_PATH = '/data/zdravko_kv.db' + TEMPORAL_DATABASE_PATH = '/data/temporal-10.db' + SQLITE_DATABASE_PATH = '/data/zdravko-10.db' + KEYVALUE_DATABASE_PATH = '/data/zdravko_kv-10.db' [processes] server = '--temporal --server' diff --git a/internal/activities/monitor.go b/internal/activities/check.go similarity index 66% rename from internal/activities/monitor.go rename to internal/activities/check.go index 74d38c3..dd579e5 100644 --- a/internal/activities/monitor.go +++ b/internal/activities/check.go @@ -18,12 +18,12 @@ type HealtcheckParam struct { Script string } -type MonitorResult struct { +type CheckResult struct { Success bool Note string } -func (a *Activities) Monitor(ctx context.Context, param HealtcheckParam) (*MonitorResult, error) { +func (a *Activities) Check(ctx context.Context, param HealtcheckParam) (*CheckResult, error) { execution := k6.NewExecution(slog.Default(), script.UnescapeString(param.Script)) result, err := execution.Run(ctx) @@ -31,23 +31,23 @@ func (a *Activities) Monitor(ctx context.Context, param HealtcheckParam) (*Monit return nil, err } - return &MonitorResult{Success: result.Success, Note: result.Note}, nil + return &CheckResult{Success: result.Success, Note: result.Note}, nil } type HealtcheckAddToHistoryParam struct { - MonitorId string - Status models.MonitorStatus + CheckId string + Status models.CheckStatus Note string WorkerGroupId string } -type MonitorAddToHistoryResult struct { +type CheckAddToHistoryResult struct { } -func (a *Activities) MonitorAddToHistory(ctx context.Context, param HealtcheckAddToHistoryParam) (*MonitorAddToHistoryResult, error) { - url := fmt.Sprintf("%s/api/v1/monitors/%s/history", a.config.ApiUrl, param.MonitorId) +func (a *Activities) CheckAddToHistory(ctx context.Context, param HealtcheckAddToHistoryParam) (*CheckAddToHistoryResult, error) { + url := fmt.Sprintf("%s/api/v1/checks/%s/history", a.config.ApiUrl, param.CheckId) - body := api.ApiV1MonitorsHistoryPOSTBody{ + body := api.ApiV1ChecksHistoryPOSTBody{ Status: param.Status, Note: param.Note, WorkerGroupId: param.WorkerGroupId, @@ -73,5 +73,5 @@ func (a *Activities) MonitorAddToHistory(ctx context.Context, param HealtcheckAd return nil, fmt.Errorf("unexpected status code: %d", response.StatusCode) } - return &MonitorAddToHistoryResult{}, nil + return &CheckAddToHistoryResult{}, nil } diff --git a/internal/config/server.go b/internal/config/server.go index 96f0b0e..8d0a006 100644 --- a/internal/config/server.go +++ b/internal/config/server.go @@ -48,8 +48,8 @@ func NewServerConfig() *ServerConfig { // Set defaults v.SetDefault("port", GetEnvOrDefault("PORT", "8000")) v.SetDefault("rooturl", GetEnvOrDefault("ROOT_URL", "http://localhost:8000")) - v.SetDefault("sqlitedatabasepath", GetEnvOrDefault("SQLITE_DATABASE_PATH", "zdravko.db")) - v.SetDefault("keyvaluedatabasepath", GetEnvOrDefault("KEYVALUE_DATABASE_PATH", "zdravko_kv.db")) + v.SetDefault("sqlitedatabasepath", GetEnvOrDefault("SQLITE_DATABASE_PATH", "store/zdravko.db")) + v.SetDefault("keyvaluedatabasepath", GetEnvOrDefault("KEYVALUE_DATABASE_PATH", "store/zdravko_kv.db")) v.SetDefault("sessionsecret", os.Getenv("SESSION_SECRET")) v.SetDefault("temporal.uihost", GetEnvOrDefault("TEMPORAL_UI_HOST", "127.0.0.1:8223")) v.SetDefault("temporal.serverhost", GetEnvOrDefault("TEMPORAL_SERVER_HOST", "127.0.0.1:7233")) diff --git a/internal/config/temporal.go b/internal/config/temporal.go index 0442d5e..ff01fb1 100644 --- a/internal/config/temporal.go +++ b/internal/config/temporal.go @@ -23,7 +23,7 @@ func NewTemporalConfig() *TemporalConfig { v := newViper() // Set defaults - v.SetDefault("databasepath", GetEnvOrDefault("TEMPORAL_DATABASE_PATH", "temporal.db")) + v.SetDefault("databasepath", GetEnvOrDefault("TEMPORAL_DATABASE_PATH", "store/temporal.db")) v.SetDefault("listenaddress", GetEnvOrDefault("TEMPORAL_LISTEN_ADDRESS", "0.0.0.0")) v.SetDefault("jwt.publickey", os.Getenv("JWT_PUBLIC_KEY")) diff --git a/internal/handlers/api.go b/internal/handlers/api.go index bac6673..ce4a573 100644 --- a/internal/handlers/api.go +++ b/internal/handlers/api.go @@ -40,26 +40,26 @@ func (h *BaseHandler) ApiV1WorkersConnectGET(c echo.Context) error { // TODO: Can we instead get this from the Workflow outcome? // // To somehow listen for the outcomes and then store them automatically. -func (h *BaseHandler) ApiV1MonitorsHistoryPOST(c echo.Context) error { +func (h *BaseHandler) ApiV1ChecksHistoryPOST(c echo.Context) error { ctx := context.Background() id := c.Param("id") - var body api.ApiV1MonitorsHistoryPOSTBody + var body api.ApiV1ChecksHistoryPOSTBody err := (&echo.DefaultBinder{}).BindBody(c, &body) if err != nil { return err } - _, err = services.GetMonitor(ctx, h.db, id) + _, err = services.GetCheck(ctx, h.db, id) if err != nil { if err == sql.ErrNoRows { - return echo.NewHTTPError(http.StatusNotFound, "Monitor not found") + return echo.NewHTTPError(http.StatusNotFound, "Check not found") } return err } - err = services.AddHistoryForMonitor(ctx, h.db, &models.MonitorHistory{ - MonitorId: id, + err = services.AddHistoryForCheck(ctx, h.db, &models.CheckHistory{ + CheckId: id, WorkerGroupId: body.WorkerGroupId, Status: body.Status, Note: body.Note, diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index bf2eb98..97dc252 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -18,7 +18,7 @@ import ( var examplesYaml embed.FS type examples struct { - Monitor string `yaml:"monitor"` + Check string `yaml:"check"` Trigger string `yaml:"trigger"` } @@ -63,7 +63,7 @@ func NewBaseHandler(db *sqlx.DB, kvStore kv.KeyValueStore, temporal client.Clien panic(err) } - examples.Monitor = script.EscapeString(examples.Monitor) + examples.Check = script.EscapeString(examples.Check) examples.Trigger = script.EscapeString(examples.Trigger) return &BaseHandler{ diff --git a/internal/handlers/index.go b/internal/handlers/index.go index de1892b..930456d 100644 --- a/internal/handlers/index.go +++ b/internal/handlers/index.go @@ -13,21 +13,21 @@ import ( type IndexData struct { *components.Base - Monitors map[string]MonitorsAndStatus - MonitorsLength int + Checks map[string]ChecksAndStatus + ChecksLength int TimeRange string - Status models.MonitorStatus + Status models.CheckStatus } -type Monitor struct { +type Check struct { Name string Group string - Status models.MonitorStatus + Status models.CheckStatus History *History } type HistoryItem struct { - Status models.MonitorStatus + Status models.CheckStatus Date time.Time } @@ -36,23 +36,23 @@ type History struct { Uptime int } -type MonitorsAndStatus struct { - Status models.MonitorStatus - Monitors []*Monitor +type ChecksAndStatus struct { + Status models.CheckStatus + Checks []*Check } func getDateString(date time.Time) string { return date.UTC().Format("2006-01-02T15:04:05") } -func getHistory(history []*models.MonitorHistory, period time.Duration, buckets int) *History { - historyMap := map[string]models.MonitorStatus{} +func getHistory(history []*models.CheckHistory, period time.Duration, buckets int) *History { + historyMap := map[string]models.CheckStatus{} numOfSuccess := 0 numTotal := 0 for i := 0; i < buckets; i++ { dateString := getDateString(time.Now().Add(period * time.Duration(-i)).Truncate(period)) - historyMap[dateString] = models.MonitorUnknown + historyMap[dateString] = models.CheckUnknown } for _, _history := range history { @@ -64,12 +64,12 @@ func getHistory(history []*models.MonitorHistory, period time.Duration, buckets } numTotal++ - if _history.Status == models.MonitorSuccess { + if _history.Status == models.CheckSuccess { numOfSuccess++ } // skip if it is already set to failure - if historyMap[dateString] == models.MonitorFailure { + if historyMap[dateString] == models.CheckFailure { continue } @@ -101,7 +101,7 @@ func getHistory(history []*models.MonitorHistory, period time.Duration, buckets func (h *BaseHandler) Index(c echo.Context) error { ctx := context.Background() - monitors, err := services.GetMonitors(ctx, h.db) + checks, err := services.GetChecks(ctx, h.db) if err != nil { return err } @@ -111,12 +111,12 @@ func (h *BaseHandler) Index(c echo.Context) error { timeRange = "90days" } - overallStatus := models.MonitorUnknown - statusByGroup := make(map[string]models.MonitorStatus) + overallStatus := models.CheckUnknown + statusByGroup := make(map[string]models.CheckStatus) - monitorsWithHistory := make([]*Monitor, len(monitors)) - for i, monitor := range monitors { - history, err := services.GetMonitorHistoryForMonitor(ctx, h.db, monitor.Id) + checksWithHistory := make([]*Check, len(checks)) + for i, check := range checks { + history, err := services.GetCheckHistoryForCheck(ctx, h.db, check.Id) if err != nil { return err } @@ -131,37 +131,37 @@ func (h *BaseHandler) Index(c echo.Context) error { historyResult = getHistory(history, time.Minute, 90) } - if statusByGroup[monitor.Group] == "" { - statusByGroup[monitor.Group] = models.MonitorUnknown + if statusByGroup[check.Group] == "" { + statusByGroup[check.Group] = models.CheckUnknown } status := historyResult.List[len(historyResult.List)-1] - if status.Status == models.MonitorSuccess { - if overallStatus == models.MonitorUnknown { + if status.Status == models.CheckSuccess { + if overallStatus == models.CheckUnknown { overallStatus = status.Status } - if statusByGroup[monitor.Group] == models.MonitorUnknown { - statusByGroup[monitor.Group] = status.Status + if statusByGroup[check.Group] == models.CheckUnknown { + statusByGroup[check.Group] = status.Status } } - if status.Status != models.MonitorSuccess && status.Status != models.MonitorUnknown { + if status.Status != models.CheckSuccess && status.Status != models.CheckUnknown { overallStatus = status.Status - statusByGroup[monitor.Group] = status.Status + statusByGroup[check.Group] = status.Status } - monitorsWithHistory[i] = &Monitor{ - Name: monitor.Name, - Group: monitor.Group, + checksWithHistory[i] = &Check{ + Name: check.Name, + Group: check.Group, Status: status.Status, History: historyResult, } } - monitorsByGroup := map[string]MonitorsAndStatus{} - for _, monitor := range monitorsWithHistory { - monitorsByGroup[monitor.Group] = MonitorsAndStatus{ - Status: statusByGroup[monitor.Group], - Monitors: append(monitorsByGroup[monitor.Group].Monitors, monitor), + checksByGroup := map[string]ChecksAndStatus{} + for _, check := range checksWithHistory { + checksByGroup[check.Group] = ChecksAndStatus{ + Status: statusByGroup[check.Group], + Checks: append(checksByGroup[check.Group].Checks, check), } } @@ -172,7 +172,7 @@ func (h *BaseHandler) Index(c echo.Context) error { NavbarActive: GetPageByTitle(Pages, "Status"), Navbar: Pages, }, - Monitors: monitorsByGroup, + Checks: checksByGroup, TimeRange: timeRange, Status: overallStatus, }) diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go index eda2d5d..e0ae624 100644 --- a/internal/handlers/settings.go +++ b/internal/handlers/settings.go @@ -36,11 +36,13 @@ func NewSettings(user *AuthenticatedUser, page *components.Page, breadCrumbs []* var SettingsPages = []*components.Page{ {Path: "/settings", Title: "Overview", Breadcrumb: "Overview"}, - {Path: "/settings/targets", Title: "Incidents", Breadcrumb: "Incidents"}, + {Path: "/settings/incidents", Title: "Incidents", Breadcrumb: "Incidents"}, {Path: "/settings/targets", Title: "Targets", Breadcrumb: "Targets"}, {Path: "/settings/targets/create", Title: "Targets Create", Breadcrumb: "Create"}, - {Path: "/settings/monitors", Title: "Checks", Breadcrumb: "Checks"}, - {Path: "/settings/monitors/create", Title: "Checks Create", Breadcrumb: "Create"}, + {Path: "/settings/hooks", Title: "Hooks", Breadcrumb: "Hooks"}, + {Path: "/settings/hooks/create", Title: "Hooks Create", Breadcrumb: "Create"}, + {Path: "/settings/checks", Title: "Checks", Breadcrumb: "Checks"}, + {Path: "/settings/checks/create", Title: "Checks Create", Breadcrumb: "Create"}, {Path: "/settings/worker-groups", Title: "Worker Groups", Breadcrumb: "Worker Groups"}, {Path: "/settings/worker-groups/create", Title: "Worker Groups Create", Breadcrumb: "Create"}, {Path: "/settings/notifications", Title: "Notifications", Breadcrumb: "Notifications"}, @@ -63,10 +65,11 @@ var SettingsSidebar = []SettingsSidebarGroup{ Pages: []*components.Page{ GetPageByTitle(SettingsPages, "Targets"), GetPageByTitle(SettingsPages, "Checks"), + GetPageByTitle(SettingsPages, "Hooks"), }, }, { - Group: "Decide", + Group: "Alert", Pages: []*components.Page{ GetPageByTitle(SettingsPages, "Triggers"), }, @@ -91,9 +94,9 @@ var SettingsSidebar = []SettingsSidebarGroup{ type SettingsOverview struct { *Settings WorkerGroupsCount int - MonitorsCount int + ChecksCount int NotificationsCount int - History []*services.MonitorHistoryWithMonitor + History []*services.CheckHistoryWithCheck } func (h *BaseHandler) SettingsOverviewGET(c echo.Context) error { @@ -105,12 +108,12 @@ func (h *BaseHandler) SettingsOverviewGET(c echo.Context) error { return err } - monitors, err := services.CountMonitors(ctx, h.db) + checks, err := services.CountChecks(ctx, h.db) if err != nil { return err } - history, err := services.GetLastNMonitorHistory(ctx, h.db, 10) + history, err := services.GetLastNCheckHistory(ctx, h.db, 10) if err != nil { return err } @@ -122,7 +125,7 @@ func (h *BaseHandler) SettingsOverviewGET(c echo.Context) error { []*components.Page{GetPageByTitle(SettingsPages, "Overview")}, ), WorkerGroupsCount: workerGroups, - MonitorsCount: monitors, + ChecksCount: checks, NotificationsCount: 42, History: history, }) diff --git a/internal/handlers/settings_incidents.go b/internal/handlers/settings_incidents.go new file mode 100644 index 0000000..856b318 --- /dev/null +++ b/internal/handlers/settings_incidents.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "net/http" + + "code.tjo.space/mentos1386/zdravko/web/templates/components" + "github.com/labstack/echo/v4" +) + +type Incident struct{} + +type SettingsIncidents struct { + *Settings + Incidents []*Incident +} + +func (h *BaseHandler) SettingsIncidentsGET(c echo.Context) error { + cc := c.(AuthenticatedContext) + + incidents := make([]*Incident, 0) + + return c.Render(http.StatusOK, "settings_incidents.tmpl", &SettingsIncidents{ + Settings: NewSettings( + cc.Principal.User, + GetPageByTitle(SettingsPages, "Incidents"), + []*components.Page{GetPageByTitle(SettingsPages, "Incidents")}, + ), + Incidents: incidents, + }) +} diff --git a/internal/handlers/settings_triggers.go b/internal/handlers/settings_triggers.go index 491c268..8f61fd1 100644 --- a/internal/handlers/settings_triggers.go +++ b/internal/handlers/settings_triggers.go @@ -14,13 +14,6 @@ import ( "github.com/labstack/echo/v4" ) -type Incident struct{} - -type SettingsIncidents struct { - *Settings - Incidents []*Incident -} - type CreateTrigger struct { Name string `validate:"required"` Script string `validate:"required"` diff --git a/internal/handlers/settingsmonitors.go b/internal/handlers/settingschecks.go similarity index 52% rename from internal/handlers/settingsmonitors.go rename to internal/handlers/settingschecks.go index 99a426a..5c7a547 100644 --- a/internal/handlers/settingsmonitors.go +++ b/internal/handlers/settingschecks.go @@ -17,7 +17,7 @@ import ( "github.com/labstack/echo/v4" ) -type CreateMonitor struct { +type CreateCheck struct { Name string `validate:"required"` Group string `validate:"required"` WorkerGroups string `validate:"required"` @@ -25,96 +25,96 @@ type CreateMonitor struct { Script string `validate:"required"` } -type UpdateMonitor struct { +type UpdateCheck struct { Group string `validate:"required"` WorkerGroups string `validate:"required"` Schedule string `validate:"required,cron"` Script string `validate:"required"` } -type MonitorWithWorkerGroupsAndStatus struct { - *models.MonitorWithWorkerGroups - Status services.MonitorStatus +type CheckWithWorkerGroupsAndStatus struct { + *models.CheckWithWorkerGroups + Status services.CheckStatus } -type SettingsMonitors struct { +type SettingsChecks struct { *Settings - Monitors map[string][]*MonitorWithWorkerGroupsAndStatus - MonitorGroups []string + Checks map[string][]*CheckWithWorkerGroupsAndStatus + CheckGroups []string } -type SettingsMonitor struct { +type SettingsCheck struct { *Settings - Monitor *MonitorWithWorkerGroupsAndStatus - History []*models.MonitorHistory + Check *CheckWithWorkerGroupsAndStatus + History []*models.CheckHistory } -type SettingsMonitorCreate struct { +type SettingsCheckCreate struct { *Settings Example string } -func (h *BaseHandler) SettingsMonitorsGET(c echo.Context) error { +func (h *BaseHandler) SettingsChecksGET(c echo.Context) error { cc := c.(AuthenticatedContext) - monitors, err := services.GetMonitorsWithWorkerGroups(context.Background(), h.db) + checks, err := services.GetChecksWithWorkerGroups(context.Background(), h.db) if err != nil { return err } - monitorsWithStatus := make([]*MonitorWithWorkerGroupsAndStatus, len(monitors)) - for i, monitor := range monitors { - status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Id) + checksWithStatus := make([]*CheckWithWorkerGroupsAndStatus, len(checks)) + for i, check := range checks { + status, err := services.GetCheckStatus(context.Background(), h.temporal, check.Id) if err != nil { return err } - monitorsWithStatus[i] = &MonitorWithWorkerGroupsAndStatus{ - MonitorWithWorkerGroups: monitor, + checksWithStatus[i] = &CheckWithWorkerGroupsAndStatus{ + CheckWithWorkerGroups: check, Status: status, } } - monitorGroups := []string{} - monitorsByGroup := map[string][]*MonitorWithWorkerGroupsAndStatus{} - for _, monitor := range monitorsWithStatus { - monitorsByGroup[monitor.Group] = append(monitorsByGroup[monitor.Group], monitor) - if slices.Contains(monitorGroups, monitor.Group) == false { - monitorGroups = append(monitorGroups, monitor.Group) + checkGroups := []string{} + checksByGroup := map[string][]*CheckWithWorkerGroupsAndStatus{} + for _, check := range checksWithStatus { + checksByGroup[check.Group] = append(checksByGroup[check.Group], check) + if slices.Contains(checkGroups, check.Group) == false { + checkGroups = append(checkGroups, check.Group) } } - return c.Render(http.StatusOK, "settings_monitors.tmpl", &SettingsMonitors{ + return c.Render(http.StatusOK, "settings_checks.tmpl", &SettingsChecks{ Settings: NewSettings( cc.Principal.User, GetPageByTitle(SettingsPages, "Checks"), []*components.Page{GetPageByTitle(SettingsPages, "Checks")}, ), - Monitors: monitorsByGroup, - MonitorGroups: monitorGroups, + Checks: checksByGroup, + CheckGroups: checkGroups, }) } -func (h *BaseHandler) SettingsMonitorsDescribeGET(c echo.Context) error { +func (h *BaseHandler) SettingsChecksDescribeGET(c echo.Context) error { cc := c.(AuthenticatedContext) slug := c.Param("id") - monitor, err := services.GetMonitorWithWorkerGroups(context.Background(), h.db, slug) + check, err := services.GetCheckWithWorkerGroups(context.Background(), h.db, slug) if err != nil { return err } - status, err := services.GetMonitorStatus(context.Background(), h.temporal, monitor.Id) + status, err := services.GetCheckStatus(context.Background(), h.temporal, check.Id) if err != nil { return err } - monitorWithStatus := &MonitorWithWorkerGroupsAndStatus{ - MonitorWithWorkerGroups: monitor, + checkWithStatus := &CheckWithWorkerGroupsAndStatus{ + CheckWithWorkerGroups: check, Status: status, } - history, err := services.GetMonitorHistoryForMonitor(context.Background(), h.db, slug) + history, err := services.GetCheckHistoryForCheck(context.Background(), h.db, slug) if err != nil { return err } @@ -124,76 +124,76 @@ func (h *BaseHandler) SettingsMonitorsDescribeGET(c echo.Context) error { maxElements = len(history) } - return c.Render(http.StatusOK, "settings_monitors_describe.tmpl", &SettingsMonitor{ + return c.Render(http.StatusOK, "settings_checks_describe.tmpl", &SettingsCheck{ Settings: NewSettings( cc.Principal.User, GetPageByTitle(SettingsPages, "Checks"), []*components.Page{ GetPageByTitle(SettingsPages, "Checks"), { - Path: fmt.Sprintf("/settings/monitors/%s", slug), + Path: fmt.Sprintf("/settings/checks/%s", slug), Title: "Describe", - Breadcrumb: monitor.Name, + Breadcrumb: check.Name, }, }), - Monitor: monitorWithStatus, + Check: checkWithStatus, History: history[:maxElements], }) } -func (h *BaseHandler) SettingsMonitorsDescribeDELETE(c echo.Context) error { +func (h *BaseHandler) SettingsChecksDescribeDELETE(c echo.Context) error { slug := c.Param("id") - err := services.DeleteMonitor(context.Background(), h.db, slug) + err := services.DeleteCheck(context.Background(), h.db, slug) if err != nil { return err } - err = services.DeleteMonitorSchedule(context.Background(), h.temporal, slug) + err = services.DeleteCheckSchedule(context.Background(), h.temporal, slug) if err != nil { return err } - return c.Redirect(http.StatusSeeOther, "/settings/monitors") + return c.Redirect(http.StatusSeeOther, "/settings/checks") } -func (h *BaseHandler) SettingsMonitorsDisableGET(c echo.Context) error { +func (h *BaseHandler) SettingsChecksDisableGET(c echo.Context) error { slug := c.Param("id") - monitor, err := services.GetMonitor(context.Background(), h.db, slug) + check, err := services.GetCheck(context.Background(), h.db, slug) if err != nil { return err } - err = services.SetMonitorStatus(context.Background(), h.temporal, monitor.Id, services.MonitorStatusPaused) + err = services.SetCheckStatus(context.Background(), h.temporal, check.Id, services.CheckStatusPaused) if err != nil { return err } - return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/monitors/%s", slug)) + return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/checks/%s", slug)) } -func (h *BaseHandler) SettingsMonitorsEnableGET(c echo.Context) error { +func (h *BaseHandler) SettingsChecksEnableGET(c echo.Context) error { slug := c.Param("id") - monitor, err := services.GetMonitor(context.Background(), h.db, slug) + check, err := services.GetCheck(context.Background(), h.db, slug) if err != nil { return err } - err = services.SetMonitorStatus(context.Background(), h.temporal, monitor.Id, services.MonitorStatusActive) + err = services.SetCheckStatus(context.Background(), h.temporal, check.Id, services.CheckStatusActive) if err != nil { return err } - return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/monitors/%s", slug)) + return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/checks/%s", slug)) } -func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error { +func (h *BaseHandler) SettingsChecksDescribePOST(c echo.Context) error { ctx := context.Background() - monitorId := c.Param("id") + checkId := c.Param("id") - update := UpdateMonitor{ + update := UpdateCheck{ Group: strings.ToLower(c.FormValue("group")), WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), Schedule: c.FormValue("schedule"), @@ -204,18 +204,18 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error { return err } - monitor, err := services.GetMonitor(ctx, h.db, monitorId) + check, err := services.GetCheck(ctx, h.db, checkId) if err != nil { return err } - monitor.Group = update.Group - monitor.Schedule = update.Schedule - monitor.Script = update.Script + check.Group = update.Group + check.Schedule = update.Schedule + check.Script = update.Script - err = services.UpdateMonitor( + err = services.UpdateCheck( ctx, h.db, - monitor, + check, ) if err != nil { return err @@ -241,23 +241,23 @@ func (h *BaseHandler) SettingsMonitorsDescribePOST(c echo.Context) error { workerGroups = append(workerGroups, workerGroup) } - err = services.UpdateMonitorWorkerGroups(ctx, h.db, monitor, workerGroups) + err = services.UpdateCheckWorkerGroups(ctx, h.db, check, workerGroups) if err != nil { return err } - err = services.CreateOrUpdateMonitorSchedule(ctx, h.temporal, monitor, workerGroups) + err = services.CreateOrUpdateCheckSchedule(ctx, h.temporal, check, workerGroups) if err != nil { return err } - return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/monitors/%s", monitorId)) + return c.Redirect(http.StatusSeeOther, fmt.Sprintf("/settings/checks/%s", checkId)) } -func (h *BaseHandler) SettingsMonitorsCreateGET(c echo.Context) error { +func (h *BaseHandler) SettingsChecksCreateGET(c echo.Context) error { cc := c.(AuthenticatedContext) - return c.Render(http.StatusOK, "settings_monitors_create.tmpl", &SettingsMonitorCreate{ + return c.Render(http.StatusOK, "settings_checks_create.tmpl", &SettingsCheckCreate{ Settings: NewSettings( cc.Principal.User, GetPageByTitle(SettingsPages, "Checks"), @@ -266,15 +266,15 @@ func (h *BaseHandler) SettingsMonitorsCreateGET(c echo.Context) error { GetPageByTitle(SettingsPages, "Checks Create"), }, ), - Example: h.examples.Monitor, + Example: h.examples.Check, }) } -func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error { +func (h *BaseHandler) SettingsChecksCreatePOST(c echo.Context) error { ctx := context.Background() - monitorId := slug.Make(c.FormValue("name")) + checkId := slug.Make(c.FormValue("name")) - create := CreateMonitor{ + create := CreateCheck{ Name: c.FormValue("name"), Group: strings.ToLower(c.FormValue("group")), WorkerGroups: strings.ToLower(strings.TrimSpace(c.FormValue("workergroups"))), @@ -306,32 +306,32 @@ func (h *BaseHandler) SettingsMonitorsCreatePOST(c echo.Context) error { workerGroups = append(workerGroups, workerGroup) } - monitor := &models.Monitor{ + check := &models.Check{ Name: create.Name, Group: create.Group, - Id: monitorId, + Id: checkId, Schedule: create.Schedule, Script: create.Script, } - err = services.CreateMonitor( + err = services.CreateCheck( ctx, h.db, - monitor, + check, ) if err != nil { return err } - err = services.UpdateMonitorWorkerGroups(ctx, h.db, monitor, workerGroups) + err = services.UpdateCheckWorkerGroups(ctx, h.db, check, workerGroups) if err != nil { return err } - err = services.CreateOrUpdateMonitorSchedule(ctx, h.temporal, monitor, workerGroups) + err = services.CreateOrUpdateCheckSchedule(ctx, h.temporal, check, workerGroups) if err != nil { return err } - return c.Redirect(http.StatusSeeOther, "/settings/monitors") + return c.Redirect(http.StatusSeeOther, "/settings/checks") } diff --git a/internal/handlers/settingsworkergroups.go b/internal/handlers/settingsworkergroups.go index 7d16984..5028c2c 100644 --- a/internal/handlers/settingsworkergroups.go +++ b/internal/handlers/settingsworkergroups.go @@ -22,7 +22,7 @@ type WorkerWithTokenAndActiveWorkers struct { } type WorkerGroupWithActiveWorkers struct { - *models.WorkerGroupWithMonitors + *models.WorkerGroupWithChecks ActiveWorkers []string } @@ -39,7 +39,7 @@ type SettingsWorker struct { func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error { cc := c.(AuthenticatedContext) - workerGroups, err := services.GetWorkerGroupsWithMonitors(context.Background(), h.db) + workerGroups, err := services.GetWorkerGroupsWithChecks(context.Background(), h.db) if err != nil { return err } @@ -51,7 +51,7 @@ func (h *BaseHandler) SettingsWorkerGroupsGET(c echo.Context) error { return err } workerGroupsWithActiveWorkers[i] = &WorkerGroupWithActiveWorkers{ - WorkerGroupWithMonitors: workerGroup, + WorkerGroupWithChecks: workerGroup, ActiveWorkers: activeWorkers, } } diff --git a/internal/services/check.go b/internal/services/check.go new file mode 100644 index 0000000..a8559ba --- /dev/null +++ b/internal/services/check.go @@ -0,0 +1,316 @@ +package services + +import ( + "context" + "log" + "sort" + "time" + + "code.tjo.space/mentos1386/zdravko/database/models" + "code.tjo.space/mentos1386/zdravko/internal/workflows" + "github.com/jmoiron/sqlx" + "go.temporal.io/sdk/client" + "go.temporal.io/sdk/temporal" + "golang.org/x/exp/maps" +) + +type CheckStatus string + +const ( + CheckStatusUnknown CheckStatus = "UNKNOWN" + CheckStatusPaused CheckStatus = "PAUSED" + CheckStatusActive CheckStatus = "ACTIVE" +) + +func getScheduleId(id string) string { + return "check-" + id +} + +func CountChecks(ctx context.Context, db *sqlx.DB) (int, error) { + var count int + err := db.GetContext(ctx, &count, "SELECT COUNT(*) FROM checks") + return count, err +} + +func GetCheckStatus(ctx context.Context, temporal client.Client, id string) (CheckStatus, error) { + schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id)) + + description, err := schedule.Describe(ctx) + if err != nil { + return CheckStatusUnknown, err + } + + if description.Schedule.State.Paused { + return CheckStatusPaused, nil + } + + return CheckStatusActive, nil +} + +func SetCheckStatus(ctx context.Context, temporal client.Client, id string, status CheckStatus) error { + schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id)) + + if status == CheckStatusActive { + return schedule.Unpause(ctx, client.ScheduleUnpauseOptions{Note: "Unpaused by user"}) + } + + if status == CheckStatusPaused { + return schedule.Pause(ctx, client.SchedulePauseOptions{Note: "Paused by user"}) + } + + return nil +} + +func CreateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error { + _, err := db.NamedExecContext(ctx, + `INSERT INTO checks (id, name, "group", script, schedule) VALUES (:id, :name, :group, :script, :schedule)`, + check, + ) + return err +} + +func UpdateCheck(ctx context.Context, db *sqlx.DB, check *models.Check) error { + _, err := db.NamedExecContext(ctx, + `UPDATE checks SET "group"=:group, script=:script, schedule=:schedule WHERE id=:id`, + check, + ) + return err +} + +func DeleteCheck(ctx context.Context, db *sqlx.DB, id string) error { + _, err := db.ExecContext(ctx, + "DELETE FROM checks WHERE id=$1", + id, + ) + return err +} + +func UpdateCheckWorkerGroups(ctx context.Context, db *sqlx.DB, check *models.Check, workerGroups []*models.WorkerGroup) error { + tx, err := db.BeginTxx(ctx, nil) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, + "DELETE FROM check_worker_groups WHERE check_id=$1", + check.Id, + ) + if err != nil { + tx.Rollback() + return err + } + for _, group := range workerGroups { + _, err = tx.ExecContext(ctx, + "INSERT INTO check_worker_groups (check_id, worker_group_id) VALUES ($1, $2)", + check.Id, + group.Id, + ) + if err != nil { + tx.Rollback() + return err + } + } + return tx.Commit() +} + +func GetCheck(ctx context.Context, db *sqlx.DB, id string) (*models.Check, error) { + check := &models.Check{} + err := db.GetContext(ctx, check, + "SELECT * FROM checks WHERE id=$1", + id, + ) + return check, err +} + +func GetCheckWithWorkerGroups(ctx context.Context, db *sqlx.DB, id string) (*models.CheckWithWorkerGroups, error) { + rows, err := db.QueryContext(ctx, + ` +SELECT + checks.id, + checks.name, + checks."group", + checks.script, + checks.schedule, + checks.created_at, + checks.updated_at, + worker_groups.name as worker_group_name +FROM checks +LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id +LEFT OUTER JOIN worker_groups ON check_worker_groups.worker_group_id = worker_groups.id +WHERE checks.id=$1 +ORDER BY checks.name +`, + id, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + check := &models.CheckWithWorkerGroups{} + + for rows.Next() { + var workerGroupName *string + err = rows.Scan( + &check.Id, + &check.Name, + &check.Group, + &check.Script, + &check.Schedule, + &check.CreatedAt, + &check.UpdatedAt, + &workerGroupName, + ) + if err != nil { + return nil, err + } + if workerGroupName != nil { + check.WorkerGroups = append(check.WorkerGroups, *workerGroupName) + } + } + + return check, err +} + +func GetChecks(ctx context.Context, db *sqlx.DB) ([]*models.Check, error) { + checks := []*models.Check{} + err := db.SelectContext(ctx, &checks, + "SELECT * FROM checks ORDER BY name", + ) + return checks, err +} + +func GetChecksWithWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.CheckWithWorkerGroups, error) { + rows, err := db.QueryContext(ctx, + ` +SELECT + checks.id, + checks.name, + checks."group", + checks.script, + checks.schedule, + checks.created_at, + checks.updated_at, + worker_groups.name as worker_group_name +FROM checks +LEFT OUTER JOIN check_worker_groups ON checks.id = check_worker_groups.check_id +LEFT OUTER JOIN worker_groups ON check_worker_groups.worker_group_id = worker_groups.id +ORDER BY checks.name +`) + if err != nil { + return nil, err + } + defer rows.Close() + + checks := map[string]*models.CheckWithWorkerGroups{} + + for rows.Next() { + check := &models.CheckWithWorkerGroups{} + + var workerGroupName *string + err = rows.Scan( + &check.Id, + &check.Name, + &check.Group, + &check.Script, + &check.Schedule, + &check.CreatedAt, + &check.UpdatedAt, + &workerGroupName, + ) + if err != nil { + return nil, err + } + if workerGroupName != nil { + workerGroups := []string{} + if checks[check.Id] != nil { + workerGroups = checks[check.Id].WorkerGroups + } + check.WorkerGroups = append(workerGroups, *workerGroupName) + } + checks[check.Id] = check + } + + checksWithWorkerGroups := maps.Values(checks) + sort.SliceStable(checksWithWorkerGroups, func(i, j int) bool { + return checksWithWorkerGroups[i].Name < checksWithWorkerGroups[j].Name + }) + + return checksWithWorkerGroups, err +} + +func DeleteCheckSchedule(ctx context.Context, t client.Client, id string) error { + schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(id)) + return schedule.Delete(ctx) +} + +func CreateOrUpdateCheckSchedule( + ctx context.Context, + t client.Client, + check *models.Check, + workerGroups []*models.WorkerGroup, +) error { + log.Println("Creating or Updating Check Schedule") + + workerGroupStrings := make([]string, len(workerGroups)) + for i, group := range workerGroups { + workerGroupStrings[i] = group.Id + } + + args := make([]interface{}, 1) + args[0] = workflows.CheckWorkflowParam{ + Script: check.Script, + CheckId: check.Id, + WorkerGroupIds: workerGroupStrings, + } + + options := client.ScheduleOptions{ + ID: getScheduleId(check.Id), + Spec: client.ScheduleSpec{ + CronExpressions: []string{check.Schedule}, + Jitter: time.Second * 10, + }, + Action: &client.ScheduleWorkflowAction{ + ID: getScheduleId(check.Id), + Workflow: workflows.NewWorkflows(nil).CheckWorkflowDefinition, + Args: args, + TaskQueue: "default", + RetryPolicy: &temporal.RetryPolicy{ + MaximumAttempts: 3, + }, + }, + } + + schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(check.Id)) + + // If exists, we update + _, err := schedule.Describe(ctx) + if err == nil { + err = schedule.Update(ctx, client.ScheduleUpdateOptions{ + DoUpdate: func(input client.ScheduleUpdateInput) (*client.ScheduleUpdate, error) { + return &client.ScheduleUpdate{ + Schedule: &client.Schedule{ + Spec: &options.Spec, + Action: options.Action, + Policy: input.Description.Schedule.Policy, + State: input.Description.Schedule.State, + }, + }, nil + }, + }) + if err != nil { + return err + } + } else { + schedule, err = t.ScheduleClient().Create(ctx, options) + if err != nil { + return err + } + } + + err = schedule.Trigger(ctx, client.ScheduleTriggerOptions{}) + if err != nil { + return err + } + + return nil +} diff --git a/internal/services/check_history.go b/internal/services/check_history.go new file mode 100644 index 0000000..bfb02f2 --- /dev/null +++ b/internal/services/check_history.go @@ -0,0 +1,67 @@ +package services + +import ( + "context" + + "code.tjo.space/mentos1386/zdravko/database/models" + "github.com/jmoiron/sqlx" +) + +type CheckHistoryWithCheck struct { + *models.CheckHistory + CheckName string `db:"check_name"` + CheckId string `db:"check_id"` +} + +func GetLastNCheckHistory(ctx context.Context, db *sqlx.DB, n int) ([]*CheckHistoryWithCheck, error) { + var checkHistory []*CheckHistoryWithCheck + err := db.SelectContext(ctx, &checkHistory, ` + SELECT + mh.*, + wg.name AS worker_group_name, + m.name AS check_name, + m.id AS check_id + FROM check_histories mh + LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id + LEFT JOIN check_worker_groups mwg ON mh.check_id = mwg.check_id + LEFT JOIN checks m ON mwg.check_id = m.id + ORDER BY mh.created_at DESC + LIMIT $1 + `, n) + return checkHistory, err +} + +func GetCheckHistoryForCheck(ctx context.Context, db *sqlx.DB, checkId string) ([]*models.CheckHistory, error) { + var checkHistory []*models.CheckHistory + err := db.SelectContext(ctx, &checkHistory, ` + SELECT + mh.*, + wg.name AS worker_group_name, + wg.id AS worker_group_id + FROM check_histories as mh + LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id + LEFT JOIN check_worker_groups mwg ON mh.check_id = mwg.check_id + WHERE mh.check_id = $1 + ORDER BY mh.created_at DESC + `, checkId) + return checkHistory, err +} + +func AddHistoryForCheck(ctx context.Context, db *sqlx.DB, history *models.CheckHistory) error { + _, err := db.NamedExecContext(ctx, + ` +INSERT INTO check_histories ( + check_id, + worker_group_id, + status, + note +) VALUES ( + :check_id, + :worker_group_id, + :status, + :note +)`, + history, + ) + return err +} diff --git a/internal/services/monitor.go b/internal/services/monitor.go deleted file mode 100644 index cbc8970..0000000 --- a/internal/services/monitor.go +++ /dev/null @@ -1,316 +0,0 @@ -package services - -import ( - "context" - "log" - "sort" - "time" - - "code.tjo.space/mentos1386/zdravko/database/models" - "code.tjo.space/mentos1386/zdravko/internal/workflows" - "github.com/jmoiron/sqlx" - "go.temporal.io/sdk/client" - "go.temporal.io/sdk/temporal" - "golang.org/x/exp/maps" -) - -type MonitorStatus string - -const ( - MonitorStatusUnknown MonitorStatus = "UNKNOWN" - MonitorStatusPaused MonitorStatus = "PAUSED" - MonitorStatusActive MonitorStatus = "ACTIVE" -) - -func getScheduleId(id string) string { - return "monitor-" + id -} - -func CountMonitors(ctx context.Context, db *sqlx.DB) (int, error) { - var count int - err := db.GetContext(ctx, &count, "SELECT COUNT(*) FROM monitors") - return count, err -} - -func GetMonitorStatus(ctx context.Context, temporal client.Client, id string) (MonitorStatus, error) { - schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id)) - - description, err := schedule.Describe(ctx) - if err != nil { - return MonitorStatusUnknown, err - } - - if description.Schedule.State.Paused { - return MonitorStatusPaused, nil - } - - return MonitorStatusActive, nil -} - -func SetMonitorStatus(ctx context.Context, temporal client.Client, id string, status MonitorStatus) error { - schedule := temporal.ScheduleClient().GetHandle(ctx, getScheduleId(id)) - - if status == MonitorStatusActive { - return schedule.Unpause(ctx, client.ScheduleUnpauseOptions{Note: "Unpaused by user"}) - } - - if status == MonitorStatusPaused { - return schedule.Pause(ctx, client.SchedulePauseOptions{Note: "Paused by user"}) - } - - return nil -} - -func CreateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error { - _, err := db.NamedExecContext(ctx, - `INSERT INTO monitors (id, name, "group", script, schedule) VALUES (:id, :name, :group, :script, :schedule)`, - monitor, - ) - return err -} - -func UpdateMonitor(ctx context.Context, db *sqlx.DB, monitor *models.Monitor) error { - _, err := db.NamedExecContext(ctx, - `UPDATE monitors SET "group"=:group, script=:script, schedule=:schedule WHERE id=:id`, - monitor, - ) - return err -} - -func DeleteMonitor(ctx context.Context, db *sqlx.DB, id string) error { - _, err := db.ExecContext(ctx, - "DELETE FROM monitors WHERE id=$1", - id, - ) - return err -} - -func UpdateMonitorWorkerGroups(ctx context.Context, db *sqlx.DB, monitor *models.Monitor, workerGroups []*models.WorkerGroup) error { - tx, err := db.BeginTxx(ctx, nil) - if err != nil { - return err - } - _, err = tx.ExecContext(ctx, - "DELETE FROM monitor_worker_groups WHERE monitor_id=$1", - monitor.Id, - ) - if err != nil { - tx.Rollback() - return err - } - for _, group := range workerGroups { - _, err = tx.ExecContext(ctx, - "INSERT INTO monitor_worker_groups (monitor_id, worker_group_id) VALUES ($1, $2)", - monitor.Id, - group.Id, - ) - if err != nil { - tx.Rollback() - return err - } - } - return tx.Commit() -} - -func GetMonitor(ctx context.Context, db *sqlx.DB, id string) (*models.Monitor, error) { - monitor := &models.Monitor{} - err := db.GetContext(ctx, monitor, - "SELECT * FROM monitors WHERE id=$1", - id, - ) - return monitor, err -} - -func GetMonitorWithWorkerGroups(ctx context.Context, db *sqlx.DB, id string) (*models.MonitorWithWorkerGroups, error) { - rows, err := db.QueryContext(ctx, - ` -SELECT - monitors.id, - monitors.name, - monitors."group", - monitors.script, - monitors.schedule, - monitors.created_at, - monitors.updated_at, - worker_groups.name as worker_group_name -FROM monitors -LEFT OUTER JOIN monitor_worker_groups ON monitors.id = monitor_worker_groups.monitor_id -LEFT OUTER JOIN worker_groups ON monitor_worker_groups.worker_group_id = worker_groups.id -WHERE monitors.id=$1 -ORDER BY monitors.name -`, - id, - ) - if err != nil { - return nil, err - } - defer rows.Close() - - monitor := &models.MonitorWithWorkerGroups{} - - for rows.Next() { - var workerGroupName *string - err = rows.Scan( - &monitor.Id, - &monitor.Name, - &monitor.Group, - &monitor.Script, - &monitor.Schedule, - &monitor.CreatedAt, - &monitor.UpdatedAt, - &workerGroupName, - ) - if err != nil { - return nil, err - } - if workerGroupName != nil { - monitor.WorkerGroups = append(monitor.WorkerGroups, *workerGroupName) - } - } - - return monitor, err -} - -func GetMonitors(ctx context.Context, db *sqlx.DB) ([]*models.Monitor, error) { - monitors := []*models.Monitor{} - err := db.SelectContext(ctx, &monitors, - "SELECT * FROM monitors ORDER BY name", - ) - return monitors, err -} - -func GetMonitorsWithWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.MonitorWithWorkerGroups, error) { - rows, err := db.QueryContext(ctx, - ` -SELECT - monitors.id, - monitors.name, - monitors."group", - monitors.script, - monitors.schedule, - monitors.created_at, - monitors.updated_at, - worker_groups.name as worker_group_name -FROM monitors -LEFT OUTER JOIN monitor_worker_groups ON monitors.id = monitor_worker_groups.monitor_id -LEFT OUTER JOIN worker_groups ON monitor_worker_groups.worker_group_id = worker_groups.id -ORDER BY monitors.name -`) - if err != nil { - return nil, err - } - defer rows.Close() - - monitors := map[string]*models.MonitorWithWorkerGroups{} - - for rows.Next() { - monitor := &models.MonitorWithWorkerGroups{} - - var workerGroupName *string - err = rows.Scan( - &monitor.Id, - &monitor.Name, - &monitor.Group, - &monitor.Script, - &monitor.Schedule, - &monitor.CreatedAt, - &monitor.UpdatedAt, - &workerGroupName, - ) - if err != nil { - return nil, err - } - if workerGroupName != nil { - workerGroups := []string{} - if monitors[monitor.Id] != nil { - workerGroups = monitors[monitor.Id].WorkerGroups - } - monitor.WorkerGroups = append(workerGroups, *workerGroupName) - } - monitors[monitor.Id] = monitor - } - - monitorsWithWorkerGroups := maps.Values(monitors) - sort.SliceStable(monitorsWithWorkerGroups, func(i, j int) bool { - return monitorsWithWorkerGroups[i].Name < monitorsWithWorkerGroups[j].Name - }) - - return monitorsWithWorkerGroups, err -} - -func DeleteMonitorSchedule(ctx context.Context, t client.Client, id string) error { - schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(id)) - return schedule.Delete(ctx) -} - -func CreateOrUpdateMonitorSchedule( - ctx context.Context, - t client.Client, - monitor *models.Monitor, - workerGroups []*models.WorkerGroup, -) error { - log.Println("Creating or Updating Monitor Schedule") - - workerGroupStrings := make([]string, len(workerGroups)) - for i, group := range workerGroups { - workerGroupStrings[i] = group.Id - } - - args := make([]interface{}, 1) - args[0] = workflows.MonitorWorkflowParam{ - Script: monitor.Script, - MonitorId: monitor.Id, - WorkerGroupIds: workerGroupStrings, - } - - options := client.ScheduleOptions{ - ID: getScheduleId(monitor.Id), - Spec: client.ScheduleSpec{ - CronExpressions: []string{monitor.Schedule}, - Jitter: time.Second * 10, - }, - Action: &client.ScheduleWorkflowAction{ - ID: getScheduleId(monitor.Id), - Workflow: workflows.NewWorkflows(nil).MonitorWorkflowDefinition, - Args: args, - TaskQueue: "default", - RetryPolicy: &temporal.RetryPolicy{ - MaximumAttempts: 3, - }, - }, - } - - schedule := t.ScheduleClient().GetHandle(ctx, getScheduleId(monitor.Id)) - - // If exists, we update - _, err := schedule.Describe(ctx) - if err == nil { - err = schedule.Update(ctx, client.ScheduleUpdateOptions{ - DoUpdate: func(input client.ScheduleUpdateInput) (*client.ScheduleUpdate, error) { - return &client.ScheduleUpdate{ - Schedule: &client.Schedule{ - Spec: &options.Spec, - Action: options.Action, - Policy: input.Description.Schedule.Policy, - State: input.Description.Schedule.State, - }, - }, nil - }, - }) - if err != nil { - return err - } - } else { - schedule, err = t.ScheduleClient().Create(ctx, options) - if err != nil { - return err - } - } - - err = schedule.Trigger(ctx, client.ScheduleTriggerOptions{}) - if err != nil { - return err - } - - return nil -} diff --git a/internal/services/monitor_history.go b/internal/services/monitor_history.go deleted file mode 100644 index 29f7940..0000000 --- a/internal/services/monitor_history.go +++ /dev/null @@ -1,67 +0,0 @@ -package services - -import ( - "context" - - "code.tjo.space/mentos1386/zdravko/database/models" - "github.com/jmoiron/sqlx" -) - -type MonitorHistoryWithMonitor struct { - *models.MonitorHistory - MonitorName string `db:"monitor_name"` - MonitorId string `db:"monitor_id"` -} - -func GetLastNMonitorHistory(ctx context.Context, db *sqlx.DB, n int) ([]*MonitorHistoryWithMonitor, error) { - var monitorHistory []*MonitorHistoryWithMonitor - err := db.SelectContext(ctx, &monitorHistory, ` - SELECT - mh.*, - wg.name AS worker_group_name, - m.name AS monitor_name, - m.id AS monitor_id - FROM monitor_histories mh - LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id - LEFT JOIN monitor_worker_groups mwg ON mh.monitor_id = mwg.monitor_id - LEFT JOIN monitors m ON mwg.monitor_id = m.id - ORDER BY mh.created_at DESC - LIMIT $1 - `, n) - return monitorHistory, err -} - -func GetMonitorHistoryForMonitor(ctx context.Context, db *sqlx.DB, monitorId string) ([]*models.MonitorHistory, error) { - var monitorHistory []*models.MonitorHistory - err := db.SelectContext(ctx, &monitorHistory, ` - SELECT - mh.*, - wg.name AS worker_group_name, - wg.id AS worker_group_id - FROM monitor_histories as mh - LEFT JOIN worker_groups wg ON mh.worker_group_id = wg.id - LEFT JOIN monitor_worker_groups mwg ON mh.monitor_id = mwg.monitor_id - WHERE mh.monitor_id = $1 - ORDER BY mh.created_at DESC - `, monitorId) - return monitorHistory, err -} - -func AddHistoryForMonitor(ctx context.Context, db *sqlx.DB, history *models.MonitorHistory) error { - _, err := db.NamedExecContext(ctx, - ` -INSERT INTO monitor_histories ( - monitor_id, - worker_group_id, - status, - note -) VALUES ( - :monitor_id, - :worker_group_id, - :status, - :note -)`, - history, - ) - return err -} diff --git a/internal/services/worker_group.go b/internal/services/worker_group.go index 7ebaa73..089a7a1 100644 --- a/internal/services/worker_group.go +++ b/internal/services/worker_group.go @@ -54,7 +54,7 @@ func GetWorkerGroups(ctx context.Context, db *sqlx.DB) ([]*models.WorkerGroup, e return workerGroups, err } -func GetWorkerGroupsWithMonitors(ctx context.Context, db *sqlx.DB) ([]*models.WorkerGroupWithMonitors, error) { +func GetWorkerGroupsWithChecks(ctx context.Context, db *sqlx.DB) ([]*models.WorkerGroupWithChecks, error) { rows, err := db.QueryContext(ctx, ` SELECT @@ -62,10 +62,10 @@ SELECT worker_groups.name, worker_groups.created_at, worker_groups.updated_at, - monitors.name as monitor_name + checks.name as check_name FROM worker_groups -LEFT OUTER JOIN monitor_worker_groups ON worker_groups.id = monitor_worker_groups.worker_group_id -LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_id = monitors.id +LEFT OUTER JOIN check_worker_groups ON worker_groups.id = check_worker_groups.worker_group_id +LEFT OUTER JOIN checks ON check_worker_groups.check_id = checks.id ORDER BY worker_groups.name `) if err != nil { @@ -73,29 +73,29 @@ ORDER BY worker_groups.name } defer rows.Close() - workerGroups := map[string]*models.WorkerGroupWithMonitors{} + workerGroups := map[string]*models.WorkerGroupWithChecks{} for rows.Next() { - workerGroup := &models.WorkerGroupWithMonitors{} + workerGroup := &models.WorkerGroupWithChecks{} - var monitorName *string + var checkName *string err = rows.Scan( &workerGroup.Id, &workerGroup.Name, &workerGroup.CreatedAt, &workerGroup.UpdatedAt, - &monitorName, + &checkName, ) if err != nil { return nil, err } - if monitorName != nil { - monitors := []string{} + if checkName != nil { + checks := []string{} if workerGroups[workerGroup.Id] != nil { - monitors = workerGroups[workerGroup.Id].Monitors + checks = workerGroups[workerGroup.Id].Checks } - workerGroup.Monitors = append(monitors, *monitorName) + workerGroup.Checks = append(checks, *checkName) } workerGroups[workerGroup.Id] = workerGroup @@ -122,7 +122,7 @@ func GetWorkerGroup(ctx context.Context, db *sqlx.DB, id string) (*models.Worker return &workerGroup, err } -func GetWorkerGroupWithMonitors(ctx context.Context, db *sqlx.DB, id string) (*models.WorkerGroupWithMonitors, error) { +func GetWorkerGroupWithChecks(ctx context.Context, db *sqlx.DB, id string) (*models.WorkerGroupWithChecks, error) { rows, err := db.QueryContext(ctx, ` SELECT @@ -130,10 +130,10 @@ SELECT worker_groups.name, worker_groups.created_at, worker_groups.updated_at, - monitors.name as monitor_name + checks.name as check_name FROM worker_groups -LEFT OUTER JOIN monitor_worker_groups ON worker_groups.id = monitor_worker_groups.worker_group_id -LEFT OUTER JOIN monitors ON monitor_worker_groups.monitor_id = monitors.id +LEFT OUTER JOIN check_worker_groups ON worker_groups.id = check_worker_groups.worker_group_id +LEFT OUTER JOIN checks ON check_worker_groups.check_id = checks.id WHERE worker_groups.id=$1 `, id, @@ -143,22 +143,22 @@ WHERE worker_groups.id=$1 } defer rows.Close() - workerGroup := &models.WorkerGroupWithMonitors{} + workerGroup := &models.WorkerGroupWithChecks{} for rows.Next() { - var monitorName *string + var checkName *string err = rows.Scan( &workerGroup.Id, &workerGroup.Name, &workerGroup.CreatedAt, &workerGroup.UpdatedAt, - &monitorName, + &checkName, ) if err != nil { return nil, err } - if monitorName != nil { - workerGroup.Monitors = append(workerGroup.Monitors, *monitorName) + if checkName != nil { + workerGroup.Checks = append(workerGroup.Checks, *checkName) } } diff --git a/internal/workflows/check.go b/internal/workflows/check.go new file mode 100644 index 0000000..2fe0f4e --- /dev/null +++ b/internal/workflows/check.go @@ -0,0 +1,58 @@ +package workflows + +import ( + "sort" + "time" + + "code.tjo.space/mentos1386/zdravko/database/models" + "code.tjo.space/mentos1386/zdravko/internal/activities" + "go.temporal.io/sdk/workflow" +) + +type CheckWorkflowParam struct { + Script string + CheckId string + WorkerGroupIds []string +} + +func (w *Workflows) CheckWorkflowDefinition(ctx workflow.Context, param CheckWorkflowParam) (models.CheckStatus, error) { + workerGroupIds := param.WorkerGroupIds + sort.Strings(workerGroupIds) + + for _, workerGroupId := range workerGroupIds { + ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ + StartToCloseTimeout: 60 * time.Second, + TaskQueue: workerGroupId, + }) + + heatlcheckParam := activities.HealtcheckParam{ + Script: param.Script, + } + + var checkResult *activities.CheckResult + err := workflow.ExecuteActivity(ctx, w.activities.Check, heatlcheckParam).Get(ctx, &checkResult) + if err != nil { + return models.CheckUnknown, err + } + + status := models.CheckFailure + if checkResult.Success { + status = models.CheckSuccess + } + + historyParam := activities.HealtcheckAddToHistoryParam{ + CheckId: param.CheckId, + Status: status, + Note: checkResult.Note, + WorkerGroupId: workerGroupId, + } + + var historyResult *activities.CheckAddToHistoryResult + err = workflow.ExecuteActivity(ctx, w.activities.CheckAddToHistory, historyParam).Get(ctx, &historyResult) + if err != nil { + return models.CheckUnknown, err + } + } + + return models.CheckSuccess, nil +} diff --git a/internal/workflows/monitor.go b/internal/workflows/monitor.go deleted file mode 100644 index a87cc6c..0000000 --- a/internal/workflows/monitor.go +++ /dev/null @@ -1,58 +0,0 @@ -package workflows - -import ( - "sort" - "time" - - "code.tjo.space/mentos1386/zdravko/database/models" - "code.tjo.space/mentos1386/zdravko/internal/activities" - "go.temporal.io/sdk/workflow" -) - -type MonitorWorkflowParam struct { - Script string - MonitorId string - WorkerGroupIds []string -} - -func (w *Workflows) MonitorWorkflowDefinition(ctx workflow.Context, param MonitorWorkflowParam) (models.MonitorStatus, error) { - workerGroupIds := param.WorkerGroupIds - sort.Strings(workerGroupIds) - - for _, workerGroupId := range workerGroupIds { - ctx = workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ - StartToCloseTimeout: 60 * time.Second, - TaskQueue: workerGroupId, - }) - - heatlcheckParam := activities.HealtcheckParam{ - Script: param.Script, - } - - var monitorResult *activities.MonitorResult - err := workflow.ExecuteActivity(ctx, w.activities.Monitor, heatlcheckParam).Get(ctx, &monitorResult) - if err != nil { - return models.MonitorUnknown, err - } - - status := models.MonitorFailure - if monitorResult.Success { - status = models.MonitorSuccess - } - - historyParam := activities.HealtcheckAddToHistoryParam{ - MonitorId: param.MonitorId, - Status: status, - Note: monitorResult.Note, - WorkerGroupId: workerGroupId, - } - - var historyResult *activities.MonitorAddToHistoryResult - err = workflow.ExecuteActivity(ctx, w.activities.MonitorAddToHistory, historyParam).Get(ctx, &historyResult) - if err != nil { - return models.MonitorUnknown, err - } - } - - return models.MonitorSuccess, nil -} diff --git a/pkg/api/monitors.go b/pkg/api/checks.go similarity index 65% rename from pkg/api/monitors.go rename to pkg/api/checks.go index 726f9ce..dc86cd4 100644 --- a/pkg/api/monitors.go +++ b/pkg/api/checks.go @@ -2,8 +2,8 @@ package api import "code.tjo.space/mentos1386/zdravko/database/models" -type ApiV1MonitorsHistoryPOSTBody struct { - Status models.MonitorStatus `json:"status"` +type ApiV1ChecksHistoryPOSTBody struct { + Status models.CheckStatus `json:"status"` Note string `json:"note"` WorkerGroupId string `json:"worker_group"` } diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 91b47ed..165f6a6 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -72,14 +72,16 @@ func Routes( //settings.GET("/targets/:id/disable", h.SettingsTargetsDisableGET) //settings.GET("/targets/:id/enable", h.SettingsTargetsEnableGET) - settings.GET("/monitors", h.SettingsMonitorsGET) - settings.GET("/monitors/create", h.SettingsMonitorsCreateGET) - settings.POST("/monitors/create", h.SettingsMonitorsCreatePOST) - settings.GET("/monitors/:id", h.SettingsMonitorsDescribeGET) - settings.POST("/monitors/:id", h.SettingsMonitorsDescribePOST) - settings.GET("/monitors/:id/delete", h.SettingsMonitorsDescribeDELETE) - settings.GET("/monitors/:id/disable", h.SettingsMonitorsDisableGET) - settings.GET("/monitors/:id/enable", h.SettingsMonitorsEnableGET) + settings.GET("/incidents", h.SettingsIncidentsGET) + + settings.GET("/checks", h.SettingsChecksGET) + settings.GET("/checks/create", h.SettingsChecksCreateGET) + settings.POST("/checks/create", h.SettingsChecksCreatePOST) + settings.GET("/checks/:id", h.SettingsChecksDescribeGET) + settings.POST("/checks/:id", h.SettingsChecksDescribePOST) + settings.GET("/checks/:id/delete", h.SettingsChecksDescribeDELETE) + settings.GET("/checks/:id/disable", h.SettingsChecksDisableGET) + settings.GET("/checks/:id/enable", h.SettingsChecksEnableGET) settings.GET("/notifications", h.SettingsNotificationsGET) @@ -101,7 +103,6 @@ func Routes( apiv1 := e.Group("/api/v1") apiv1.Use(h.Authenticated) apiv1.GET("/workers/connect", h.ApiV1WorkersConnectGET) - apiv1.POST("/monitors/:id/history", h.ApiV1MonitorsHistoryPOST) // Error handler e.HTTPErrorHandler = func(err error, c echo.Context) { diff --git a/pkg/server/worker.go b/pkg/server/worker.go index e505905..d0963b6 100644 --- a/pkg/server/worker.go +++ b/pkg/server/worker.go @@ -20,7 +20,7 @@ func NewWorker(temporalClient client.Client, cfg *config.ServerConfig) *Worker { workerWorkflows := workflows.NewWorkflows(workerActivities) // Register Workflows - w.RegisterWorkflow(workerWorkflows.MonitorWorkflowDefinition) + w.RegisterWorkflow(workerWorkflows.CheckWorkflowDefinition) return &Worker{ worker: w, diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go index 61f53d9..81273e2 100644 --- a/pkg/worker/worker.go +++ b/pkg/worker/worker.go @@ -92,11 +92,11 @@ func (w *Worker) Start() error { workerWorkflows := workflows.NewWorkflows(workerActivities) // Register Workflows - w.worker.RegisterWorkflow(workerWorkflows.MonitorWorkflowDefinition) + w.worker.RegisterWorkflow(workerWorkflows.CheckWorkflowDefinition) // Register Activities - w.worker.RegisterActivity(workerActivities.Monitor) - w.worker.RegisterActivity(workerActivities.MonitorAddToHistory) + w.worker.RegisterActivity(workerActivities.Check) + w.worker.RegisterActivity(workerActivities.CheckAddToHistory) return w.worker.Run(worker.InterruptCh()) } diff --git a/web/static/css/main.css b/web/static/css/main.css index 8714499..3f91b24 100644 --- a/web/static/css/main.css +++ b/web/static/css/main.css @@ -51,9 +51,6 @@ code { @apply shadow; } -.sidebar { - @apply flex flex-row flex-wrap justify-center lg:flex-col lg:w-48 gap-2 h-fit text-sm font-medium text-gray-900; -} .sidebar a { @apply w-full block rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-700 focus:text-blue-700; } diff --git a/web/static/css/tailwind.css b/web/static/css/tailwind.css index 83f35cb..b9e7b51 100644 --- a/web/static/css/tailwind.css +++ b/web/static/css/tailwind.css @@ -825,6 +825,10 @@ video { flex-direction: column; } +.flex-wrap { + flex-wrap: wrap; +} + .items-center { align-items: center; } @@ -1361,28 +1365,6 @@ code { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.sidebar { - display: flex; - height: -moz-fit-content; - height: fit-content; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - gap: 0.5rem; - font-size: 0.875rem; - line-height: 1.25rem; - font-weight: 500; - --tw-text-opacity: 1; - color: rgb(17 24 39 / var(--tw-text-opacity)); -} - -@media (min-width: 1024px) { - .sidebar { - width: 12rem; - flex-direction: column; - } -} - .sidebar a { display: block; width: 100%; @@ -1856,10 +1838,22 @@ code { margin-top: 5rem; } + .lg\:w-48 { + width: 12rem; + } + .lg\:grid-cols-\[min-content_minmax\(0\2c 1fr\)\] { grid-template-columns: min-content minmax(0,1fr); } + .lg\:flex-col { + flex-direction: column; + } + + .lg\:items-start { + align-items: flex-start; + } + .lg\:justify-start { justify-content: flex-start; } diff --git a/web/templates/components/settings.tmpl b/web/templates/components/settings.tmpl index ce72719..40dabae 100644 --- a/web/templates/components/settings.tmpl +++ b/web/templates/components/settings.tmpl @@ -10,15 +10,17 @@