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

This commit is contained in:
sandrodach committed 2026-10-31 10:16:47 +01:00
commit 378872d7a4
11 files changed
+110 -29

No files matched your search

+14 -1
View File
@@ -6,6 +6,18 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
## [Unreleased]
## [0.17.1] - 2026-10-31
### Changed
- Kanban maximieren: Nur noch das Board wird breiter, Listen-Reiter und „Neue Aufgabe anlegen“ behalten ihre Breite. Das Board wächst weich animiert, jede Spalte wird höchstens 380 px breit und das Board bleibt mittig (bei wenigen Spalten also schmaler als das Fenster).
- Ladebildschirm: Statt immer in Blau erscheint er in den Farben des zuletzt auf dem Gerät genutzten Themes (Hintergrund, Logo in der Akzentfarbe, Statusleiste passend); beim allerersten Besuch bleibt er blau. Neu dafür ist `boot-theme.js` (eigene Datei wegen der Content-Security-Policy); die App-Caches werden einmal erneuert.
- Journal: Vor dem Titel eines Tages steht statt des bunten Kalender-Emojis ein schlichtes Kalender-Symbol in der Textfarbe des Themes (nicht anklickbar, das Icon lässt sich im Journal ohnehin nicht ändern).
### Fixed
- App installieren am Handy: Meldet Chrome erst nach dem Öffnen der Anleitung, dass MindSift installierbar ist (z. B. kurz nach dem Entfernen der alten App), erscheint in der Anleitung jetzt ein Knopf „Jetzt installieren“ für die direkte Installation.
## [0.17.0] - 2026-10-31
### Added
@@ -395,7 +407,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.17.0...dev
[Unreleased]: https://git.dsit.me/sandrodach/MindSift/compare/v0.17.1...dev
[0.17.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.17.0...v0.17.1
[0.17.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.1...v0.17.0
[0.16.1]: https://git.dsit.me/sandrodach/MindSift/compare/v0.16.0...v0.16.1
[0.16.0]: https://git.dsit.me/sandrodach/MindSift/compare/v0.15.1...v0.16.0
+1 -1
View File
@@ -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.17.0-16a34a?style=flat-square)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/Version-v0.17.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.
+21 -5
View File
@@ -3,8 +3,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<!-- Blue while loading (matches the splash screen); the app switches it to the theme background -->
<!-- Black like the system bars of the installed app; boot-theme.js and then the app switch it to the
theme background -->
<meta name="theme-color" content="#000000" />
<script src="/boot-theme.js"></script>
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="MindSift" />
@@ -43,11 +45,25 @@
<script src="/pwa-install.js"></script>
</head>
<body class="bg-[#0b0f19] text-[#e3e3e3] antialiased selection:bg-[#8ab4f8]/30 selection:text-[#8ab4f8]">
<!-- Shown until React mounts and replaces it: full blue with the logo, no text, no white flash -->
<!-- Shown until React mounts and replaces it: the logo on the background of the theme used last on
this device (boot-theme.js), full blue on the first visit; no text, no white flash -->
<div id="root">
<div style="position:fixed;inset:0;background:#2563eb;display:flex;align-items:center;justify-content:center">
<!-- the icon's own blue square disappears on blue, so the visible brain is ~55% of this size -->
<img src="/icon.svg" width="128" height="128" alt="" style="display:block" />
<div style="position:fixed;inset:0;background:var(--boot-bg,#2563eb);display:flex;align-items:center;justify-content:center">
<!-- icon.svg inline, so its square takes the theme's accent (on blue it disappears in the background) -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="128" height="128" style="display:block">
<rect width="512" height="512" rx="108" style="fill:var(--boot-accent,#2563eb)" />
<g transform="translate(256, 256) scale(12.5) translate(-12, -12)" fill="none" style="stroke:var(--boot-on-accent,#ffffff)" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/>
<path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/>
<path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/>
<path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/>
<path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/>
<path d="M3.477 10.896a4 4 0 0 1 .585-.396"/>
<path d="M19.938 10.5a4 4 0 0 1 .585.396"/>
<path d="M6 18a4 4 0 0 1-1.967-.516"/>
<path d="M19.967 17.484A4 4 0 0 1 18 18"/>
</g>
</svg>
</div>
</div>
<script type="module" src="/src/main.tsx"></script>
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mindsift-frontend",
"version": "0.17.0",
"version": "0.17.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mindsift-frontend",
"version": "0.17.0",
"version": "0.17.1",
"license": "Unlicense",
"dependencies": {
"@fontsource-variable/inter": "5.3.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mindsift-frontend",
"private": true,
"version": "0.17.0",
"version": "0.17.1",
"license": "Unlicense",
"type": "module",
"scripts": {
+17
View File
@@ -0,0 +1,17 @@
// Runs before the first paint: the loading screen takes the colours of the theme used last on this
// device (stored by the app in lib/theme.ts), so a light or orange theme does not start in blue.
// A file instead of an inline script so the CSP can stay at script-src 'self'.
(function () {
try {
var colors = JSON.parse(localStorage.getItem('mindsift_boot_colors') || 'null');
if (!colors || typeof colors.bg !== 'string' || typeof colors.accent !== 'string') return;
var root = document.documentElement;
root.style.setProperty('--boot-bg', colors.bg);
root.style.setProperty('--boot-accent', colors.accent);
if (typeof colors.onAccent === 'string') root.style.setProperty('--boot-on-accent', colors.onAccent);
var meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute('content', colors.bg);
} catch (e) {
// no storage: the loading screen stays blue
}
})();
+3 -1
View File
@@ -7,7 +7,7 @@
// would hand it the old values), the cache only offline.
// - API requests are never touched; offline data comes from the app's own cache (lib/offline.ts).
// Raise VERSION when this file changes in a way that needs fresh caches.
const VERSION = 'v5';
const VERSION = 'v6';
const SHELL_CACHE = `mindsift-shell-${VERSION}`;
const ASSET_CACHE = `mindsift-assets-${VERSION}`;
const MAX_ASSETS = 80;
@@ -23,6 +23,8 @@ const SHELL_URLS = [
'/icon-512x512.png',
'/apple-touch-icon.png',
'/badge-96x96.png',
'/boot-theme.js',
'/pwa-install.js',
];
self.addEventListener('install', (event) => {
+13 -6
View File
@@ -164,8 +164,6 @@ export const App: React.FC = () => {
const [deck, setDeck] = useState<Thought[]>([]);
const [lists, setLists] = useState<SharedList[]>([]);
const [activeTab, setActiveTab] = useState<ActiveTab>('triage');
// Kanban asked for the full window width (reported by the tasks view)
const [tasksWide, setTasksWide] = useState(false);
const [previousTab, setPreviousTab] = useState<ActiveTab>('triage');
const [selectedDocId, setSelectedDocId] = useState<string | null>(null);
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
@@ -1170,9 +1168,7 @@ export const App: React.FC = () => {
{/* Main Container */}
{/* The workspace fills the window exactly and scrolls only inside the note, never the page */}
<main
className={`flex-1 w-full mx-auto px-3 sm:px-4 pt-3 pb-24 ${
activeTab === 'tasks' && tasksWide ? 'lg:max-w-none lg:px-6' : 'max-w-6xl'
} ${
className={`flex-1 max-w-6xl w-full mx-auto px-3 sm:px-4 pt-3 pb-24 ${
activeTab === 'workspace' ? 'min-h-0 flex flex-col sm:pb-0' : 'sm:pb-6'
}`}
>
@@ -1196,7 +1192,6 @@ export const App: React.FC = () => {
refreshTrigger={refreshRevision}
initialSelectedTaskId={selectedTaskId}
openTasksCount={stats.tasksOpenCount}
onWideChange={setTasksWide}
/>
)}
@@ -1741,6 +1736,18 @@ export const App: React.FC = () => {
</div>
) : (
<div className="space-y-3 pt-1">
{/* Chrome may report "installable" only after the guide opened (e.g. shortly after the
old app was removed): then the real install is one tap away again */}
{deferredPwaPrompt && (
<button
type="button"
onClick={handleInstallWebApp}
className="btn-primary w-full h-12 text-sm font-semibold rounded-xl flex items-center justify-center gap-2 shadow-md active:scale-98 transition-transform cursor-pointer"
>
<Download className="w-4 h-4" />
<span>Jetzt installieren</span>
</button>
)}
<div className="p-3.5 rounded-xl bg-canvas border border-border space-y-2.5 text-xs text-main">
<p className="font-semibold text-accent flex items-center gap-1.5">
<Smartphone className="w-4 h-4" />
+18 -8
View File
@@ -62,10 +62,25 @@ interface TasksViewProps {
refreshTrigger?: number;
initialSelectedTaskId?: string | null;
openTasksCount?: number; // all open tasks incl. shared lists (same as the nav bar counter)
onWideChange?: (wide: boolean) => void; // Kanban stretched to the full window width
}
const KANBAN_WIDE_KEY = 'mindsift:kanban_wide';
// Wide Kanban: a column grows to at most this width, the board stays centred
const KANBAN_COL_MAX = 380;
const KANBAN_GAP = 12; // gap-3
// Only the board leaves the page column (header and "new task" keep their width): its width follows
// the columns (at most KANBAN_COL_MAX each, at most the window) and a margin keeps it centred – a
// negative one when it is wider than the page column. Width and margin are animated.
const kanbanBoardStyle = (wide: boolean, columns: number): React.CSSProperties => {
if (!wide) return { '--kanban-w': '100%', '--kanban-ml': '0px' } as React.CSSProperties;
const w = `min(100vw - 4rem, ${columns * KANBAN_COL_MAX + (columns - 1) * KANBAN_GAP}px)`;
return {
'--kanban-cols': columns,
'--kanban-w': w,
'--kanban-ml': `calc((100% - ${w}) / 2)`,
} as React.CSSProperties;
};
type TasksSession = { listId: string; viewMode: 'list' | 'kanban' };
@@ -76,7 +91,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
refreshTrigger = 0,
initialSelectedTaskId,
openTasksCount,
onWideChange,
}) => {
// Open list and view survive switching to other tabs (like the workspace's open note)
const [viewMode, setViewMode] = useState<'list' | 'kanban'>(() =>
@@ -100,10 +114,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
}
};
const isKanbanWide = viewMode === 'kanban' && kanbanWide;
useEffect(() => {
onWideChange?.(isKanbanWide);
}, [isKanbanWide, onWideChange]);
useEffect(() => () => onWideChange?.(false), [onWideChange]);
const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open');
const [tasks, setTasks] = useState<Thought[]>([]);
const [taskLists, setTaskLists] = useState<TaskList[]>([]);
@@ -2214,10 +2224,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
/* DYNAMIC KANBAN BOARD */
<div className="space-y-2">
<div
className={`grid grid-cols-1 md:grid-cols-2 gap-3 pt-1 ${
className={`grid grid-cols-1 md:grid-cols-2 gap-3 pt-1 lg:w-[var(--kanban-w)] lg:ml-[var(--kanban-ml)] transition-[width,margin] duration-300 ease-out ${
isKanbanWide ? 'lg:grid-cols-[repeat(var(--kanban-cols),minmax(0,1fr))]' : 'lg:grid-cols-4'
}`}
style={isKanbanWide ? ({ '--kanban-cols': activeColumns.length } as React.CSSProperties) : undefined}
style={kanbanBoardStyle(isKanbanWide, activeColumns.length)}
>
{activeColumns.map((col, colIdx) => {
const colTasks = kanbanTasks.filter((t) => {
@@ -24,6 +24,7 @@ import {
Undo2,
Redo2,
MoreHorizontal,
CalendarDays,
} from 'lucide-react';
import { Document, DocumentBlock, User } from '../../lib/types';
import { api } from '../../lib/api';
@@ -1265,10 +1266,17 @@ export const DocumentEditor: React.FC<DocumentEditorProps> = ({
<button
type="button"
onClick={() => canEdit && !inJournal && setShowEmojiPicker((prev) => !prev)}
className="text-2xl sm:text-3xl hover:opacity-80 transition-opacity cursor-pointer select-none leading-none bg-transparent border-0 outline-none p-0 focus:outline-none focus:ring-0 shadow-none"
title="Icon ändern"
className={`text-2xl sm:text-3xl transition-opacity select-none leading-none bg-transparent border-0 outline-none p-0 focus:outline-none focus:ring-0 shadow-none ${
inJournal ? 'cursor-default' : 'hover:opacity-80 cursor-pointer'
}`}
title={inJournal ? undefined : 'Icon ändern'}
>
{doc?.icon || '📝'}
{/* Journal days: a quiet calendar in the theme's text colour instead of the colourful emoji */}
{inJournal ? (
<CalendarDays className="w-6 h-6 sm:w-7 sm:h-7 text-muted" strokeWidth={1.75} />
) : (
doc?.icon || '📝'
)}
</button>
{showEmojiPicker && canEdit && (
<EmojiPickerModal
+9 -1
View File
@@ -236,6 +236,7 @@ export function getAccentColor(): string | null {
export function setAccentColor(hex: string | null): void {
store(ACCENT_KEY, hex);
applyAccent(hex);
syncThemeColorMeta(); // keeps the loading screen's colours current
}
function hexToRgb(hex: string): [number, number, number] {
@@ -352,15 +353,22 @@ function applyToDocument(themeId: ThemeId, customCss: string | null): void {
// The status bar (theme-color) starts black (like the Android navigation bar of the installed app,
// which only takes the manifest colour); once the app runs it follows the theme's background.
// The colours are also kept for the next start: public/boot-theme.js paints the loading screen in them.
function syncThemeColorMeta(): void {
const meta = document.querySelector('meta[name="theme-color"]');
if (!meta || typeof requestAnimationFrame === 'undefined') return;
requestAnimationFrame(() => {
const bg = getComputedStyle(document.documentElement).getPropertyValue('--bg-app').trim();
const css = getComputedStyle(document.documentElement);
const bg = css.getPropertyValue('--bg-app').trim();
if (bg) meta.setAttribute('content', bg);
const accent = css.getPropertyValue('--accent-primary').trim();
const onAccent = css.getPropertyValue('--text-on-accent').trim();
if (bg && accent) store(BOOT_COLORS_KEY, JSON.stringify({ bg, accent, onAccent }));
});
}
const BOOT_COLORS_KEY = 'mindsift_boot_colors';
function store(key: string, value: string | null): void {
try {
if (value === null) localStorage.removeItem(key);