Merge dev into main (v0.37.1)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 28s

This commit is contained in:
sandrodach committed 2026-11-01 13:20:01 +01:00
commit 17349a9573
10 files changed
+43 -6

No files matched your search

+7 -1
View File
@@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
## [Unreleased] ## [Unreleased]
## [0.37.1] - 2026-11-01
### Fixed
- Beim Herunterfahren wartet der Server, bis eine laufende Verdichtung des Notiz-Protokolls fertig ist, bevor er die Datenbank schließt. Dieselbe Lücke ließ einen Test auf dem Build-Server gelegentlich scheitern.
## [0.37.0] - 2026-11-01 ## [0.37.0] - 2026-11-01
### Added ### Added
@@ -617,7 +622,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
- Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken. - Geteilte Listen (Einkaufs-, Pack- und Checklisten) mit `#Kategorie`-Gruppierung, Mengenangaben und Echtzeit-Abhaken.
- Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung. - Benutzer-, Gruppen- und Admin-Verwaltung (Ersteinrichtungs-Assistent, Einladungscodes, Profilbilder, persönliche API-Keys & Webhook-Ingest `/api/v1/webhooks/ingest`, JSON-Datenexport) sowie CSS-Theme-Engine mit 4 Themes (`Dark`, `Light`, `High Contrast`, `Soft`) und PWA-Unterstützung.
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.37.0...dev [Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.37.1...dev
[0.37.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.37.0...v0.37.1
[0.37.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.36.0...v0.37.0 [0.37.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.36.0...v0.37.0
[0.36.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.35.0...v0.36.0 [0.36.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.35.0...v0.36.0
[0.35.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.34.0...v0.35.0 [0.35.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.34.0...v0.35.0
+1 -1
View File
@@ -6,7 +6,7 @@
[![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#) [![Self-Hosted](https://img.shields.io/badge/100%25-Self--Hosted-2563eb?style=flat-square)](#)
[![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart) [![Docker](https://img.shields.io/badge/Deployment-Single%20Container-2496ed?style=flat-square&logo=docker&logoColor=white)](#-schnellstart)
[![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir) [![SQLite](https://img.shields.io/badge/Daten-SQLite-003b57?style=flat-square&logo=sqlite&logoColor=white)](#-deine-daten-gehören-dir)
[![Version](https://img.shields.io/badge/Version-v0.37.0-16a34a?style=flat-square)](CHANGELOG.md) [![Version](https://img.shields.io/badge/Version-v0.37.1-16a34a?style=flat-square)](CHANGELOG.md)
**MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server. **MindSift** ist eine selbst gehostete Schaltstelle für spontane Gedanken, Aufgaben mit Kanban-Board, hierarchische Outliner-Notizen, die mehrere Personen gleichzeitig bearbeiten können, und gemeinsame Listen. Alles läuft als ein einzelner Container mit eingebetteter SQLite-Datenbank, auf deinem eigenen Server.
+2
View File
@@ -103,4 +103,6 @@ func main() {
if err := httpServer.Shutdown(ctx); err != nil { if err := httpServer.Shutdown(ctx); err != nil {
slog.Warn("HTTP server shutdown finished with error", "error", err) slog.Warn("HTTP server shutdown finished with error", "error", err)
} }
// Let a running compaction finish before the deferred store.Close
srv.Close()
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.37.0", "version": "0.37.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"version": "0.37.0", "version": "0.37.1",
"license": "Unlicense", "license": "Unlicense",
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "5.3.0", "@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mindsift-frontend", "name": "mindsift-frontend",
"private": true, "private": true,
"version": "0.37.0", "version": "0.37.1",
"license": "Unlicense", "license": "Unlicense",
"type": "module", "type": "module",
"scripts": { "scripts": {
+1
View File
@@ -26,6 +26,7 @@ func TestDemoResetKeepsSessionsAndRestoresSampleData(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
srv := NewServer(&config.Config{DatabasePath: dbPath, JWTSecret: "demo-secret", DemoMode: true}, store, ws.NewHub(), nil) srv := NewServer(&config.Config{DatabasePath: dbPath, JWTSecret: "demo-secret", DemoMode: true}, store, ws.NewHub(), nil)
t.Cleanup(srv.Close)
router := srv.Router() router := srv.Router()
call := func(method, path, token, body string) *httptest.ResponseRecorder { call := func(method, path, token, body string) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, path, strings.NewReader(body)) req := httptest.NewRequest(method, path, strings.NewReader(body))
+6 -1
View File
@@ -266,9 +266,14 @@ func (s *Server) handleDocLeave(w http.ResponseWriter, r *http.Request) {
const compactionInterval = 6 * time.Hour const compactionInterval = 6 * time.Hour
func (s *Server) startCompaction() { func (s *Server) startCompaction() {
defer s.bg.Done()
for { for {
s.compactOnce() s.compactOnce()
time.Sleep(compactionInterval) select {
case <-s.stop:
return
case <-time.After(compactionInterval):
}
} }
} }
+1
View File
@@ -24,6 +24,7 @@ func TestDemoSendsNothingAndKeepsAccounts(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
srv := NewServer(&config.Config{DatabasePath: dbPath, JWTSecret: "demo-secret", DemoMode: true}, store, ws.NewHub(), nil) srv := NewServer(&config.Config{DatabasePath: dbPath, JWTSecret: "demo-secret", DemoMode: true}, store, ws.NewHub(), nil)
t.Cleanup(srv.Close)
router := srv.Router() router := srv.Router()
token := sessionLogin(t, router, "admin", "admin", "visitor-1") token := sessionLogin(t, router, "admin", "admin", "visitor-1")
_ = sessionLogin(t, router, "admin", "admin", "visitor-2") _ = sessionLogin(t, router, "admin", "admin", "visitor-2")
+15
View File
@@ -7,6 +7,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"strings" "strings"
"sync"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@@ -41,6 +42,11 @@ type Server struct {
notifyPushHook func(userID string, msg pushMessage) error notifyPushHook func(userID string, msg pushMessage) error
pushHTTPClient *http.Client // tests: reach a local push service pushHTTPClient *http.Client // tests: reach a local push service
push pushKeys push pushKeys
// Background work that touches the database right from the start (compaction); Close stops it
stop chan struct{}
stopOnce sync.Once
bg sync.WaitGroup
} }
func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS fs.FS) *Server { func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS fs.FS) *Server {
@@ -51,6 +57,7 @@ func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS
staticFS: staticFS, staticFS: staticFS,
leases: collab.NewLeaseManager(nil), leases: collab.NewLeaseManager(nil),
notifier: collab.NewNotifier(), notifier: collab.NewNotifier(),
stop: make(chan struct{}),
} }
srv.loginGuard = newLoginGuard(!cfg.DisableLoginProtection) srv.loginGuard = newLoginGuard(!cfg.DisableLoginProtection)
srv.voice = newVoiceService(cfg.Speech.MaxParallel) srv.voice = newVoiceService(cfg.Speech.MaxParallel)
@@ -63,6 +70,7 @@ func NewServer(cfg *config.Config, store *storage.Store, wsHub *ws.Hub, staticFS
store.OnTaskCreated = func(th *domain.Thought) { store.OnTaskCreated = func(th *domain.Thought) {
srv.wsHub.BroadcastToUsers(srv.taskAudience(th), "TASK_CREATED", th) srv.wsHub.BroadcastToUsers(srv.taskAudience(th), "TASK_CREATED", th)
} }
srv.bg.Add(1)
go srv.startCompaction() go srv.startCompaction()
if cfg.DemoMode && cfg.DemoResetMinutes > 0 { if cfg.DemoMode && cfg.DemoResetMinutes > 0 {
go srv.startDemoResetTicker() go srv.startDemoResetTicker()
@@ -93,6 +101,13 @@ func (s *Server) taskAudience(th *domain.Thought) []string {
return append(s.store.GetTaskListUserIDs(th.TaskListID), th.UserID) return append(s.store.GetTaskListUserIDs(th.TaskListID), th.UserID)
} }
// Close stops the background compaction and waits until a running pass has finished, so the database
// can be closed safely afterwards (tests close it right away; a pass still writing would race with that).
func (s *Server) Close() {
s.stopOnce.Do(func() { close(s.stop) })
s.bg.Wait()
}
func (s *Server) Router() http.Handler { func (s *Server) Router() http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
+7
View File
@@ -46,6 +46,7 @@ func setupTestServer(t *testing.T) (*Server, *storage.Store, string) {
} }
hub := ws.NewHub() hub := ws.NewHub()
srv := NewServer(cfg, store, hub, nil) srv := NewServer(cfg, store, hub, nil)
t.Cleanup(srv.Close)
token, err := auth.GenerateToken(cfg.JWTSecret, user.ID, user.Email, user.DisplayName, user.TokenVersion, testSession(t, store, user.ID)) token, err := auth.GenerateToken(cfg.JWTSecret, user.ID, user.Email, user.DisplayName, user.TokenVersion, testSession(t, store, user.ID))
if err != nil { if err != nil {
@@ -71,6 +72,7 @@ func TestFirstBootSetupAndInviteRegistrationAndNonNullSlices(t *testing.T) {
AllowRegistration: false, AllowRegistration: false,
} }
srv := NewServer(cfg, store, ws.NewHub(), nil) srv := NewServer(cfg, store, ws.NewHub(), nil)
t.Cleanup(srv.Close)
router := srv.Router() router := srv.Router()
// 1. Fresh DB must report needsSetup = true // 1. Fresh DB must report needsSetup = true
@@ -455,6 +457,7 @@ func TestLoginWithUsernameAndDemoMode(t *testing.T) {
DemoMode: true, DemoMode: true,
} }
srv := NewServer(cfg, store, ws.NewHub(), nil) srv := NewServer(cfg, store, ws.NewHub(), nil)
t.Cleanup(srv.Close)
router := srv.Router() router := srv.Router()
// 1. Check auth status reports demoMode: true // 1. Check auth status reports demoMode: true
@@ -542,6 +545,7 @@ func TestSpaceMembersDirectInvitationAPI(t *testing.T) {
JWTSecret: "test-secret", JWTSecret: "test-secret",
} }
srv := NewServer(cfg, store, ws.NewHub(), nil) srv := NewServer(cfg, store, ws.NewHub(), nil)
t.Cleanup(srv.Close)
router := srv.Router() router := srv.Router()
adminUser, err := store.GetUserByLogin("admin") adminUser, err := store.GetUserByLogin("admin")
@@ -771,6 +775,7 @@ func TestWebhookTriageBadgeAndIngestion(t *testing.T) {
AllowRegistration: false, AllowRegistration: false,
} }
srv := NewServer(cfg, store, ws.NewHub(), nil) srv := NewServer(cfg, store, ws.NewHub(), nil)
t.Cleanup(srv.Close)
router := srv.Router() router := srv.Router()
// 1. Setup user // 1. Setup user
@@ -864,6 +869,7 @@ func TestSSEGlobalEndpoint(t *testing.T) {
AllowRegistration: false, AllowRegistration: false,
} }
srv := NewServer(cfg, store, hub, nil) srv := NewServer(cfg, store, hub, nil)
t.Cleanup(srv.Close)
router := srv.Router() router := srv.Router()
setupBody := []byte(`{ setupBody := []byte(`{
@@ -1345,6 +1351,7 @@ func TestFirstBootSetupWithSampleData(t *testing.T) {
} }
defer store.Close() defer store.Close()
srv := NewServer(&config.Config{Port: "8080", DatabasePath: dbPath, JWTSecret: "test-jwt-secret-key"}, store, ws.NewHub(), nil) srv := NewServer(&config.Config{Port: "8080", DatabasePath: dbPath, JWTSecret: "test-jwt-secret-key"}, store, ws.NewHub(), nil)
t.Cleanup(srv.Close)
// Too short passwords are refused (FLAW-09) // Too short passwords are refused (FLAW-09)
short := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", strings.NewReader(`{"email":"me@example.com","password":"secret"}`)) short := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", strings.NewReader(`{"email":"me@example.com","password":"secret"}`))