From a3a709440f1f71a9eec04101619d026ce488e765 Mon Sep 17 00:00:00 2001 From: Sandro Date: Thu, 29 Oct 2026 07:13:29 +0100 Subject: [PATCH 1/3] =?UTF-8?q?fix(admin):=20SMTP=20port=20can=20be=20clea?= =?UTF-8?q?red=20and=20saves=20without=20'Ung=C3=BCltiges=20JSON'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The UI sent the port as a number while the server expects text, so every save failed; the field also jumped back to 587 when emptied. The port is now edited as text (digits only), and the server accepts number or text. Co-Authored-By: Claude Opus 5.5 --- .../features/settings/AdminSettingsView.tsx | 12 ++++--- frontend/src/lib/types.ts | 2 +- internal/domain/models.go | 31 +++++++++++++++++++ internal/domain/tags_test.go | 17 ++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/frontend/src/features/settings/AdminSettingsView.tsx b/frontend/src/features/settings/AdminSettingsView.tsx index 05fc8a4..772be45 100644 --- a/frontend/src/features/settings/AdminSettingsView.tsx +++ b/frontend/src/features/settings/AdminSettingsView.tsx @@ -96,7 +96,7 @@ export const AdminSettingsView: React.FC = ({ allowRegistration: false, instanceName: 'MindSift', smtpHost: '', - smtpPort: 587, + smtpPort: '587', smtpUser: '', smtpPass: '', smtpFrom: '', @@ -126,7 +126,7 @@ export const AdminSettingsView: React.FC = ({ allowRegistration: Boolean(sys.allowRegistration), instanceName: sys.instanceName || 'MindSift', smtpHost: sys.smtpHost || '', - smtpPort: sys.smtpPort || 587, + smtpPort: sys.smtpPort ? String(sys.smtpPort) : '587', smtpUser: sys.smtpUser || '', smtpPass: sys.smtpPass || '', smtpFrom: sys.smtpFrom || '', @@ -1150,10 +1150,12 @@ export const AdminSettingsView: React.FC = ({ SMTP Port (`SMTP_PORT`) - setSettings((prev) => ({ ...prev, smtpPort: Number(e.target.value) })) + // Digits only; an empty field stays empty while typing (saved as 587) + setSettings((prev) => ({ ...prev, smtpPort: e.target.value.replace(/\D/g, '').slice(0, 5) })) } placeholder="587" className="w-full h-9 px-3 input-field font-mono" diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index fe48e41..488d8e4 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -161,7 +161,7 @@ export interface SystemSettings { allowRegistration: boolean; instanceName: string; smtpHost?: string; - smtpPort?: number; + smtpPort?: string; // sent as text, like the server stores it smtpUser?: string; smtpPass?: string; smtpFrom?: string; diff --git a/internal/domain/models.go b/internal/domain/models.go index 0b21eb2..307a311 100644 --- a/internal/domain/models.go +++ b/internal/domain/models.go @@ -3,6 +3,7 @@ package domain import ( "crypto/rand" "encoding/hex" + "encoding/json" "fmt" "regexp" "strings" @@ -289,6 +290,36 @@ type SystemSettings struct { SMTPConfiguredViaEnv bool `json:"smtpConfiguredViaEnv,omitempty"` } +// UnmarshalJSON accepts smtpPort as text or number: older clients sent a number, which made the whole +// settings request fail with "Ungültiges JSON". +func (s *SystemSettings) UnmarshalJSON(data []byte) error { + type plain SystemSettings + aux := struct { + *plain + SMTPPort json.RawMessage `json:"smtpPort"` + }{plain: (*plain)(s)} + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + port := strings.TrimSpace(string(aux.SMTPPort)) + switch { + case port == "" || port == "null": + case strings.HasPrefix(port, `"`): + var str string + if err := json.Unmarshal(aux.SMTPPort, &str); err != nil { + return err + } + s.SMTPPort = strings.TrimSpace(str) + default: + var n json.Number + if err := json.Unmarshal(aux.SMTPPort, &n); err != nil { + return err + } + s.SMTPPort = n.String() + } + return nil +} + type DocumentBlock struct { ID string `json:"id"` DocumentID string `json:"documentId,omitempty"` diff --git a/internal/domain/tags_test.go b/internal/domain/tags_test.go index 37ab2f2..4be0d9d 100644 --- a/internal/domain/tags_test.go +++ b/internal/domain/tags_test.go @@ -1,6 +1,7 @@ package domain import ( + "encoding/json" "reflect" "testing" ) @@ -32,3 +33,19 @@ func TestStaleAnchorTags(t *testing.T) { t.Fatalf("only tags that exist solely as anchors are stale, got %v", got) } } + +func TestSystemSettingsAcceptsPortAsNumberOrText(t *testing.T) { + for _, body := range []string{`{"smtpPort": 465, "smtpHost": "mail"}`, `{"smtpPort": "465", "smtpHost": "mail"}`} { + var s SystemSettings + if err := json.Unmarshal([]byte(body), &s); err != nil { + t.Fatalf("%s: %v", body, err) + } + if s.SMTPPort != "465" || s.SMTPHost != "mail" { + t.Errorf("%s: got port %q host %q", body, s.SMTPPort, s.SMTPHost) + } + } + var empty SystemSettings + if err := json.Unmarshal([]byte(`{"smtpPort": ""}`), &empty); err != nil || empty.SMTPPort != "" { + t.Errorf("empty port: %q (%v)", empty.SMTPPort, err) + } +} From 9bc8716f417f430e28b9d725be86ef6bbe527941 Mon Sep 17 00:00:00 2001 From: Sandro Date: Thu, 29 Oct 2026 07:13:29 +0100 Subject: [PATCH 2/3] feat(pwa): blue splash screen with the logo, status bar follows the theme Manifest: short name, blue background/theme colour for the Android splash. index.html shows a plain blue screen with the logo until React mounts. theme-color starts blue and then follows the active theme background. Co-Authored-By: Claude Opus 5.5 --- Doku/ui-patterns.md | 7 +++++++ frontend/index.html | 11 +++++++++-- frontend/public/manifest.json | 8 ++++---- frontend/src/lib/theme.ts | 12 ++++++++++++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/Doku/ui-patterns.md b/Doku/ui-patterns.md index c40bea5..f89a889 100644 --- a/Doku/ui-patterns.md +++ b/Doku/ui-patterns.md @@ -61,3 +61,10 @@ Komponente: [CountBadge.tsx](../frontend/src/components/CountBadge.tsx), CSS-Kla - `inline`: im Textfluss. - Zahlen im mobilen Menü (`MobileActionContext`, `badge: number`) werden automatisch als `CountBadge` gezeigt; Text-Badges wie „2 aktiv“ bleiben Textmarken. - **Wichtig für neue Klassen:** Tailwind entfernt Klassen aus `@layer components`, die nicht wörtlich im Code stehen. Klassennamen deshalb nie zusammensetzen (`count-badge-${variant}`), sondern ausschreiben. + +## Ladebildschirm & Statusleiste + +- `manifest.json`: `name` „MindSift“ (kurz, der Slogan steht in `description`), `background_color` und `theme_color` `#2563eb` – Android baut daraus den Splash-Screen. +- `index.html` enthält in `#root` einen reinen HTML-Platzhalter (vollflächig Blau, Logo in der Mitte, kein Text). React ersetzt ihn beim ersten Rendern, dazwischen blitzt nichts Weißes auf. +- `meta theme-color` startet blau und wird von [`theme.ts`](../frontend/src/lib/theme.ts) nach dem Start und bei jedem Theme-Wechsel auf `--bg-app` des aktiven Themes gesetzt, damit die Statusleiste zum Header passt. + diff --git a/frontend/index.html b/frontend/index.html index 97f14bb..ce3aa67 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,7 +3,8 @@ - + + @@ -49,7 +50,13 @@ -
+ +
+
+ + +
+