feat(phase3): reminders, ICS feeds, templates and nightly backups
Reminders are absolute or relative ("10 minutes before"). A relative one
is resolved against the task in SQL, so moving a task moves its reminders
too. They are marked sent before delivery, not after: a reminder that
goes out unrecorded repeats every minute forever, which is worse than
one recorded that fails to send.
The calendar feed is a per-user token in the path, because Apple Calendar
and Google fetch with no credentials and no way to prompt for any. Tasks
go out as VTODO — the same component CalDAV will serve — and timed ones
also as VEVENT, since Google ignores VTODO entirely.
Templates store relative day offsets rather than dates, which is what
makes them reusable: an onboarding checklist is "day 1, day 3, day 14",
not three dates in March. Building a project from one is a single
transaction, so a failure cannot leave half a checklist.
Backups use SQLite's VACUUM INTO, not a file copy — a copy of a live
WAL database can miss recent writes or catch a page mid-update, and the
result looks like a database right up until it is needed. Rotation keeps
fourteen by count, not by age: a container that was off for a month must
not come back and delete every backup it has.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4d142b2fa7
commit
7e4c730049
+17
-1
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/kristianwind/verdande/internal/config"
|
||||
"github.com/kristianwind/verdande/internal/httpapi"
|
||||
"github.com/kristianwind/verdande/internal/jobs"
|
||||
"github.com/kristianwind/verdande/internal/store"
|
||||
)
|
||||
|
||||
@@ -53,9 +54,19 @@ func run() error {
|
||||
defer db.Close()
|
||||
log.Info("database ready", "path", db.Path())
|
||||
|
||||
api := httpapi.New(cfg, db, log, webAssets(log))
|
||||
|
||||
// Background work: reminders, the nightly backup, and the trash emptying
|
||||
// itself. Started before the listener so a reminder that came due while the
|
||||
// process was down goes out immediately rather than on the next tick.
|
||||
jobCtx, stopJobs := context.WithCancel(context.Background())
|
||||
defer stopJobs()
|
||||
runner := jobs.New(cfg, db, log, api.Mail(), api.Hub())
|
||||
runner.Start(jobCtx)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
Handler: httpapi.New(cfg, db, log, webAssets(log)),
|
||||
Handler: api,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
// No WriteTimeout: it would also cap the WebSocket and any long file
|
||||
// download. Per-handler timeouts do that job with the right granularity.
|
||||
@@ -89,6 +100,11 @@ func run() error {
|
||||
return fmt.Errorf("shutdown: %w", err)
|
||||
}
|
||||
|
||||
// Let the background jobs finish the pass they are in. A shutdown during a
|
||||
// backup would otherwise leave a partial file that looks like a backup.
|
||||
stopJobs()
|
||||
runner.Wait()
|
||||
|
||||
// WAL mode leaves recent writes in the -wal file. Checkpointing on the way out
|
||||
// means a backup taken of a stopped container is complete.
|
||||
if _, err := db.Exec(`PRAGMA wal_checkpoint(TRUNCATE)`); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user