feat(notifications): improved notification templates (#4321)

This commit is contained in:
Jarek Kowalski
2024-12-29 16:11:59 -08:00
committed by GitHub
parent c1757a0c67
commit f8bed5dafa
11 changed files with 729 additions and 48 deletions

View File

@@ -0,0 +1,30 @@
package notifydata
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/internal/clock"
)
func TestNewErrorInfo(t *testing.T) {
startTime := clock.Now()
endTime := startTime.Add(2 * time.Second)
err := errors.New("test error") //nolint:err113
e := NewErrorInfo("test operation", "test details", startTime, endTime, err)
require.Equal(t, "test operation", e.Operation)
require.Equal(t, "test details", e.OperationDetails)
require.Equal(t, startTime, e.StartTime)
require.Equal(t, endTime, e.EndTime)
require.Equal(t, "test error", e.ErrorMessage)
require.Equal(t, "test error", e.ErrorDetails)
require.Equal(t, startTime.Truncate(time.Second), e.StartTimestamp())
require.Equal(t, endTime.Truncate(time.Second), e.EndTimestamp())
require.Equal(t, 2*time.Second, e.Duration())
}

View File

@@ -1,6 +1,8 @@
package notifydata
import (
"fmt"
"strings"
"time"
"github.com/kopia/kopia/snapshot"
@@ -56,19 +58,6 @@ func (m *ManifestWithError) TotalSizeDelta() int64 {
return m.Manifest.RootEntry.FileSize
}
// New returns the total size of the snapshot in bytes.
func (m *ManifestWithError) New() int64 {
if m.Manifest.RootEntry == nil {
return 0
}
if m.Manifest.RootEntry.DirSummary != nil {
return m.Manifest.RootEntry.DirSummary.TotalFileSize
}
return m.Manifest.RootEntry.FileSize
}
// TotalFiles returns the total number of files in the snapshot.
func (m *ManifestWithError) TotalFiles() int64 {
if m.Manifest.RootEntry == nil {
@@ -134,30 +123,81 @@ func (m *ManifestWithError) Duration() time.Duration {
return time.Duration(m.Manifest.EndTime - m.Manifest.StartTime).Round(durationPrecision)
}
// Status codes.
const (
StatusCodeIncomplete = "incomplete"
StatusCodeFatal = "fatal"
StatusCodeError = "error"
StatusCodeSuccess = "success"
)
// StatusCode returns the status code of the manifest.
func (m *ManifestWithError) StatusCode() string {
if m.Error != "" {
return "fatal"
return StatusCodeFatal
}
if m.Manifest.IncompleteReason != "" {
return "incomplete"
return StatusCodeIncomplete
}
if m.Manifest.RootEntry != nil && m.Manifest.RootEntry.DirSummary != nil {
if m.Manifest.RootEntry.DirSummary.FatalErrorCount > 0 {
return "fatal"
return StatusCodeFatal
}
if m.Manifest.RootEntry.DirSummary.IgnoredErrorCount > 0 {
return "error"
return StatusCodeError
}
}
return "ok"
return StatusCodeSuccess
}
// MultiSnapshotStatus represents the status of multiple snapshots.
type MultiSnapshotStatus struct {
Snapshots []*ManifestWithError `json:"snapshots"`
}
// OverallStatus returns the overall status of the snapshots.
func (m MultiSnapshotStatus) OverallStatus() string {
var (
numIncomplete int
numFatal int
numErrors int
numSuccess int
)
for _, s := range m.Snapshots {
switch s.StatusCode() {
case StatusCodeIncomplete:
numIncomplete++
case StatusCodeError:
numErrors++
case StatusCodeFatal:
numFatal++
case StatusCodeSuccess:
numSuccess++
}
}
var errorStrings []string
if numFatal > 1 {
errorStrings = append(errorStrings, fmt.Sprintf("%d fatal errors", numFatal))
} else if numFatal == 1 {
errorStrings = append(errorStrings, "a fatal error")
}
if numErrors > 1 {
errorStrings = append(errorStrings, fmt.Sprintf("%d errors", numErrors))
} else if numErrors == 1 {
errorStrings = append(errorStrings, "an error")
}
if len(errorStrings) == 0 {
return "success"
}
return "encountered " + strings.Join(errorStrings, " and ")
}

View File

@@ -0,0 +1,611 @@
package notifydata_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/internal/clock"
"github.com/kopia/kopia/notification/notifydata"
"github.com/kopia/kopia/snapshot"
)
func TestOverallStatus(t *testing.T) {
tests := []struct {
name string
snapshots []*notifydata.ManifestWithError
expected string
}{
{
name: "all success",
snapshots: []*notifydata.ManifestWithError{
{Manifest: snapshot.Manifest{}},
{Manifest: snapshot.Manifest{}},
},
expected: "success",
},
{
name: "one fatal error",
snapshots: []*notifydata.ManifestWithError{
{Manifest: snapshot.Manifest{}, Error: "fatal error"},
{Manifest: snapshot.Manifest{}},
},
expected: "encountered a fatal error",
},
{
name: "multiple fatal errors",
snapshots: []*notifydata.ManifestWithError{
{Manifest: snapshot.Manifest{}, Error: "fatal error"},
{Manifest: snapshot.Manifest{}, Error: "fatal error"},
},
expected: "encountered 2 fatal errors",
},
{
name: "one error",
snapshots: []*notifydata.ManifestWithError{
{Manifest: snapshot.Manifest{RootEntry: &snapshot.DirEntry{DirSummary: &fs.DirectorySummary{IgnoredErrorCount: 1}}}},
{Manifest: snapshot.Manifest{}},
},
expected: "encountered an error",
},
{
name: "one fatal error and two errors",
snapshots: []*notifydata.ManifestWithError{
{Manifest: snapshot.Manifest{}, Error: "fatal error"},
{Manifest: snapshot.Manifest{}},
{Manifest: snapshot.Manifest{RootEntry: &snapshot.DirEntry{DirSummary: &fs.DirectorySummary{IgnoredErrorCount: 1}}}},
{Manifest: snapshot.Manifest{RootEntry: &snapshot.DirEntry{DirSummary: &fs.DirectorySummary{IgnoredErrorCount: 1}}}},
},
expected: "encountered a fatal error and 2 errors",
},
{
name: "one fatal error and one errors",
snapshots: []*notifydata.ManifestWithError{
{Manifest: snapshot.Manifest{}, Error: "fatal error"},
{Manifest: snapshot.Manifest{}},
{Manifest: snapshot.Manifest{RootEntry: &snapshot.DirEntry{DirSummary: &fs.DirectorySummary{IgnoredErrorCount: 1}}}},
},
expected: "encountered a fatal error and an error",
},
{
name: "multiple errors",
snapshots: []*notifydata.ManifestWithError{
{Manifest: snapshot.Manifest{RootEntry: &snapshot.DirEntry{DirSummary: &fs.DirectorySummary{IgnoredErrorCount: 1}}}},
{Manifest: snapshot.Manifest{RootEntry: &snapshot.DirEntry{DirSummary: &fs.DirectorySummary{IgnoredErrorCount: 1}}}},
},
expected: "encountered 2 errors",
},
{
name: "incomplete snapshot",
snapshots: []*notifydata.ManifestWithError{
{Manifest: snapshot.Manifest{IncompleteReason: "incomplete"}},
{Manifest: snapshot.Manifest{}},
},
expected: "success",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mss := notifydata.MultiSnapshotStatus{Snapshots: tt.snapshots}
require.Equal(t, tt.expected, mss.OverallStatus())
})
}
}
func TestStatusCode(t *testing.T) {
tests := []struct {
name string
manifest notifydata.ManifestWithError
expected string
}{
{
name: "fatal error",
manifest: notifydata.ManifestWithError{
Error: "fatal error",
},
expected: notifydata.StatusCodeFatal,
},
{
name: "incomplete snapshot",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
IncompleteReason: "incomplete",
},
},
expected: notifydata.StatusCodeIncomplete,
},
{
name: "fatal error in dir summary",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
FatalErrorCount: 1,
},
},
},
},
expected: notifydata.StatusCodeFatal,
},
{
name: "ignored error in dir summary",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
IgnoredErrorCount: 1,
},
},
},
},
expected: notifydata.StatusCodeError,
},
{
name: "success",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{},
},
expected: notifydata.StatusCodeSuccess,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.manifest.StatusCode())
})
}
}
func TestManifestWithErrorMethods(t *testing.T) {
startTime := clock.Now().Add(-1*time.Minute - 330*time.Millisecond)
endTime := clock.Now()
dirSummary := &fs.DirectorySummary{
TotalFileSize: 1000,
TotalFileCount: 10,
TotalDirCount: 5,
}
tests := []struct {
name string
manifest notifydata.ManifestWithError
expected struct {
startTimestamp time.Time
endTimestamp time.Time
totalSize int64
totalFiles int64
totalDirs int64
duration time.Duration
}
}{
{
name: "complete manifest",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
StartTime: fs.UTCTimestamp(startTime.UnixNano()),
EndTime: fs.UTCTimestamp(endTime.UnixNano()),
RootEntry: &snapshot.DirEntry{
DirSummary: dirSummary,
},
},
},
expected: struct {
startTimestamp time.Time
endTimestamp time.Time
totalSize int64
totalFiles int64
totalDirs int64
duration time.Duration
}{
startTimestamp: startTime.UTC().Truncate(time.Second),
endTimestamp: endTime.UTC().Truncate(time.Second),
totalSize: 1000,
totalFiles: 10,
totalDirs: 5,
duration: endTime.Sub(startTime).Truncate(100 * time.Millisecond),
},
},
{
name: "empty manifest",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{},
},
expected: struct {
startTimestamp time.Time
endTimestamp time.Time
totalSize int64
totalFiles int64
totalDirs int64
duration time.Duration
}{
startTimestamp: time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC),
endTimestamp: time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC),
totalSize: 0,
totalFiles: 0,
totalDirs: 0,
duration: 0,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected.startTimestamp, tt.manifest.StartTimestamp())
require.Equal(t, tt.expected.endTimestamp, tt.manifest.EndTimestamp())
require.Equal(t, tt.expected.totalSize, tt.manifest.TotalSize())
require.Equal(t, tt.expected.totalFiles, tt.manifest.TotalFiles())
require.Equal(t, tt.expected.totalDirs, tt.manifest.TotalDirs())
require.Equal(t, tt.expected.duration, tt.manifest.Duration())
})
}
}
func TestTotalSizeDelta(t *testing.T) {
tests := []struct {
name string
manifest notifydata.ManifestWithError
expected int64
}{
{
name: "no previous manifest",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileSize: 1000,
},
},
},
},
expected: 0,
},
{
name: "no root entry in current manifest",
manifest: notifydata.ManifestWithError{
Previous: &snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileSize: 1000,
},
},
},
},
expected: 0,
},
{
name: "no dir summary in current manifest",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
FileSize: 500,
},
},
Previous: &snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileSize: 1000,
},
},
},
},
expected: 500,
},
{
name: "dir summary in both manifests",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileSize: 1500,
},
},
},
Previous: &snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileSize: 1000,
},
},
},
},
expected: 500,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.manifest.TotalSizeDelta())
})
}
}
func TestTotalFilesDelta(t *testing.T) {
tests := []struct {
name string
manifest notifydata.ManifestWithError
expected int64
}{
{
name: "no previous manifest",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileCount: 10,
},
},
},
},
expected: 0,
},
{
name: "no root entry in current manifest",
manifest: notifydata.ManifestWithError{
Previous: &snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileCount: 10,
},
},
},
},
expected: 0,
},
{
name: "no dir summary in current manifest",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{},
},
Previous: &snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileCount: 10,
},
},
},
},
expected: 1,
},
{
name: "dir summary in both manifests",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileCount: 15,
},
},
},
Previous: &snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileCount: 10,
},
},
},
},
expected: 5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.manifest.TotalFilesDelta())
})
}
}
func TestTotalDirsDelta(t *testing.T) {
tests := []struct {
name string
manifest notifydata.ManifestWithError
expected int64
}{
{
name: "no previous manifest",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalDirCount: 5,
},
},
},
},
expected: 0,
},
{
name: "no root entry in current manifest",
manifest: notifydata.ManifestWithError{
Previous: &snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalDirCount: 5,
},
},
},
},
expected: 0,
},
{
name: "no dir summary in current manifest",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{},
},
Previous: &snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalDirCount: 5,
},
},
},
},
expected: 0,
},
{
name: "dir summary in both manifests",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalDirCount: 10,
},
},
},
Previous: &snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalDirCount: 5,
},
},
},
},
expected: 5,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.manifest.TotalDirsDelta())
})
}
}
func TestTotalFiles(t *testing.T) {
tests := []struct {
name string
manifest notifydata.ManifestWithError
expected int64
}{
{
name: "no root entry",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{},
},
expected: 0,
},
{
name: "root entry with dir summary",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileCount: 10,
},
},
},
},
expected: 10,
},
{
name: "root entry without dir summary",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{},
},
},
expected: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.manifest.TotalFiles())
})
}
}
func TestTotalDirs(t *testing.T) {
tests := []struct {
name string
manifest notifydata.ManifestWithError
expected int64
}{
{
name: "no root entry",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{},
},
expected: 0,
},
{
name: "root entry with dir summary",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalDirCount: 5,
},
},
},
},
expected: 5,
},
{
name: "root entry without dir summary",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{},
},
},
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.manifest.TotalDirs())
})
}
}
func TestTotalSize(t *testing.T) {
tests := []struct {
name string
manifest notifydata.ManifestWithError
expected int64
}{
{
name: "no root entry",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{},
},
expected: 0,
},
{
name: "root entry with dir summary",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
DirSummary: &fs.DirectorySummary{
TotalFileSize: 1000,
},
},
},
},
expected: 1000,
},
{
name: "root entry without dir summary",
manifest: notifydata.ManifestWithError{
Manifest: snapshot.Manifest{
RootEntry: &snapshot.DirEntry{
FileSize: 500,
},
},
},
expected: 500,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.manifest.TotalSize())
})
}
}

View File

@@ -7,6 +7,7 @@
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/fs"
@@ -187,7 +188,9 @@ func verifyTemplate(t *testing.T, embeddedTemplateName, expectedSuffix string, a
want := string(wantBytes)
require.Equal(t, want, buf.String())
assert.Equal(t, want, buf.String())
require.NoError(t, os.Remove(actualFileName))
if want == buf.String() {
require.NoError(t, os.Remove(actualFileName))
}
}

View File

@@ -1,4 +1,4 @@
Subject: Kopia created {{ len .EventArgs.Snapshots }} snapshot{{ if gt (len .EventArgs.Snapshots) 1 }}s{{end}} on {{.Hostname}}
Subject: Kopia {{.EventArgs.OverallStatus}} creating {{ if gt (len .EventArgs.Snapshots) 1 }}{{ len .EventArgs.Snapshots }} snapshots{{else}}snapshot{{end}} on {{.Hostname}}
<!doctype html>
<html>

View File

@@ -1,5 +1,4 @@
Subject: Kopia created {{ len .EventArgs.Snapshots }} snapshot{{ if gt (len .EventArgs.Snapshots) 1 }}s{{end}} on {{.Hostname}}
Subject: Kopia {{.EventArgs.OverallStatus}} creating {{ if gt (len .EventArgs.Snapshots) 1 }}{{ len .EventArgs.Snapshots }} snapshots{{else}}snapshot{{end}} on {{.Hostname}}
{{ range .EventArgs.Snapshots | sortSnapshotManifestsByName}}Path: {{ .Manifest.Source.Path }}
Status: {{ .StatusCode }}

View File

@@ -1,4 +1,4 @@
Subject: Kopia created 4 snapshots on some-host
Subject: Kopia encountered a fatal error creating 4 snapshots on some-host
<!doctype html>
<html>
@@ -90,7 +90,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td><span class="path">/some/path</span></td>
<td>Wed, 01 Jan 2020 19:04:05 PST</td>
<td>1.1s</td>
@@ -104,7 +104,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td colspan="6">
<b style="color:red">Failed Entries:</b>
<ul>
@@ -121,7 +121,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td><span class="path">/some/path</span></td>
<td>Wed, 01 Jan 2020 19:04:05 PST</td>
<td>1.1s</td>
@@ -135,7 +135,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td colspan="6">
<b style="color:red">Failed Entries:</b>
<ul>
@@ -152,7 +152,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td><span class="path">/some/path2</span></td>
<td>Wed, 01 Jan 2020 19:04:05 PST</td>
<td>1.1s</td>
@@ -166,7 +166,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td colspan="6">
<b style="color:red">Failed Entries:</b>
<ul>

View File

@@ -1,4 +1,4 @@
Subject: Kopia created 4 snapshots on some-host
Subject: Kopia encountered a fatal error creating 4 snapshots on some-host
<!doctype html>
<html>
@@ -90,7 +90,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td><span class="path">/some/path</span></td>
<td>Thu, 02 Jan 2020 03:04:05 +0000</td>
<td>1.1s</td>
@@ -104,7 +104,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td colspan="6">
<b style="color:red">Failed Entries:</b>
<ul>
@@ -121,7 +121,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td><span class="path">/some/path</span></td>
<td>Thu, 02 Jan 2020 03:04:05 +0000</td>
<td>1.1s</td>
@@ -135,7 +135,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td colspan="6">
<b style="color:red">Failed Entries:</b>
<ul>
@@ -152,7 +152,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td><span class="path">/some/path2</span></td>
<td>Thu, 02 Jan 2020 03:04:05 +0000</td>
<td>1.1s</td>
@@ -166,7 +166,7 @@ Subject: Kopia created 4 snapshots on some-host
<tr class="snapshotstatus-ok">
<tr class="snapshotstatus-success">
<td colspan="6">
<b style="color:red">Failed Entries:</b>
<ul>

View File

@@ -1,5 +1,4 @@
Subject: Kopia created 4 snapshots on some-host
Subject: Kopia encountered a fatal error creating 4 snapshots on some-host
Path: /some/other/path
Status: fatal
@@ -12,7 +11,7 @@ Path: /some/other/path
Path: /some/path
Status: ok
Status: success
Start: Wed, 01 Jan 2020 19:04:05 PST
Duration: 1.1s
Size: 456 B (+56 B)
@@ -26,7 +25,7 @@ Path: /some/path
Path: /some/path
Status: ok
Status: success
Start: Wed, 01 Jan 2020 19:04:05 PST
Duration: 1.1s
Size: 456 B (-44 B)
@@ -40,7 +39,7 @@ Path: /some/path
Path: /some/path2
Status: ok
Status: success
Start: Wed, 01 Jan 2020 19:04:05 PST
Duration: 1.1s
Size: 456 B

View File

@@ -1,5 +1,4 @@
Subject: Kopia created 4 snapshots on some-host
Subject: Kopia encountered a fatal error creating 4 snapshots on some-host
Path: /some/other/path
Status: fatal
@@ -12,7 +11,7 @@ Path: /some/other/path
Path: /some/path
Status: ok
Status: success
Start: Thu, 02 Jan 2020 03:04:05 +0000
Duration: 1.1s
Size: 456 B (+56 B)
@@ -26,7 +25,7 @@ Path: /some/path
Path: /some/path
Status: ok
Status: success
Start: Thu, 02 Jan 2020 03:04:05 +0000
Duration: 1.1s
Size: 456 B (-44 B)
@@ -40,7 +39,7 @@ Path: /some/path
Path: /some/path2
Status: ok
Status: success
Start: Thu, 02 Jan 2020 03:04:05 +0000
Duration: 1.1s
Size: 456 B

View File

@@ -19,7 +19,7 @@
// Message represents a notification message.
type Message struct {
Subject string `json:"subject"`
Headers map[string]string `json:"headers"`
Headers map[string]string `json:"headers,omitempty"`
Severity Severity `json:"severity"`
Body string `json:"body"`
}