From 83bba8d20b6ab77869984181e67daa84c3eae8ca Mon Sep 17 00:00:00 2001 From: Sandro Date: Mon, 2 Nov 2026 21:52:00 +0100 Subject: [PATCH 1/3] test(import): a hand-edited backup cannot raise the role or reach other accounts Co-Authored-By: Claude Opus 5.5 --- internal/storage/import_markdown_test.go | 50 ++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/internal/storage/import_markdown_test.go b/internal/storage/import_markdown_test.go index dd6ada6..faf51cd 100644 --- a/internal/storage/import_markdown_test.go +++ b/internal/storage/import_markdown_test.go @@ -125,6 +125,56 @@ func TestImportMindSiftJSONRoundTrip(t *testing.T) { } } +// A backup changed by hand cannot raise the importer's role or reach other accounts: the profile in it +// (role, id) is ignored, and notes and blocks get new IDs and no task links. +func TestImportIgnoresProfileAndForeignIDs(t *testing.T) { + store, anna, ben := newImportStore(t) + annaDoc, _ := store.CreateDocument(anna.ID, "", "Annas Notiz", "", "", []domain.DocumentBlock{{ID: "blk_anna", Type: "bullet", Content: "Original"}}, nil, false) + annaTask, _ := store.CreateThoughtWithDetails(anna.ID, "Annas Aufgabe", "normal", "manual", "", "", "todo", "") + + exp := domain.FullExport{ + ExportedAt: "2026-11-02T00:00:00Z", + Version: "manipuliert", + User: domain.User{ID: anna.ID, Email: anna.Email, Role: "admin", IsActive: true}, + Documents: []domain.Document{{ + ID: annaDoc.ID, + Title: "Übernommen?", + Blocks: []domain.DocumentBlock{ + {ID: "blk_anna", Type: "bullet", Content: "Überschrieben?"}, + {ID: "blk_x", Type: "todo", Content: "Verknüpft?", TaskID: annaTask.ID}, + }, + }}, + } + data, _ := json.Marshal(exp) + plan, err := PlanMindSiftJSONImport(data) + if err != nil { + t.Fatalf("plan: %v", err) + } + if _, err := store.ApplyImport(ben.ID, plan); err != nil { + t.Fatalf("apply: %v", err) + } + + if u, _ := store.GetUserByID(ben.ID); u.Role != "member" { + t.Errorf("import must not change the role, got %q", u.Role) + } + if u, _ := store.GetUserByID(anna.ID); u.Role != anna.Role { + t.Errorf("import must not touch the account named in the backup, role now %q", u.Role) + } + if d, err := store.GetDocumentByID(anna.ID, annaDoc.ID); err != nil || d.Title != "Annas Notiz" || d.Blocks[0].Content != "Original" { + t.Errorf("Anna's note must stay as it was: %+v %v", d, err) + } + docs, _ := store.GetDocuments(ben.ID) + if len(docs) != 1 || docs[0].ID == annaDoc.ID || docs[0].Title != "Übernommen?" { + t.Fatalf("the note should be Ben's own copy with a new ID: %+v", docs) + } + got, _ := store.GetDocumentByID(ben.ID, docs[0].ID) + for _, b := range got.Blocks { + if b.ID == "blk_anna" || b.ID == "blk_x" || b.TaskID != "" { + t.Errorf("imported blocks need new IDs and no task links: %+v", b) + } + } +} + func zipOf(t *testing.T, files map[string]string) []byte { t.Helper() var buf bytes.Buffer From b3767ab387a9071acf5612152711cb950be88ed4 Mon Sep 17 00:00:00 2001 From: Sandro Date: Mon, 2 Nov 2026 22:00:43 +0100 Subject: [PATCH 2/3] feat(ui): back and forward also step through task lists and lists The open task list and the open list are part of the place in the browser history; the app's own choice of a first list replaces the entry. Going back to a task list shows its tasks from memory right away. Co-Authored-By: Claude Opus 5.5 --- .../src/features/lists/SharedListsView.tsx | 22 +++++++++++++++++-- frontend/src/features/triage/TasksView.tsx | 17 +++++++++++++- frontend/src/lib/navHistory.ts | 8 +++++-- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/frontend/src/features/lists/SharedListsView.tsx b/frontend/src/features/lists/SharedListsView.tsx index 0bd02cb..77106ea 100644 --- a/frontend/src/features/lists/SharedListsView.tsx +++ b/frontend/src/features/lists/SharedListsView.tsx @@ -19,6 +19,7 @@ import { CountBadge } from '../../components/CountBadge'; import { ListSwitcher } from '../../components/ListSwitcher'; import { LivePill } from '../../components/LivePill'; import { isPhoneWidth } from '../../lib/breakpoints'; +import { reportNav, onNavStep, restoringNavPlace } from '../../lib/navHistory'; import { OriginBadge } from '../../components/OriginBadge'; import { VoiceInputButton } from '../../components/VoiceInputButton'; import { FormSheet } from '../../components/FormSheet'; @@ -92,8 +93,24 @@ export const SharedListsView: React.FC = ({ onRefreshGloba const { setGroups, setPrimaryAction } = useMobileActions(); const [lists, setLists] = useState([]); // The open list is remembered on this device (other tabs of the app, new tab, restart) - const [activeListId, setActiveListId] = useState( - () => initialListId || readViewMemory<{ listId: string }>('lists').listId || '' + const [activeListId, setActiveListId] = useState(() => { + // Coming back here by the browser's back/forward: the list of that step + const step = restoringNavPlace(); + return (step?.tab === 'lists' && step.shopList) || initialListId || readViewMemory<{ listId: string }>('lists').listId || ''; + }); + // Switching lists is a step of the browser history (lib/navHistory.ts) + const autoSelectedListRef = useRef(false); + useEffect(() => { + const replace = autoSelectedListRef.current; + autoSelectedListRef.current = false; + if (activeListId) reportNav({ shopList: activeListId }, { replace }); + }, [activeListId]); + useEffect( + () => + onNavStep((place) => { + if (place.tab === 'lists' && place.shopList) setActiveListId(place.shopList); + }), + [] ); useEffect(() => { if (activeListId) writeViewMemory('lists', { listId: activeListId }); @@ -167,6 +184,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba if (initialListId && deduplicated.some((l) => l.id === initialListId)) { setActiveListId(initialListId); } else if (deduplicated.length > 0 && (!activeListId || !deduplicated.some((l) => l.id === activeListId))) { + autoSelectedListRef.current = true; // chosen by the app, not a step of its own in the history setActiveListId(deduplicated[0].id); } } catch { diff --git a/frontend/src/features/triage/TasksView.tsx b/frontend/src/features/triage/TasksView.tsx index 2af2451..28b5710 100644 --- a/frontend/src/features/triage/TasksView.tsx +++ b/frontend/src/features/triage/TasksView.tsx @@ -42,6 +42,7 @@ import { AvatarStack, AvatarPerson } from '../../components/AvatarStack'; import { CountBadge } from '../../components/CountBadge'; import { ListSwitcher } from '../../components/ListSwitcher'; import { isPhoneWidth } from '../../lib/breakpoints'; +import { reportNav, onNavStep, restoringNavPlace } from '../../lib/navHistory'; import { LivePill } from '../../components/LivePill'; import { FormSheet } from '../../components/FormSheet'; import { EmojiIconButton } from '../../components/EmojiIconButton'; @@ -181,7 +182,11 @@ export const TasksView: React.FC = ({ const [tasks, setTasks] = useState([]); const [taskLists, setTaskLists] = useState([]); const [selectedListId, setSelectedListIdState] = useState( - () => remembered.listId || 'all' + () => { + // Coming back here by the browser's back/forward: the list of that step + const step = restoringNavPlace(); + return (step?.tab === 'tasks' && step.taskList) || remembered.listId || 'all'; + } ); const viewMode: ViewMode = viewModes[selectedListId] || 'list'; // Only Kanban is stored; the list view is the default @@ -365,6 +370,16 @@ export const TasksView: React.FC = ({ setSelectedListIdState(id); }, []); + // Switching task lists is a step of the browser history (lib/navHistory.ts) + useEffect(() => reportNav({ taskList: selectedListId }), [selectedListId]); + useEffect( + () => + onNavStep((place) => { + if (place.tab === 'tasks' && place.taskList) setSelectedListId(place.taskList); + }), + [setSelectedListId] + ); + const onRefreshStatsRef = useRef(onRefreshStats); onRefreshStatsRef.current = onRefreshStats; // The realtime handler is registered once; it must reload with the current list, not the first one diff --git a/frontend/src/lib/navHistory.ts b/frontend/src/lib/navHistory.ts index 73957d0..f058982 100644 --- a/frontend/src/lib/navHistory.ts +++ b/frontend/src/lib/navHistory.ts @@ -1,5 +1,5 @@ // In-app back and forward: where the user is (page, open note, workspace view, zoomed bullet, on phones -// list or note) goes into the browser history, so the browser's back button (mouse button, Android back) +// list or note, open task list and list) goes into the browser history, so the browser's back button (mouse button, Android back) // steps back inside the app instead of leaving it. Each part reports its own piece; changes from one // action (e.g. opening a note from a task switches page and note) become one entry. While the app follows // a back/forward step, reports change nothing: the step itself already is the entry. @@ -10,6 +10,8 @@ export interface NavPlace { view?: string | null; // workspace view: editor, todos, tags, trash, journal … zoom?: string | null; // bullet the open note is zoomed into detail?: boolean; // phones: the note (true) or the list of notes (false) + taskList?: string | null; // open task list on the tasks page ("all" for Alle Aufgaben) + shopList?: string | null; // open list on the lists page } const RESTORE_MS = 600; // the app needs a moment to follow a step (views mount, notes load) @@ -25,7 +27,9 @@ const same = (a: NavPlace, b: NavPlace) => (a.docId ?? null) === (b.docId ?? null) && (a.view ?? null) === (b.view ?? null) && (a.zoom ?? null) === (b.zoom ?? null) && - Boolean(a.detail) === Boolean(b.detail); + Boolean(a.detail) === Boolean(b.detail) && + (a.taskList ?? null) === (b.taskList ?? null) && + (a.shopList ?? null) === (b.shopList ?? null); function flush() { if (!pending) return; From d52195ab752e38399c10a2500b2f333670d2cd5b Mon Sep 17 00:00:00 2001 From: Sandro Date: Mon, 2 Nov 2026 22:00:43 +0100 Subject: [PATCH 3/3] chore: release v0.64.1 Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 +++++++- README.md | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd839c..e555e0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V ## [Unreleased] +## [0.64.1] - 2026-11-02 + +### Fixed +- Zurück-Taste: Auch der Wechsel der Aufgabenliste und der Einkaufsliste ist jetzt ein eigener Schritt; Zurück führt zur vorher offenen Liste. + ## [0.64.0] - 2026-11-02 ### Added @@ -917,7 +922,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. - 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.64.0...dev +[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.64.1...dev +[0.64.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.64.0...v0.64.1 [0.64.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.63.2...v0.64.0 [0.63.2]: https://git.dsit.me/sandrodach/MindSift/compare/v0.63.1...v0.63.2 [0.63.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.63.0...v0.63.1 diff --git a/README.md b/README.md index 6810669..e555b03 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![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) [![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.64.0-16a34a?style=flat-square)](CHANGELOG.md) +[![Version](https://img.shields.io/badge/Version-v0.64.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. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3908dda..1559f7c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "mindsift-frontend", - "version": "0.64.0", + "version": "0.64.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mindsift-frontend", - "version": "0.64.0", + "version": "0.64.1", "license": "AGPL-3.0-only", "dependencies": { "@fontsource-variable/inter": "5.3.0", diff --git a/frontend/package.json b/frontend/package.json index 74d1c47..0f5a6a0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "mindsift-frontend", "private": true, - "version": "0.64.0", + "version": "0.64.1", "license": "AGPL-3.0-only", "type": "module", "scripts": {