v0.309.0: the notification page shows three group switches; details and cooldown under Részletek (R-388 slice 1, decision 200)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
+18
-3
@@ -2910,8 +2910,23 @@ live hardware), which is exactly what the marker covers.
|
||||
|
||||
#### Default Enabled Events
|
||||
|
||||
Events the customer receives notifications for (configurable in settings):
|
||||
`backup_failed`, `db_dump_failed`, `disk_warning`, `disk_critical`, `storage_disconnected`, `node_down`, `health_critical`, `expected_backup_missed`, `expected_dbdump_missed`
|
||||
Events the customer receives notifications for (configurable in settings) — `settings.DefaultEnabledEvents`:
|
||||
`backup_failed`, `db_dump_failed`, `disk_warning`, `disk_critical`, `storage_disconnected`, `node_down`, `health_critical`, `expected_backup_missed`, `expected_dbdump_missed`, `offbox_enlarge_blocked`, `app_update_undone`, `app_update_held`, `app_stopped_unhealthy`
|
||||
|
||||
#### The page: three group switches (v0.309.0, R-388 slice 1, `08` §8.1, `09` §3 decision 200)
|
||||
|
||||
The notification page shows three switches instead of one per detector — „A gépem és a meghajtóim" / "My
|
||||
machine and drives", „Az alkalmazásaim" / "My apps", „Amit mi intézünk" / "What we handle" — each with one
|
||||
sentence of what it sends. The membership table is `settings.NotificationGroups` (every detector on the page
|
||||
is in exactly one group, pinned by `TestR388_EveryPageSwitchIsInExactlyOneGroup`). A group is a VIEW over the
|
||||
same `enabled_events` list: no wire change, no hub change. A group with some detectors on and some off shows
|
||||
as „egyedi" / "custom". The per-detector switches and the cooldown field sit under a „Részletek" / "Details"
|
||||
fold (`<details>`, no script). The save rule (`Server.applyNotificationGroupSwitches`): a group switch the
|
||||
household MOVED wins (sets or clears all its detectors); a group switch left where it was rendered leaves the
|
||||
per-detector switches authoritative — so a custom group saved untouched writes the list back byte-for-byte
|
||||
(`TestR388_CustomGroupSavedBackUnchanged`). A POST without the `group_<key>_shown` fields (an old page) is
|
||||
handled exactly as before. Note: today's default list fills NO group completely (each misses a member), so a
|
||||
fresh box shows all three as „egyedi" (`TestR388_DefaultListAsGroups` pins this until the default is decided).
|
||||
|
||||
#### Preference Sync
|
||||
|
||||
@@ -3249,7 +3264,7 @@ Five sections:
|
||||
2. **Version & update** — current/latest version, check/update buttons, auto-update status, last update result
|
||||
3. **Storage paths** — add/remove, edit labels, set default, toggle schedulable, per-path app list with sizes, safe disconnect/reconnect for USB drives
|
||||
4. **Password change** — current + new + confirm, min 8 chars
|
||||
5. **Notifications** — email, event checkboxes, cooldown hours, test email button
|
||||
5. **Notifications** — email, three group switches (v0.309.0), the per-event checkboxes and cooldown hours under „Részletek", test email button
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1943,7 +1943,6 @@
|
||||
"settings_notifications.ertesitesi_szunet": "Notification pause",
|
||||
"settings_notifications.ez_jelenik_meg_a_kimeno": "This appears as the sender of outgoing e-mails, next to the app name.",
|
||||
"settings_notifications.felado_neve_opcionalis": "Sender name (optional)",
|
||||
"settings_notifications.hibak_es_figyelmeztetesek": "Errors and warnings:",
|
||||
"settings_notifications.lemez_beteloben_90_felett": "Disk filling up (over 90%)",
|
||||
"settings_notifications.lemez_megtelt_vagy_meghibasodott": "Disk full or failed",
|
||||
"settings_notifications.masodlagos_mentes_sikertelen": "Secondary backup failed",
|
||||
@@ -1957,9 +1956,19 @@
|
||||
"settings_notifications.rendszer_allapot_helyreallt": "System health recovered",
|
||||
"settings_notifications.rendszer_allapot_kritikus": "System health critical",
|
||||
"settings_notifications.szerver_nem_elerheto": "Server not reachable",
|
||||
"settings_notifications.tajekoztato": "For information:",
|
||||
"settings_notifications.tavoli_mentes_tarhelykeret_figyelmeztete": "Remote backup — storage quota warning",
|
||||
"settings_notifications.teszt_email_kuldese": "Send test e-mail",
|
||||
"settings_notifications.groups_label": "What should we e-mail you about?",
|
||||
"settings_notifications.group_machine": "My machine and drives",
|
||||
"settings_notifications.group_machine_desc": "You get an e-mail when a drive is unplugged or back, filling up, full or failing, or the off-site backup is short of space. These need you.",
|
||||
"settings_notifications.group_apps": "My apps",
|
||||
"settings_notifications.group_apps_desc": "You get an e-mail when an app is not running or we stopped it because it kept crashing, and when an update was undone or failed.",
|
||||
"settings_notifications.group_handled": "What we handle",
|
||||
"settings_notifications.group_handled_desc": "You get an e-mail when a backup failed or did not run, or the machine cannot be reached. We see these too and deal with them; you need not do anything.",
|
||||
"settings_notifications.group_custom": "custom",
|
||||
"settings_notifications.group_custom_hint": "This group has your own selection. Switching it on turns on all of its notifications.",
|
||||
"settings_notifications.reszletek": "Details",
|
||||
"settings_notifications.reszletek_hint": "Here you can switch each notification on or off one by one.",
|
||||
"settings_security.a_cloudflare_api_token_nincs": "The Cloudflare API token is not configured. Ask the operator to set it up.<br>\n <small>The token needs the <strong>Zone WAF:Edit</strong> permission.</small>",
|
||||
"settings_security.a_helyi_halozati_hozzaferes_mindig": "Access from the local network is always allowed (it does not pass through Cloudflare).",
|
||||
"settings_security.a_jelszavas_vedelem_nincs_beallitva": "Password protection is not set up. Ask the operator to set it up.",
|
||||
|
||||
@@ -1931,7 +1931,6 @@
|
||||
"settings_notifications.ertesitesi_szunet": "Értesítési szünet",
|
||||
"settings_notifications.ez_jelenik_meg_a_kimeno": "Ez jelenik meg a kimenő emailek feladójaként az alkalmazás neve mellett.",
|
||||
"settings_notifications.felado_neve_opcionalis": "Feladó neve (opcionális)",
|
||||
"settings_notifications.hibak_es_figyelmeztetesek": "Hibák és figyelmeztetések:",
|
||||
"settings_notifications.lemez_beteloben_90_felett": "Lemez betelőben (90% felett)",
|
||||
"settings_notifications.lemez_megtelt_vagy_meghibasodott": "Lemez megtelt vagy meghibásodott",
|
||||
"settings_notifications.masodlagos_mentes_sikertelen": "Másodlagos mentés sikertelen",
|
||||
@@ -1945,9 +1944,19 @@
|
||||
"settings_notifications.rendszer_allapot_helyreallt": "Rendszer állapot helyreállt",
|
||||
"settings_notifications.rendszer_allapot_kritikus": "Rendszer állapot kritikus",
|
||||
"settings_notifications.szerver_nem_elerheto": "Szerver nem elérhető",
|
||||
"settings_notifications.tajekoztato": "Tájékoztató:",
|
||||
"settings_notifications.tavoli_mentes_tarhelykeret_figyelmeztete": "Távoli mentés — tárhelykeret-figyelmeztetés",
|
||||
"settings_notifications.teszt_email_kuldese": "Teszt email küldése",
|
||||
"settings_notifications.groups_label": "Miről küldjünk levelet?",
|
||||
"settings_notifications.group_machine": "A gépem és a meghajtóim",
|
||||
"settings_notifications.group_machine_desc": "Levelet kapsz, ha egy meghajtó leválik vagy visszakerül, betelőben van, megtelt vagy meghibásodott, vagy a távoli mentésnek kevés a helye. Ezekkel neked van teendőd.",
|
||||
"settings_notifications.group_apps": "Az alkalmazásaim",
|
||||
"settings_notifications.group_apps_desc": "Levelet kapsz, ha egy alkalmazás nem fut, vagy leállítottuk, mert újra és újra összeomlott, és ha egy frissítést visszavontunk vagy nem sikerült.",
|
||||
"settings_notifications.group_handled": "Amit mi intézünk",
|
||||
"settings_notifications.group_handled_desc": "Levelet kapsz, ha egy mentés nem sikerült vagy elmaradt, vagy a gép nem elérhető. Ezeket mi is látjuk és intézzük, neked nincs teendőd.",
|
||||
"settings_notifications.group_custom": "egyedi",
|
||||
"settings_notifications.group_custom_hint": "Ennél a csoportnál egyedi beállításod van. Ha bekapcsolod, minden értesítése bekapcsol.",
|
||||
"settings_notifications.reszletek": "Részletek",
|
||||
"settings_notifications.reszletek_hint": "Itt egyenként is be- és kikapcsolhatod az értesítéseket.",
|
||||
"settings_security.a_cloudflare_api_token_nincs": "A Cloudflare API token nincs konfigurálva. Kérd az üzemeltetőt a beállításhoz.<br>\n <small>A tokennek <strong>Zone WAF:Edit</strong> jogosultsággal kell rendelkeznie.</small>",
|
||||
"settings_security.a_helyi_halozati_hozzaferes_mindig": "A helyi hálózati hozzáférés mindig engedélyezett (nem halad át a Cloudflare-en).",
|
||||
"settings_security.a_jelszavas_vedelem_nincs_beallitva": "A jelszavas védelem nincs beállítva. Kérd az üzemeltetőt a beállításhoz.",
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package settings
|
||||
|
||||
// R-388 slice 1 (`08-alarm-ladder.md` §8.1, `09` §3 decision 200; operator answers 2026-10-12):
|
||||
// the notification page shows THREE group switches instead of one switch per detector. A group is a
|
||||
// view over the SAME `enabled_events` list — saving a group writes all its detectors into it, so
|
||||
// nothing on the wire and nothing on the hub changes.
|
||||
//
|
||||
// Every detector the page can save belongs to EXACTLY ONE group. Pinned by
|
||||
// TestR388_EveryPageSwitchIsInExactlyOneGroup (web), which reads the rendered page's switches and
|
||||
// fails when a new detector gets a checkbox but no group.
|
||||
|
||||
// Notification group keys (also the form-field stems `group_<key>` / `group_<key>_shown`).
|
||||
const (
|
||||
NotifGroupMachine = "machine" // „A gépem és a meghajtóim" — the household acts
|
||||
NotifGroupApps = "apps" // „Az alkalmazásaim"
|
||||
NotifGroupHandled = "handled" // „Amit mi intézünk" — we act; the operator always gets these
|
||||
)
|
||||
|
||||
// Group states as the page renders them.
|
||||
const (
|
||||
NotifGroupOn = "on"
|
||||
NotifGroupOff = "off"
|
||||
NotifGroupCustom = "custom" // some of its detectors on, some off — „egyedi"
|
||||
)
|
||||
|
||||
// NotificationGroup is one switch on the notification page and the detectors behind it.
|
||||
type NotificationGroup struct {
|
||||
Key string
|
||||
Events []string
|
||||
}
|
||||
|
||||
// NotificationGroups is the §8.1 membership table, in page order.
|
||||
var NotificationGroups = []NotificationGroup{
|
||||
{Key: NotifGroupMachine, Events: []string{
|
||||
"storage_disconnected", "storage_reconnected", "disk_warning", "disk_critical", "offbox_enlarge_blocked",
|
||||
}},
|
||||
{Key: NotifGroupApps, Events: []string{
|
||||
"app_stopped_unhealthy", "app_start_failed", "app_update_held", "app_update_undone",
|
||||
}},
|
||||
{Key: NotifGroupHandled, Events: []string{
|
||||
"backup_failed", "db_dump_failed", "backup_integrity_failed", "crossdrive_failed",
|
||||
"expected_backup_missed", "expected_dbdump_missed", "node_down", "health_critical", "health_recovered",
|
||||
}},
|
||||
}
|
||||
|
||||
// NotificationGroupByKey returns the group with this key, or false.
|
||||
func NotificationGroupByKey(key string) (NotificationGroup, bool) {
|
||||
for _, g := range NotificationGroups {
|
||||
if g.Key == key {
|
||||
return g, true
|
||||
}
|
||||
}
|
||||
return NotificationGroup{}, false
|
||||
}
|
||||
|
||||
// NotificationGroupState reports whether all, none or some of the group's detectors are in enabled.
|
||||
// An unknown key reads as off (renders unchecked, and the save leaves the details authoritative).
|
||||
func NotificationGroupState(enabled []string, key string) string {
|
||||
g, ok := NotificationGroupByKey(key)
|
||||
if !ok {
|
||||
return NotifGroupOff
|
||||
}
|
||||
set := make(map[string]bool, len(enabled))
|
||||
for _, e := range enabled {
|
||||
set[e] = true
|
||||
}
|
||||
on := 0
|
||||
for _, e := range g.Events {
|
||||
if set[e] {
|
||||
on++
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case on == len(g.Events):
|
||||
return NotifGroupOn
|
||||
case on == 0:
|
||||
return NotifGroupOff
|
||||
default:
|
||||
return NotifGroupCustom
|
||||
}
|
||||
}
|
||||
|
||||
// GroupState is the template's view of NotificationGroupState over this household's list.
|
||||
// VALUE receiver on purpose: a pointer receiver compiles and passes vet, then 500s at render
|
||||
// (`.claude/rules/ui-hungarian.md`).
|
||||
func (p NotificationPrefs) GroupState(key string) string {
|
||||
return NotificationGroupState(p.EnabledEvents, key)
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/infra"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/logx"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/scheduler"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
@@ -2846,6 +2847,47 @@ func dedupeEvents(in []string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// applyNotificationGroupSwitches — R-388 slice 1 (`08` §8.1). The page posts, per group, the switch
|
||||
// (`group_<key>` = "on" when ticked) and the state it was RENDERED in (`group_<key>_shown`:
|
||||
// on / off / custom). The per-detector switches under „Részletek" are posted too, so the rule is:
|
||||
//
|
||||
// - the household MOVED the group switch (rendered on → now off, rendered off/custom → now on):
|
||||
// the group wins — all its detectors are set, or all cleared;
|
||||
// - the switch is where it was rendered: the per-detector switches are authoritative, exactly as
|
||||
// before this slice. So a „custom" (egyedi) group saved untouched writes its detectors back as
|
||||
// they were — no household's saved choice changes (TestR388_CustomGroupSavedBackUnchanged).
|
||||
//
|
||||
// A form with no `_shown` field (an old page left open, a bookmarked POST) is not touched by this
|
||||
// step at all: the per-detector names decide, as in v0.308.0.
|
||||
func (s *Server) applyNotificationGroupSwitches(r *http.Request, enabled []string) []string {
|
||||
for _, g := range settings.NotificationGroups {
|
||||
shown := r.FormValue("group_" + g.Key + "_shown")
|
||||
if shown != settings.NotifGroupOn && shown != settings.NotifGroupOff && shown != settings.NotifGroupCustom {
|
||||
continue
|
||||
}
|
||||
on := r.FormValue("group_"+g.Key) == "on"
|
||||
switch {
|
||||
case shown == settings.NotifGroupOn && !on:
|
||||
members := make(map[string]bool, len(g.Events))
|
||||
for _, e := range g.Events {
|
||||
members[e] = true
|
||||
}
|
||||
kept := enabled[:0:0]
|
||||
for _, e := range enabled {
|
||||
if !members[e] {
|
||||
kept = append(kept, e)
|
||||
}
|
||||
}
|
||||
enabled = kept
|
||||
logx.Debugf(s.logger, "[web] notification group switched off: group=%s", g.Key)
|
||||
case shown != settings.NotifGroupOn && on:
|
||||
enabled = append(enabled, g.Events...)
|
||||
logx.Debugf(s.logger, "[web] notification group switched on: group=%s was=%s", g.Key, shown)
|
||||
}
|
||||
}
|
||||
return enabled
|
||||
}
|
||||
|
||||
func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.ParseForm()
|
||||
|
||||
@@ -2916,6 +2958,7 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
|
||||
enabledEvents = append(enabledEvents, c.events...)
|
||||
}
|
||||
}
|
||||
enabledEvents = s.applyNotificationGroupSwitches(r, enabledEvents)
|
||||
enabledEvents = dedupeEvents(enabledEvents)
|
||||
|
||||
// R-329 Part 4 — A SAVE THAT CHANGES NOTHING MUST STORE NOTHING NEW.
|
||||
|
||||
@@ -3,6 +3,7 @@ package web
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
@@ -262,7 +263,7 @@ func i18nCasesA() []i18nCase {
|
||||
d := i18nLayoutData("settings-notifications", "Értesítések")
|
||||
d["HubEnabled"] = true
|
||||
d["NotificationSuccess"], d["NotificationError"] = "A beállítás elmentve.", "A mentés nem sikerült."
|
||||
d["NotificationPrefs"] = map[string]interface{}{"Email": "anna@example.hu", "EnabledEvents": []string{"backup_failed", "node_down"}, "CooldownHours": 4}
|
||||
d["NotificationPrefs"] = &settings.NotificationPrefs{Email: "anna@example.hu", EnabledEvents: []string{"backup_failed", "node_down"}, CooldownHours: 4}
|
||||
d["AppEmailAvailable"], d["AppEmailEnabled"], d["AppEmailFromName"] = true, true, "Kovács család"
|
||||
d["AppEmailSuccess"], d["AppEmailError"] = "Az e-mail beállítás mentve.", "Az e-mail beállítás hibás."
|
||||
return d
|
||||
@@ -272,7 +273,15 @@ func i18nCasesA() []i18nCase {
|
||||
d := i18nLayoutData("settings-notifications", "Értesítések")
|
||||
d["HubEnabled"] = true
|
||||
d["NotificationClearConfirm"] = true
|
||||
d["NotificationPrefs"] = map[string]interface{}{"Email": "", "EnabledEvents": []string{}, "CooldownHours": 6}
|
||||
d["NotificationPrefs"] = &settings.NotificationPrefs{Email: "", EnabledEvents: []string{}, CooldownHours: 6}
|
||||
return d
|
||||
}},
|
||||
// v0.309.0 (R-388 slice 1, decision 200): every group „egyedi" (custom) — some of its detectors on,
|
||||
// some off — so each group's custom badge and hint renders; captured when born.
|
||||
{"settings_notifications_groups_custom", "settings_notifications", func() map[string]interface{} {
|
||||
d := i18nLayoutData("settings-notifications", "Értesítések")
|
||||
d["HubEnabled"] = true
|
||||
d["NotificationPrefs"] = &settings.NotificationPrefs{Email: "anna@example.hu", EnabledEvents: []string{"disk_warning", "app_update_held", "backup_failed"}, CooldownHours: 6}
|
||||
return d
|
||||
}},
|
||||
{"settings_notifications_nohub", "settings_notifications", func() map[string]interface{} {
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// R-388 slice 1 (`08-alarm-ladder.md` §8.1, `09` §3 decision 200) — the notification page shows three
|
||||
// group switches; the per-detector switches and the cooldown live under a „Részletek" fold.
|
||||
//
|
||||
// THE LAYER: the REAL render (`settingsNotificationsPageHandler`) and the REAL save
|
||||
// (`settingsNotificationsHandler`) over a REAL temp-file `Settings`. `renderedForm` posts back exactly
|
||||
// what a browser would — every ticked checkbox, every hidden field, the address and the cooldown — so
|
||||
// the round trips below exercise what the TEMPLATE renders, not what a test assumes it renders.
|
||||
|
||||
var r388InputRe = regexp.MustCompile(`<input ([^>]*)>`)
|
||||
var r388AttrRe = regexp.MustCompile(`([a-z]+)(?:="([^"]*)")?`)
|
||||
|
||||
// renderPage renders the notifications page and returns its body.
|
||||
func r388RenderPage(t *testing.T, s *Server) string {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodGet, "/settings/notifications", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
s.settingsNotificationsPageHandler(rr, req)
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Fatalf("render: HTTP %d", rr.Code)
|
||||
}
|
||||
return rr.Body.String()
|
||||
}
|
||||
|
||||
// renderedForm returns what a browser submits for the notifications form when the household presses
|
||||
// Save without touching anything.
|
||||
func renderedForm(t *testing.T, body string) url.Values {
|
||||
t.Helper()
|
||||
start := strings.Index(body, `action="/settings/notifications"`)
|
||||
end := strings.Index(body, `action="/settings/app-email"`)
|
||||
if start < 0 {
|
||||
t.Fatalf("the notifications form did not render — this test would then prove nothing")
|
||||
}
|
||||
if end < start {
|
||||
end = len(body)
|
||||
}
|
||||
form := url.Values{}
|
||||
for _, m := range r388InputRe.FindAllStringSubmatch(body[start:end], -1) {
|
||||
attrs := map[string]string{}
|
||||
for _, a := range r388AttrRe.FindAllStringSubmatch(m[1], -1) {
|
||||
attrs[a[1]] = a[2]
|
||||
}
|
||||
if _, isChecked := attrs["checked"]; attrs["type"] == "checkbox" {
|
||||
if isChecked {
|
||||
form.Set(attrs["name"], "on")
|
||||
}
|
||||
continue
|
||||
}
|
||||
if attrs["name"] != "" {
|
||||
form.Set(attrs["name"], attrs["value"])
|
||||
}
|
||||
}
|
||||
return form
|
||||
}
|
||||
|
||||
func r388Seed(t *testing.T, sett *settings.Settings, events []string) {
|
||||
t.Helper()
|
||||
if err := sett.SetNotificationPrefs(&settings.NotificationPrefs{
|
||||
Email: "seed@felhom.eu", EnabledEvents: events, CooldownHours: 6,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func r388Save(t *testing.T, s *Server, form url.Values) {
|
||||
t.Helper()
|
||||
if rr := postNotifications(t, s, form); rr.Code >= 400 {
|
||||
t.Fatalf("save: HTTP %d", rr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func sortedCopy(in []string) []string {
|
||||
out := append([]string(nil), in...)
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Every per-detector switch the page renders belongs to exactly one group, and every group member has
|
||||
// a switch — a new detector that gets a checkbox but no group fails here (the invariant stated in
|
||||
// settings/notif_groups.go).
|
||||
func TestR388_EveryPageSwitchIsInExactlyOneGroup(t *testing.T) {
|
||||
s, _ := notifyGuardServer(t)
|
||||
body := r388RenderPage(t, s)
|
||||
page := map[string]bool{}
|
||||
for _, m := range regexp.MustCompile(`name="event_([a-z_]+)"`).FindAllStringSubmatch(body, -1) {
|
||||
page[m[1]] = true
|
||||
}
|
||||
if len(page) < 18 {
|
||||
t.Fatalf("only %d per-detector switches rendered, want ≥18 — the probe is not reading the page", len(page))
|
||||
}
|
||||
member := map[string]int{}
|
||||
for _, g := range settings.NotificationGroups {
|
||||
for _, e := range g.Events {
|
||||
member[e]++
|
||||
}
|
||||
}
|
||||
for e := range page {
|
||||
if member[e] != 1 {
|
||||
t.Errorf("switch event_%s is in %d groups, want exactly 1 (`08` §8.1 table)", e, member[e])
|
||||
}
|
||||
}
|
||||
for e := range member {
|
||||
if !page[e] {
|
||||
t.Errorf("group member %s has no switch on the page", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Each group, switched on from an empty list, writes exactly its detectors; switched off again (the
|
||||
// details still render them ticked), it clears them — the group switch wins over the fold.
|
||||
func TestR388_EachGroupRoundTrips(t *testing.T) {
|
||||
for _, g := range settings.NotificationGroups {
|
||||
t.Run(g.Key, func(t *testing.T) {
|
||||
s, sett := notifyGuardServer(t)
|
||||
r388Seed(t, sett, []string{})
|
||||
|
||||
form := renderedForm(t, r388RenderPage(t, s))
|
||||
if got := form.Get("group_" + g.Key + "_shown"); got != settings.NotifGroupOff {
|
||||
t.Fatalf("empty list rendered group %s as %q, want off", g.Key, got)
|
||||
}
|
||||
form.Set("group_"+g.Key, "on")
|
||||
r388Save(t, s, form)
|
||||
if got := sett.GetNotificationPrefs().EnabledEvents; !reflect.DeepEqual(sortedCopy(got), sortedCopy(g.Events)) {
|
||||
t.Fatalf("group %s switched on stored %v, want exactly %v", g.Key, got, g.Events)
|
||||
}
|
||||
|
||||
form = renderedForm(t, r388RenderPage(t, s))
|
||||
if form.Get("group_"+g.Key) != "on" || form.Get("group_"+g.Key+"_shown") != settings.NotifGroupOn {
|
||||
t.Fatalf("group %s did not render ON after it was saved on: %v", g.Key, form)
|
||||
}
|
||||
form.Del("group_" + g.Key) // the household unticks the group switch
|
||||
r388Save(t, s, form)
|
||||
if got := sett.GetNotificationPrefs().EnabledEvents; len(got) != 0 {
|
||||
t.Fatalf("group %s switched off still stores %v — the ticked details overrode the group switch", g.Key, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A list that mixes a group (some on, some off) renders that group as „egyedi", and saving the page
|
||||
// untouched stores the list byte-for-byte as it was — no household's saved choice changes.
|
||||
func TestR388_CustomGroupSavedBackUnchanged(t *testing.T) {
|
||||
cases := map[string][]string{
|
||||
"defaults": append([]string(nil), settings.DefaultEnabledEvents...),
|
||||
"mixed": {"disk_warning", "app_update_held", "backup_failed", "health_recovered"},
|
||||
}
|
||||
for name, stored := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
s, sett := notifyGuardServer(t)
|
||||
r388Seed(t, sett, stored)
|
||||
before := append([]string(nil), sett.GetNotificationPrefs().EnabledEvents...)
|
||||
|
||||
body := r388RenderPage(t, s)
|
||||
form := renderedForm(t, body)
|
||||
customs := 0
|
||||
for _, g := range settings.NotificationGroups {
|
||||
want := settings.NotificationGroupState(before, g.Key)
|
||||
if got := form.Get("group_" + g.Key + "_shown"); got != want {
|
||||
t.Errorf("group %s rendered %q, want %q", g.Key, got, want)
|
||||
}
|
||||
if want == settings.NotifGroupCustom {
|
||||
customs++
|
||||
if form.Get("group_"+g.Key) == "on" {
|
||||
t.Errorf("custom group %s rendered with its switch ticked", g.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
if customs == 0 {
|
||||
t.Fatalf("case %s has no custom group — it would prove nothing", name)
|
||||
}
|
||||
// ASCII fragment of „egyedi" in the badge (positive control); its absence on an all-on page is
|
||||
// checked in TestR388_FullListRendersThreeGroupsOn (negative control).
|
||||
if !strings.Contains(body, `<span class="badge badge-neutral">egyedi</span>`) {
|
||||
t.Errorf("no „egyedi\" badge rendered for a custom group")
|
||||
}
|
||||
|
||||
r388Save(t, s, form)
|
||||
if after := sett.GetNotificationPrefs().EnabledEvents; !reflect.DeepEqual(before, after) {
|
||||
t.Errorf("an untouched save CHANGED the stored list.\n before: %v\n after: %v", before, after)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A custom group switched ON turns on all of its detectors and leaves the other groups as they were.
|
||||
func TestR388_CustomGroupSwitchedOnTakesAll(t *testing.T) {
|
||||
s, sett := notifyGuardServer(t)
|
||||
r388Seed(t, sett, []string{"disk_warning", "backup_failed"})
|
||||
form := renderedForm(t, r388RenderPage(t, s))
|
||||
form.Set("group_machine", "on")
|
||||
r388Save(t, s, form)
|
||||
machine, _ := settings.NotificationGroupByKey(settings.NotifGroupMachine)
|
||||
want := sortedCopy(append(append([]string(nil), machine.Events...), "backup_failed"))
|
||||
if got := sortedCopy(sett.GetNotificationPrefs().EnabledEvents); !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("stored %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// Every detector on → three groups rendered on, no „egyedi" badge.
|
||||
func TestR388_FullListRendersThreeGroupsOn(t *testing.T) {
|
||||
s, sett := notifyGuardServer(t)
|
||||
var all []string
|
||||
for _, g := range settings.NotificationGroups {
|
||||
all = append(all, g.Events...)
|
||||
}
|
||||
r388Seed(t, sett, all)
|
||||
body := r388RenderPage(t, s)
|
||||
form := renderedForm(t, body)
|
||||
for _, g := range settings.NotificationGroups {
|
||||
if form.Get("group_"+g.Key) != "on" || form.Get("group_"+g.Key+"_shown") != settings.NotifGroupOn {
|
||||
t.Errorf("group %s not rendered on for the full list", g.Key)
|
||||
}
|
||||
}
|
||||
if strings.Contains(body, "egyedi") {
|
||||
t.Errorf("an all-on page shows an „egyedi\" badge")
|
||||
}
|
||||
}
|
||||
|
||||
// Today's DefaultEnabledEvents, read as groups. §8.1 and the operator's Q1 want all three ON for a new
|
||||
// household; today NONE is fully on (each misses a member: storage_reconnected; app_start_failed, OFF
|
||||
// by the R-329 ruling; backup_integrity_failed, crossdrive_failed, health_recovered). This slice does
|
||||
// NOT change the default (the lead decides). The test pins the fact so a change to either side is seen;
|
||||
// when the default is made to cover the groups, flip `want` to on.
|
||||
func TestR388_DefaultListAsGroups(t *testing.T) {
|
||||
want := map[string]string{
|
||||
settings.NotifGroupMachine: settings.NotifGroupCustom,
|
||||
settings.NotifGroupApps: settings.NotifGroupCustom,
|
||||
settings.NotifGroupHandled: settings.NotifGroupCustom,
|
||||
}
|
||||
for _, g := range settings.NotificationGroups {
|
||||
if got := settings.NotificationGroupState(settings.DefaultEnabledEvents, g.Key); got != want[g.Key] {
|
||||
t.Errorf("DefaultEnabledEvents renders group %s as %q, this test pins %q", g.Key, got, want[g.Key])
|
||||
}
|
||||
}
|
||||
// A fresh box (no stored prefs) renders through the same path.
|
||||
s, _ := notifyGuardServer(t)
|
||||
form := renderedForm(t, r388RenderPage(t, s))
|
||||
for _, g := range settings.NotificationGroups {
|
||||
if got := form.Get("group_" + g.Key + "_shown"); got != want[g.Key] {
|
||||
t.Errorf("a fresh box renders group %s as %q, want %q", g.Key, got, want[g.Key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The details fold still saves one switch on its own: all groups on, one detector unticked under
|
||||
// „Részletek" → exactly that detector is gone and its group becomes custom.
|
||||
func TestR388_DetailsFoldSavesAnIndividualSwitch(t *testing.T) {
|
||||
s, sett := notifyGuardServer(t)
|
||||
var all []string
|
||||
for _, g := range settings.NotificationGroups {
|
||||
all = append(all, g.Events...)
|
||||
}
|
||||
r388Seed(t, sett, all)
|
||||
body := r388RenderPage(t, s)
|
||||
open := strings.Index(body, `<details class="notif-details"`)
|
||||
if open < 0 {
|
||||
t.Fatalf("no „Részletek\" fold on the page")
|
||||
}
|
||||
fold := body[open:]
|
||||
fold = fold[:strings.Index(fold, `</details>`)]
|
||||
for _, needle := range []string{`name="event_disk_warning"`, `name="cooldown_hours"`} {
|
||||
if !strings.Contains(fold, needle) {
|
||||
t.Errorf("%s is not inside the „Részletek\" fold", needle)
|
||||
}
|
||||
}
|
||||
|
||||
form := renderedForm(t, body)
|
||||
form.Del("event_disk_warning")
|
||||
form.Set("cooldown_hours", "12")
|
||||
r388Save(t, s, form)
|
||||
got := sett.GetNotificationPrefs()
|
||||
var want []string
|
||||
for _, e := range all {
|
||||
if e != "disk_warning" {
|
||||
want = append(want, e)
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(sortedCopy(got.EnabledEvents), sortedCopy(want)) {
|
||||
t.Fatalf("stored %v, want all but disk_warning", got.EnabledEvents)
|
||||
}
|
||||
if got.CooldownHours != 12 {
|
||||
t.Errorf("cooldown under the fold not saved: %d", got.CooldownHours)
|
||||
}
|
||||
if st := got.GroupState(settings.NotifGroupMachine); st != settings.NotifGroupCustom {
|
||||
t.Errorf("machine group after one detector off = %q, want custom", st)
|
||||
}
|
||||
}
|
||||
@@ -24,96 +24,144 @@
|
||||
placeholder="{{T "settings_notifications.pelda_email"}}" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{T "settings_notifications.hibak_es_figyelmeztetesek"}}</label>
|
||||
<div class="checkbox-group">
|
||||
<label>{{T "settings_notifications.groups_label"}}</label>
|
||||
{{with .NotificationPrefs}}{{$st := .GroupState "machine"}}
|
||||
<div class="notif-group">
|
||||
<input type="hidden" name="group_machine_shown" value="{{$st}}">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_backup_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.biztonsagi_mentes_sikertelen"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_db_dump_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "db_dump_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.adatbazis_mentes_sikertelen"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_backup_integrity_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_integrity_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.mentes_serules_eszlelve"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_crossdrive_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "crossdrive_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.masodlagos_mentes_sikertelen"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_offbox_enlarge_blocked" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "offbox_enlarge_blocked"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.tavoli_mentes_tarhelykeret_figyelmeztete"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_disk_warning" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "disk_warning"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.lemez_beteloben_90_felett"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_disk_critical" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "disk_critical"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.lemez_megtelt_vagy_meghibasodott"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_storage_disconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_disconnected"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.meghajto_levalasztva"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_node_down" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "node_down"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.szerver_nem_elerheto"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_health_critical" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_critical"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.rendszer_allapot_kritikus"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_expected_backup_missed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "expected_backup_missed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.elvart_fajlmentes_elmaradt"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_expected_dbdump_missed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "expected_dbdump_missed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.elvart_adatbazismentes_elmaradt"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_app_start_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "app_start_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.alkalmazas_nem_fut"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_app_update_undone" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "app_update_undone"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.app_update_undone"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_app_update_held" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "app_update_held"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.app_update_held"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_app_stopped_unhealthy" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "app_stopped_unhealthy"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.app_stopped_unhealthy"}}</span>
|
||||
<input type="checkbox" name="group_machine" {{if eq $st "on"}}checked{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.group_machine"}}</span>{{if eq $st "custom"}}
|
||||
<span class="badge badge-neutral">{{T "settings_notifications.group_custom"}}</span>{{end}}
|
||||
</label>
|
||||
<span class="form-hint">{{T "settings_notifications.group_machine_desc"}}</span>{{if eq $st "custom"}}
|
||||
<span class="form-hint">{{T "settings_notifications.group_custom_hint"}} <a href="#notif-details">{{T "settings_notifications.reszletek"}}</a></span>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{T "settings_notifications.tajekoztato"}}</label>
|
||||
<div class="checkbox-group">
|
||||
{{end}}
|
||||
{{with .NotificationPrefs}}{{$st := .GroupState "apps"}}
|
||||
<div class="notif-group">
|
||||
<input type="hidden" name="group_apps_shown" value="{{$st}}">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_storage_reconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_reconnected"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.meghajto_ujra_csatlakoztatva"}}</span>
|
||||
<input type="checkbox" name="group_apps" {{if eq $st "on"}}checked{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.group_apps"}}</span>{{if eq $st "custom"}}
|
||||
<span class="badge badge-neutral">{{T "settings_notifications.group_custom"}}</span>{{end}}
|
||||
</label>
|
||||
<span class="form-hint">{{T "settings_notifications.group_apps_desc"}}</span>{{if eq $st "custom"}}
|
||||
<span class="form-hint">{{T "settings_notifications.group_custom_hint"}} <a href="#notif-details">{{T "settings_notifications.reszletek"}}</a></span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{with .NotificationPrefs}}{{$st := .GroupState "handled"}}
|
||||
<div class="notif-group">
|
||||
<input type="hidden" name="group_handled_shown" value="{{$st}}">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_health_recovered" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_recovered"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.rendszer_allapot_helyreallt"}}</span>
|
||||
<input type="checkbox" name="group_handled" {{if eq $st "on"}}checked{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.group_handled"}}</span>{{if eq $st "custom"}}
|
||||
<span class="badge badge-neutral">{{T "settings_notifications.group_custom"}}</span>{{end}}
|
||||
</label>
|
||||
<span class="form-hint">{{T "settings_notifications.group_handled_desc"}}</span>{{if eq $st "custom"}}
|
||||
<span class="form-hint">{{T "settings_notifications.group_custom_hint"}} <a href="#notif-details">{{T "settings_notifications.reszletek"}}</a></span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cooldown_hours">{{T "settings_notifications.ertesitesi_szunet"}}</label>
|
||||
<div class="form-inline">
|
||||
<input type="number" id="cooldown_hours" name="cooldown_hours" min="1" max="168"
|
||||
value="{{with .NotificationPrefs}}{{.CooldownHours}}{{end}}"
|
||||
class="form-control form-control-narrow">
|
||||
<span class="form-hint">{{T "settings_notifications.ora_azonos_problema_eseten_ennyi"}}</span>
|
||||
<details class="notif-details" id="notif-details">
|
||||
<summary>{{T "settings_notifications.reszletek"}}</summary>
|
||||
<p class="form-hint">{{T "settings_notifications.reszletek_hint"}}</p>
|
||||
<div class="form-group">
|
||||
<label>{{T "settings_notifications.group_machine"}}</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_storage_disconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_disconnected"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.meghajto_levalasztva"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_storage_reconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_reconnected"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.meghajto_ujra_csatlakoztatva"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_disk_warning" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "disk_warning"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.lemez_beteloben_90_felett"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_disk_critical" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "disk_critical"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.lemez_megtelt_vagy_meghibasodott"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_offbox_enlarge_blocked" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "offbox_enlarge_blocked"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.tavoli_mentes_tarhelykeret_figyelmeztete"}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{T "settings_notifications.group_apps"}}</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_app_stopped_unhealthy" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "app_stopped_unhealthy"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.app_stopped_unhealthy"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_app_start_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "app_start_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.alkalmazas_nem_fut"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_app_update_held" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "app_update_held"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.app_update_held"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_app_update_undone" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "app_update_undone"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.app_update_undone"}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{T "settings_notifications.group_handled"}}</label>
|
||||
<div class="checkbox-group">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_backup_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.biztonsagi_mentes_sikertelen"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_db_dump_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "db_dump_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.adatbazis_mentes_sikertelen"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_backup_integrity_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_integrity_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.mentes_serules_eszlelve"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_crossdrive_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "crossdrive_failed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.masodlagos_mentes_sikertelen"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_expected_backup_missed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "expected_backup_missed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.elvart_fajlmentes_elmaradt"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_expected_dbdump_missed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "expected_dbdump_missed"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.elvart_adatbazismentes_elmaradt"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_node_down" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "node_down"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.szerver_nem_elerheto"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_health_critical" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_critical"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.rendszer_allapot_kritikus"}}</span>
|
||||
</label>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" name="event_health_recovered" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_recovered"}}checked{{end}}{{end}}{{end}}>
|
||||
<span class="toggle-label">{{T "settings_notifications.rendszer_allapot_helyreallt"}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="cooldown_hours">{{T "settings_notifications.ertesitesi_szunet"}}</label>
|
||||
<div class="form-inline">
|
||||
<input type="number" id="cooldown_hours" name="cooldown_hours" min="1" max="168"
|
||||
value="{{with .NotificationPrefs}}{{.CooldownHours}}{{end}}"
|
||||
class="form-control form-control-narrow">
|
||||
<span class="form-hint">{{T "settings_notifications.ora_azonos_problema_eseten_ennyi"}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">{{T "settings_notifications.mentes"}}</button>
|
||||
<button type="submit" formaction="/settings/notifications/test" class="btn btn-outline">{{T "settings_notifications.teszt_email_kuldese"}}</button>
|
||||
|
||||
@@ -2639,6 +2639,23 @@ a.stat-card:hover {
|
||||
.storage-add-details {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
/* R-388 slice 1 — the notification page's three group switches and the „Részletek" fold */
|
||||
.notif-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .25rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.notif-details {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.notif-details summary {
|
||||
cursor: pointer;
|
||||
font-size: .9rem;
|
||||
}
|
||||
.notif-details[open] summary {
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.storage-add-details[open] summary {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user