Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2210dc082a |
@@ -105,6 +105,7 @@ func run() error {
|
||||
runner.Push = api.PushToUser
|
||||
runner.SendBeacon = api.SendBeacon
|
||||
runner.DailyPlans = api.DailyPlans
|
||||
runner.NotifyOfUpdates = api.NotifyOfUpdates
|
||||
runner.Start(jobCtx)
|
||||
|
||||
srv := &http.Server{
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/kristianwind/verdande/internal/store"
|
||||
"github.com/kristianwind/verdande/internal/update"
|
||||
)
|
||||
|
||||
// Besked til administratorerne, når der er kommet en ny version.
|
||||
//
|
||||
// Tjekket fandtes i forvejen, og svaret stod ét sted: nederst på en
|
||||
// indstillingsside. Det er en oplysning, man kun får, hvis man i forvejen havde
|
||||
// mistanke om, at der var noget at se efter — og "der er kommet en rettelse af
|
||||
// noget, du er ramt af" er ikke noget, man gætter sig til at kigge efter.
|
||||
//
|
||||
// Kun til administratorer, af samme grund som versionsfeltet er det: en besked om,
|
||||
// at serveren er bagud, er en besked om en andens arbejde, hvis man ikke selv kan
|
||||
// gøre noget ved den. Og den fører hen, hvor knappen til at gøre det sidder.
|
||||
|
||||
const updateScope = "update"
|
||||
|
||||
// NotifyOfUpdates er timeslaget. Selve tjekket mellemlagrer sit svar i seks timer,
|
||||
// så en time mellem opslagene her koster ikke noget — og en instans, der har været
|
||||
// slukket, opdager det ved den første time, den er vågen.
|
||||
func (s *Server) NotifyOfUpdates(ctx context.Context) error {
|
||||
return s.notifyOfUpdate(ctx, s.updates.Status(ctx))
|
||||
}
|
||||
|
||||
// notifyOfUpdate tager status som argument frem for at hente den, så den kan
|
||||
// prøves uden at spørge GitHub om noget.
|
||||
func (s *Server) notifyOfUpdate(ctx context.Context, status update.Status) error {
|
||||
// Slået fra betyder slået fra. Et tjek, operatøren ikke har bedt om, må ikke
|
||||
// blive til en besked ad bagvejen.
|
||||
if status.Disabled || !status.Available || status.Latest == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
stored, err := s.db.InstanceSettings(ctx, updateScope)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Én besked pr. version, ikke én pr. dag. En påmindelse hver morgen om det
|
||||
// samme er den slags besked, folk slår fra — og så er den næste, der betyder
|
||||
// noget, også slået fra.
|
||||
if last, _ := stored["notified_version"].(string); last == status.Latest {
|
||||
return nil
|
||||
}
|
||||
|
||||
users, err := s.db.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
title := updateTitle(status.Latest)
|
||||
body := firstLineOf(status.Notes)
|
||||
for _, u := range users {
|
||||
if !u.IsAdmin {
|
||||
continue
|
||||
}
|
||||
s.notifyDirect(ctx, &store.Notification{
|
||||
UserID: u.ID, Kind: "update.available", Title: title, Body: body,
|
||||
})
|
||||
}
|
||||
|
||||
// Skrevet efter beskederne, og kun hvis de blev sendt. Skrevet først ville en
|
||||
// fejl midtvejs betyde, at halvdelen fik besked og resten aldrig gjorde.
|
||||
return s.db.SetInstanceSettings(ctx, updateScope, map[string]any{
|
||||
"notified_version": status.Latest,
|
||||
})
|
||||
}
|
||||
|
||||
// updateTitle er skrevet på dansk, fordi den også bliver til en push-besked, og
|
||||
// push har ingen ordbog at slå op i. Fladen skriver sin egen sætning ud fra
|
||||
// `kind` — se KINDS i Notifications.svelte.
|
||||
func updateTitle(version string) string {
|
||||
return "verdande " + version + " er klar"
|
||||
}
|
||||
|
||||
// firstLineOf er den første linje af udgivelsesnoterne, som beskedens krop.
|
||||
//
|
||||
// Hele teksten ville være en udgivelsesnote i en notifikation, og det er ikke det,
|
||||
// en notifikation er til: den skal sige, at der er noget, og hvor man læser det.
|
||||
func firstLineOf(notes string) string {
|
||||
for _, line := range strings.Split(notes, "\n") {
|
||||
if line = strings.TrimSpace(line); line != "" {
|
||||
return firstWords(line, 30)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kristianwind/verdande/internal/update"
|
||||
)
|
||||
|
||||
// En ny version siger til af sig selv. Uden det stod svaret nederst på en
|
||||
// indstillingsside, som man kun åbner, hvis man i forvejen havde mistanke om, at
|
||||
// der var noget at se efter.
|
||||
func TestANewVersionTellsTheAdministrator(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
status := update.Status{
|
||||
Current: "v0.44.0", Latest: "v0.45.0", Available: true,
|
||||
Notes: "Nyt:\n\n- noget godt", URL: "https://example.dk/v0.45.0",
|
||||
}
|
||||
if err := ts.api.notifyOfUpdate(t.Context(), status); err != nil {
|
||||
t.Fatalf("besked om ny version: %v", err)
|
||||
}
|
||||
|
||||
list, unread := notifications(t, ts)
|
||||
if len(list) != 1 || unread != 1 {
|
||||
t.Fatalf("beskeder = %d (%v ulæste), want 1", len(list), unread)
|
||||
}
|
||||
n := firstOf(t, list)
|
||||
if n["kind"] != "update.available" {
|
||||
t.Errorf("besked = %v", n)
|
||||
}
|
||||
// Titlen bærer versionen, fordi den også bliver til en push-besked, og push
|
||||
// har ingen ordbog at slå sætningen op i.
|
||||
if title, _ := n["title"].(string); title != "verdande v0.45.0 er klar" {
|
||||
t.Errorf("titel = %q", title)
|
||||
}
|
||||
|
||||
// Én besked pr. version, ikke én pr. time. En påmindelse hver time om det
|
||||
// samme er den slags besked, folk slår fra — og så er den næste, der betyder
|
||||
// noget, også slået fra.
|
||||
if err := ts.api.notifyOfUpdate(t.Context(), status); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if list, _ := notifications(t, ts); len(list) != 1 {
|
||||
t.Errorf("den samme version gav %d beskeder, want 1", len(list))
|
||||
}
|
||||
|
||||
// Og den næste version er en ny ting at få at vide.
|
||||
status.Latest = "v0.46.0"
|
||||
if err := ts.api.notifyOfUpdate(t.Context(), status); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if list, _ := notifications(t, ts); len(list) != 2 {
|
||||
t.Errorf("en ny version gav %d beskeder, want 2", len(list))
|
||||
}
|
||||
}
|
||||
|
||||
// Kun administratorer. En besked om, at serveren er bagud, er en besked om en
|
||||
// andens arbejde, hvis man ikke selv kan gøre noget ved den.
|
||||
func TestOnlyAdministratorsHearAboutANewVersion(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
other := ts.newUser(t, "sofie@example.dk", "Sofie")
|
||||
|
||||
err := ts.api.notifyOfUpdate(t.Context(), update.Status{
|
||||
Current: "v0.44.0", Latest: "v0.45.0", Available: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if list, _ := notifications(t, other); len(list) != 0 {
|
||||
t.Errorf("en almindelig bruger fik %d beskeder om en opdatering", len(list))
|
||||
}
|
||||
if list, _ := notifications(t, ts); len(list) != 1 {
|
||||
t.Errorf("administratoren fik %d beskeder, want 1", len(list))
|
||||
}
|
||||
}
|
||||
|
||||
// Et tjek, operatøren har slået fra, må ikke blive til en besked ad bagvejen.
|
||||
func TestNoUpdateNoticeWhenTheCheckIsOff(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
for _, status := range []update.Status{
|
||||
{Current: "v0.44.0", Disabled: true},
|
||||
{Current: "v0.44.0", Available: false},
|
||||
{Current: "v0.44.0", Available: true, Latest: ""},
|
||||
} {
|
||||
if err := ts.api.notifyOfUpdate(t.Context(), status); err != nil {
|
||||
t.Fatalf("%v: %v", status, err)
|
||||
}
|
||||
}
|
||||
if list, _ := notifications(t, ts); len(list) != 0 {
|
||||
t.Errorf("der blev sagt til om en opdatering, der ikke var: %d beskeder", len(list))
|
||||
}
|
||||
}
|
||||
|
||||
// Beskeden fører hen, hvor knappen sidder — ikke ud af programmet. Prøvet på det,
|
||||
// fladen læser: en besked uden note, opgave eller projekt skal stadig kunne
|
||||
// klikkes.
|
||||
func TestAnUpdateNoticeCarriesNoTarget(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
|
||||
err := ts.api.notifyOfUpdate(t.Context(), update.Status{
|
||||
Current: "v0.44.0", Latest: "v0.45.0", Available: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := firstOf(t, mustNotifications(t, ts))
|
||||
for _, key := range []string{"note_id", "task_id", "project_id"} {
|
||||
if v, ok := n[key]; ok && v != "" {
|
||||
t.Errorf("%s = %v, want ingenting", key, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustNotifications(t *testing.T, ts *testServer) []any {
|
||||
t.Helper()
|
||||
list, _ := notifications(t, ts)
|
||||
if len(list) == 0 {
|
||||
t.Fatal("ingen beskeder")
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -54,6 +54,9 @@ type Runner struct {
|
||||
// AI-nøglen ligger derovre, og jobbet skal ikke kende nogen af delene.
|
||||
DailyPlans func(ctx context.Context) error
|
||||
|
||||
// NotifyOfUpdates tells the administrators when a newer version is out.
|
||||
NotifyOfUpdates func(ctx context.Context) error
|
||||
|
||||
// SendBeacon reports this installation to the collector, if it is switched on
|
||||
// and a day has passed. Supplied by the HTTP layer for the same reason as the
|
||||
// syncs: that is where the instance settings and the version live.
|
||||
@@ -98,6 +101,9 @@ func (r *Runner) Start(ctx context.Context) {
|
||||
// form som ovenstående, og af samme grund. Timen er personens egen: klokken
|
||||
// syv er syv forskellige steder.
|
||||
r.every(ctx, "dailyplan", time.Hour, r.dailyPlans)
|
||||
// Nye versioner. Hver time er rigeligt: selve tjekket mellemlagrer sit svar i
|
||||
// seks timer, og beskeden sendes én gang pr. version.
|
||||
r.every(ctx, "updates", time.Hour, r.updateNotices)
|
||||
}
|
||||
|
||||
func (r *Runner) Wait() { r.wg.Wait() }
|
||||
@@ -132,6 +138,13 @@ func (r *Runner) every(ctx context.Context, name string, interval time.Duration,
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *Runner) updateNotices(ctx context.Context) error {
|
||||
if r.NotifyOfUpdates == nil {
|
||||
return nil
|
||||
}
|
||||
return r.NotifyOfUpdates(ctx)
|
||||
}
|
||||
|
||||
func (r *Runner) dailyPlans(ctx context.Context) error {
|
||||
if r.DailyPlans == nil {
|
||||
return nil
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
'note.shared': 'notif.noteShared',
|
||||
'note.mention': 'notif.mention',
|
||||
'daily.plan': 'notif.dailyPlan',
|
||||
'update.available': 'notif.update',
|
||||
comment: 'notif.comment'
|
||||
};
|
||||
|
||||
@@ -52,6 +53,9 @@
|
||||
function follow(n) {
|
||||
open = false;
|
||||
if (!n.read) app.markRead(n.id);
|
||||
// En ny version hører ingen steder hen i data — den fører hen, hvor knappen
|
||||
// til at gøre noget ved den sidder.
|
||||
if (n.kind === 'update.available') return goto('/indstillinger/notifikationer');
|
||||
if (n.note_id) goto(`/noter?note=${n.note_id}`);
|
||||
else if (n.task_id) app.openDetail(n.task_id);
|
||||
else if (n.project_id) goto(`/projekt/${n.project_id}`);
|
||||
|
||||
@@ -33,6 +33,7 @@ export const da = {
|
||||
'notif.noteShared': '{name} delte en note med dig',
|
||||
'notif.mention': '{name} nævnte dig i en note',
|
||||
'notif.dailyPlan': 'Dagens plan',
|
||||
'notif.update': 'Der er kommet en ny version',
|
||||
'notif.comment': '{name} kommenterede',
|
||||
'notif.justNow': 'lige nu',
|
||||
'notif.minutes': '{n} min. siden',
|
||||
|
||||
@@ -30,6 +30,7 @@ export const en = {
|
||||
'notif.noteShared': '{name} shared a note with you',
|
||||
'notif.mention': '{name} mentioned you in a note',
|
||||
'notif.dailyPlan': "Today's plan",
|
||||
'notif.update': 'A new version is out',
|
||||
'notif.comment': '{name} commented',
|
||||
'notif.justNow': 'just now',
|
||||
'notif.minutes': '{n} min ago',
|
||||
|
||||
Reference in New Issue
Block a user