Provide a look into the chache (#12)
That's the current state of my view into the cache. Both, HTML, JSON and CSV formats are available. It's far from perfect, but better than nothing at all. Before I continue, I want to check if this is going into the right direction. Co-authored-by: Lysander Trischler <twtxt@lyse.isobeef.org> Reviewed-on: #12 Co-authored-by: lyse <lyse@noreply@mills.io> Co-committed-by: lyse <lyse@noreply@mills.io>master
parent
bc5e29d71b
commit
adea938480
@ -0,0 +1,70 @@ |
||||
{{define "content"}} |
||||
<article> |
||||
<div> |
||||
<hgroup> |
||||
<h2>{{ $.FeedsTitle }} ({{ $.Pager.Nums }})</h2> |
||||
</hgroup> |
||||
<form type="get"> |
||||
<label>Filter by case-insensitive URL substring: |
||||
<input type="text" name="q" value="{{ $.SearchQuery }}" /> |
||||
</label> |
||||
<input type="submit" value="Filter" /> |
||||
</form> |
||||
{{ template "feeds-pager" . }} |
||||
<table> |
||||
<thead> |
||||
<tr> |
||||
<th>Feed URL |
||||
{{ template "feeds-sort-links" (dict "Ctx" . "SortField" "url" "SortName" "feed URL") }} |
||||
</th> |
||||
<th>Successes |
||||
{{ template "feeds-sort-links" (dict "Ctx" . "SortField" "successes" "SortName" "number of successful fetch and parse attempts") }} |
||||
</th> |
||||
<th>Failures |
||||
{{ template "feeds-sort-links" (dict "Ctx" . "SortField" "failures" "SortName" "number of failed fetch and parse attempts") }} |
||||
</th> |
||||
<th>Discovered At |
||||
{{ template "feeds-sort-links" (dict "Ctx" . "SortField" "discovered" "SortName" "feed discovery timestamp") }} |
||||
</th> |
||||
<th>Last Scraped At |
||||
{{ template "feeds-sort-links" (dict "Ctx" . "SortField" "scraped" "SortName" "last scraped timestamp") }} |
||||
</th> |
||||
<th>Last Updated At |
||||
{{ template "feeds-sort-links" (dict "Ctx" . "SortField" "updated" "SortName" "last updated timestamp") }} |
||||
</th> |
||||
<th>Last Error |
||||
{{ template "feeds-sort-links" (dict "Ctx" . "SortField" "error" "SortName" "last error") }} |
||||
</th> |
||||
<th>Fetch Avg |
||||
{{ template "feeds-sort-links" (dict "Ctx" . "SortField" "fetchavg" "SortName" "fetch average") }} |
||||
</th> |
||||
<th>New Avg |
||||
{{ template "feeds-sort-links" (dict "Ctx" . "SortField" "newavg" "SortName" "new average") }} |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{ range $.Feeds }} |
||||
<tr> |
||||
<td><a href="{{ .URL }}" title="Visit {{ .URL }}">{{ .URL }}</a></td> |
||||
<td>{{ .Success }}</td> |
||||
<td>{{ .Failure }}</td> |
||||
<td>{{ if not .DiscoveredAt.IsZero }}{{ dateInZone "2006-01-02 15:04Z" .DiscoveredAt "UTC" }}{{ else }}unknown{{ end }}</td> |
||||
<td>{{ if not .LastScrapedAt.IsZero }}{{ dateInZone "2006-01-02 15:04Z" .LastScrapedAt "UTC" }}{{ else }}unknown{{ end }}</td> |
||||
<td>{{ .LastUpdated }}</td> |
||||
<td>{{ .LastError }}</td> |
||||
<td>{{ printf "%.2f" .FetchAvg }}</td> |
||||
<td>{{ printf "%.2f" .NewAvg }}</td> |
||||
</tr> |
||||
{{ end }} |
||||
<tbody> |
||||
</table> |
||||
{{ template "feeds-pager" . }} |
||||
</div> |
||||
<div></div> |
||||
</article> |
||||
<style> |
||||
.container { max-width: 100% } |
||||
th, td { padding: .2rem } |
||||
</style> |
||||
{{end}} |
@ -0,0 +1,201 @@ |
||||
package internal |
||||
|
||||
import ( |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestNonZeroRFC3339MillisUTC(t *testing.T) { |
||||
mustParseTime := func(ts string) time.Time { |
||||
timestamp, err := time.Parse(time.RFC3339Nano, ts) |
||||
require.NoError(t, err, "test setup error, cannot parse timestamp as RFC 3339 with nanoseconds") |
||||
return timestamp |
||||
} |
||||
|
||||
for _, tt := range []struct { |
||||
name string |
||||
timestamp time.Time |
||||
expected string |
||||
}{ |
||||
{ |
||||
name: "when zero timestamp, then return empty string", |
||||
timestamp: time.Time{}, |
||||
expected: "", |
||||
}, |
||||
{ |
||||
name: "when UTC timestamp, then return formatted timestamp", |
||||
timestamp: mustParseTime("2022-12-15T14:15:58.763999Z"), |
||||
expected: "2022-12-15T14:15:58.763Z", |
||||
}, |
||||
{ |
||||
name: "when UTC+0 timestamp, then return formatted timestamp", |
||||
timestamp: mustParseTime("2022-12-15T14:15:58.763999+00:00"), |
||||
expected: "2022-12-15T14:15:58.763Z", |
||||
}, |
||||
{ |
||||
name: "when UTC+1 timestamp, then return formatted timestamp in UTC", |
||||
timestamp: mustParseTime("2022-12-15T14:15:58.763999+01:00"), |
||||
expected: "2022-12-15T13:15:58.763Z", |
||||
}, |
||||
{ |
||||
name: "when UTC-1 timestamp in minutes granularity, then return formatted timestamp in UTC and milliseconds precision", |
||||
timestamp: mustParseTime("2022-12-15T14:15:00-01:00"), |
||||
expected: "2022-12-15T15:15:00.000Z", |
||||
}, |
||||
} { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
assert.Equal(t, tt.expected, NonZeroRFC3339MillisUTC(tt.timestamp)) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestFindIndex(t *testing.T) { |
||||
for _, tt := range []struct { |
||||
name string |
||||
elements []string |
||||
search string |
||||
expected int |
||||
}{ |
||||
{ |
||||
name: "nil slice does not contain empty string", |
||||
elements: nil, |
||||
search: "", |
||||
expected: -1, |
||||
}, |
||||
{ |
||||
name: "nil slice does not contain arbitrary string", |
||||
elements: []string{}, |
||||
search: "foo", |
||||
expected: -1, |
||||
}, |
||||
{ |
||||
name: "empty slice does not contain empty string", |
||||
elements: []string{}, |
||||
search: "", |
||||
expected: -1, |
||||
}, |
||||
{ |
||||
name: "empty slice does not contain arbitrary string", |
||||
elements: []string{}, |
||||
search: "foo", |
||||
expected: -1, |
||||
}, |
||||
{ |
||||
name: "single element slice does not contain empty string", |
||||
elements: []string{"bar"}, |
||||
search: "", |
||||
expected: -1, |
||||
}, |
||||
{ |
||||
name: "single element slice does not contain different string", |
||||
elements: []string{"bar"}, |
||||
search: "foo", |
||||
expected: -1, |
||||
}, |
||||
{ |
||||
name: "single element slice does contain same string", |
||||
elements: []string{"bar"}, |
||||
search: "bar", |
||||
expected: 0, |
||||
}, |
||||
{ |
||||
name: "multiple element slice does not contain different string", |
||||
elements: []string{"bar", "foo"}, |
||||
search: "eggs", |
||||
expected: -1, |
||||
}, |
||||
{ |
||||
name: "multiple element slice does contain search string at first index", |
||||
elements: []string{"bar", "foo"}, |
||||
search: "bar", |
||||
expected: 0, |
||||
}, |
||||
{ |
||||
name: "multiple element slice does contain search string at second index", |
||||
elements: []string{"bar", "foo"}, |
||||
search: "foo", |
||||
expected: 1, |
||||
}, |
||||
{ |
||||
name: "multiple element slice with duplicates does contain search string and find first index", |
||||
elements: []string{"bar", "foo", "eggs", "and", "spam", "with", "eggs"}, |
||||
search: "eggs", |
||||
expected: 2, |
||||
}, |
||||
} { |
||||
t.Run(tt.name, func(t *testing.T) { |
||||
assert.Equal(t, tt.expected, FindIndex(tt.elements, tt.search)) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestDeleteIndex(t *testing.T) { |
||||
for _, tt := range []struct { |
||||
name string |
||||
elements []string |
||||
index int |
||||
expected []string |
||||
}{ |
||||
{ |
||||
name: "removing from nil slice returns nil", |
||||
elements: nil, |
||||
index: 0, |
||||
expected: nil, |
||||
}, |
||||
{ |
||||
name: "removing from empty slice returns nil", |
||||
elements: []string{}, |
||||
index: 0, |
||||
expected: []string{}, |
||||
}, |
||||
{ |
||||
name: "removing first element from single element slice returns empty slice", |
||||
elements: []string{"foo"}, |
||||
index: 0, |
||||
expected: []string{}, |
||||
}, |
||||
{ |
||||
name: "removing second element from single element slice returns same slice", |
||||
elements: []string{"foo"}, |
||||
index: 1, |
||||
expected: []string{"foo"}, |
||||
}, |
||||
{ |
||||
name: "removing before first element from single element slice returns same slice", |
||||
elements: []string{"foo"}, |
||||
index: -1, |
||||
expected: []string{"foo"}, |
||||
}, |
||||
{ |
||||
name: "removing first element from multiple element slice returns second element", |
||||
elements: []string{"foo", "bar"}, |
||||
index: 0, |
||||
expected: []string{"bar"}, |
||||
}, |
||||
{ |
||||
name: "removing second element from multiple element slice returns first element", |
||||
elements: []string{"foo", "bar"}, |
||||
index: 1, |
||||
expected: []string{"foo"}, |
||||
}, |
||||
{ |
||||
name: "removing third element from multiple element slice returns same slice", |
||||
elements: []string{"foo", "bar"}, |
||||
index: 2, |
||||
expected: []string{"foo", "bar"}, |
||||
}, |
||||
{ |
||||
name: "removing before first element from multiple element slice returns same slice", |
||||
elements: []string{"foo", "bar"}, |
||||
index: -1, |
||||
expected: []string{"foo", "bar"}, |
||||
}, |
||||
} { |
||||