From b56bfdeb529e8bd5e6976914a25aec7da81367ce Mon Sep 17 00:00:00 2001 From: mARTin Date: Tue, 10 Mar 2026 21:51:22 +0100 Subject: [PATCH] fix: tab persistence, dissolve tabs, and Synology CalDAV discovery fallback - Fix someday tabs disappearing on reload (include tab field in fetch mapping) - Add dissolve/delete tab feature with X button on hover (all 5 languages) - Fix Synology calendar "cannot find homeUrl" by falling back to derived homeUrl when well-known CalDAV discovery fails v1.25.2 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/app/globals.css | 61 +++++++++++++++++++++ src/components/WeeklyView.tsx | 100 +++++++++++++++++++++++++--------- src/lib/synology-calendar.ts | 40 ++++++++++++-- 4 files changed, 170 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 0d138e6..c12ee99 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.25.1", + "version": "1.25.2", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/app/globals.css b/src/app/globals.css index e12f3d1..0e6e02a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1548,6 +1548,67 @@ h3 { color: #fff; } +/* Someday Tab Wrappers with dissolve buttons */ +.someday-tab-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.someday-tab-dissolve { + position: absolute; + right: -2px; + top: -2px; + background: var(--weekly-bg, #fff); + border: 1px solid var(--weekly-border, #e5e7eb); + border-radius: 50%; + cursor: pointer; + color: var(--weekly-text-light, #999); + padding: 1px; + line-height: 0; + opacity: 0; + transition: opacity 0.15s; +} + +.someday-tab-wrapper:hover .someday-tab-dissolve { + opacity: 1; +} + +.someday-tab-dissolve:hover { + color: #ef4444; + border-color: #ef4444; +} + +.someday-tab-wrapper-h { + position: relative; + display: inline-flex; + align-items: center; +} + +.someday-tab-dissolve-h { + position: absolute; + right: -4px; + top: -4px; + background: var(--weekly-bg, #fff); + border: 1px solid var(--weekly-border, #e5e7eb); + border-radius: 50%; + cursor: pointer; + color: var(--weekly-text-light, #999); + padding: 1px; + line-height: 0; + opacity: 0; + transition: opacity 0.15s; +} + +.someday-tab-wrapper-h:hover .someday-tab-dissolve-h { + opacity: 1; +} + +.someday-tab-dissolve-h:hover { + color: #ef4444; + border-color: #ef4444; +} + /* Someday Tab Select (in list headers) */ .someday-tab-select { appearance: none; diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 793d090..f90ce1c 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -261,6 +261,7 @@ const translations: Record = { newTabName: "New tab name:", assignTab: "Assign to tab", renameTab: "Double-click to rename", + dissolveTab: "Remove tab (keep lists)", loading: "Loading your tasks...", sycing: "Syncing...", synced: "Synced", @@ -448,6 +449,7 @@ const translations: Record = { newTabName: "Neuer Tab-Name:", assignTab: "Tab zuweisen", renameTab: "Doppelklick zum Umbenennen", + dissolveTab: "Tab entfernen (Listen behalten)", loading: "Lade Aufgaben...", syncing: "Synchronisiere...", synced: "Synchronisiert", @@ -634,6 +636,7 @@ const translations: Record = { newTabName: "Nom du nouvel onglet :", assignTab: "Assigner à un onglet", renameTab: "Double-cliquez pour renommer", + dissolveTab: "Supprimer l'onglet (garder les listes)", loading: "Chargement de vos tâches…", sycing: "Synchronisation…", synced: "Synchronisé", @@ -820,6 +823,7 @@ const translations: Record = { newTabName: "Nombre de nueva pestaña:", assignTab: "Asignar a pestaña", renameTab: "Doble clic para renombrar", + dissolveTab: "Eliminar pestaña (mantener listas)", loading: "Cargando tus tareas…", sycing: "Sincronizando…", synced: "Sincronizado", @@ -1006,6 +1010,7 @@ const translations: Record = { newTabName: "Nome nuova scheda:", assignTab: "Assegna a scheda", renameTab: "Doppio clic per rinominare", + dissolveTab: "Rimuovi scheda (mantieni liste)", loading: "Caricamento delle attività…", sycing: "Sincronizzazione…", synced: "Sincronizzato", @@ -1481,6 +1486,23 @@ export default function WeeklyView() { } }; + const dissolveTab = async (tabName: string) => { + const listsToUpdate = somedayLists.filter(l => l.tab === tabName); + setSomedayLists(prev => prev.map(l => l.tab === tabName ? { ...l, tab: null } : l)); + if (activeSomedayTab === tabName) setSomedayTab(null); + for (const list of listsToUpdate) { + try { + await fetch("/api/someday-lists", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: list.id, tab: null }), + }); + } catch (e) { + console.error("Failed to dissolve tab for list:", e); + } + } + }; + const filteredSomedayLists = useMemo(() => { if (activeSomedayTab === null) return somedayLists; return somedayLists.filter(l => (l.tab || null) === activeSomedayTab); @@ -2613,7 +2635,8 @@ export default function WeeklyView() { data.lists.map((l: any) => ({ id: l.id, title: l.title, - tasks: l.tasks || [], // Tasks will be overwritten/populated by fetchTasks + tab: l.tab || null, + tasks: l.tasks || [], externalId: l.externalId || null, externalProvider: l.externalProvider || null, })), @@ -6386,18 +6409,29 @@ export default function WeeklyView() { }} /> ) : ( - +
+ + +
) ))} @@ -6473,15 +6507,21 @@ export default function WeeklyView() { onClick={() => setSomedayTab(null)} >{t.allTabs} {somedayTabs.map(tab => ( - +
+ + +
))} )} @@ -6496,11 +6536,17 @@ export default function WeeklyView() { onClick={() => setSomedayTab(null)} >{t.allTabs} {somedayTabs.map(tab => ( - +
+ + +
))} )} diff --git a/src/lib/synology-calendar.ts b/src/lib/synology-calendar.ts index aed4651..11f9586 100644 --- a/src/lib/synology-calendar.ts +++ b/src/lib/synology-calendar.ts @@ -39,6 +39,36 @@ const createClient = (serverUrl: string, username: string, password: string) => }); }; +/** + * Login with fallback: Synology's CalDAV doesn't support well-known discovery, + * so if login() fails with "cannot find homeUrl", retry with the serverUrl as homeUrl. + */ +const loginClient = async (client: DAVClient, serverUrl: string, username: string) => { + try { + await client.login(); + } catch (err: any) { + if (err?.message?.includes('homeUrl')) { + // Synology doesn't support .well-known discovery — derive homeUrl from serverUrl + let homeUrl = serverUrl.replace(/\/$/, ''); + if (!homeUrl.includes('/caldav')) { + homeUrl = `${homeUrl}/caldav/${username}`; + } + // Strip any calendar-specific path segments (UUID), keep up to username + const match = homeUrl.match(/^(https?:\/\/[^/]+\/caldav(?:\.php)?\/[^/]+)\/?/); + if (match) homeUrl = match[1] + '/'; + + console.log('[SYNOLOGY CALENDAR] Well-known discovery failed, retrying with homeUrl:', homeUrl); + (client as any).account = { + serverUrl: client.serverUrl, + accountType: 'caldav', + homeUrl, + }; + } else { + throw err; + } + } +}; + /** * Validate credentials by attempting to fetch calendars * @returns List of found calendars if successful @@ -46,7 +76,7 @@ const createClient = (serverUrl: string, username: string, password: string) => export const validateCredentials = async (serverUrl: string, username: string, password: string): Promise => { try { const client = createClient(serverUrl, username, password); - await client.login(); + await loginClient(client, serverUrl, username); const calendars = await client.fetchCalendars(); @@ -91,7 +121,7 @@ export const getUpcomingEvents = async ( ): Promise => { try { const client = createClient(serverUrl, username, password); - await client.login(); + await loginClient(client, serverUrl, username); const calendars = await client.fetchCalendars(); const getPath = (url: string) => { @@ -264,7 +294,7 @@ export const createEvent = async ( ): Promise => { try { const client = createClient(serverUrl, username, password); - await client.login(); + await loginClient(client, serverUrl, username); const calendars = await client.fetchCalendars(); const targetCalendar = calendars.find(c => c.url === calendarUrl); @@ -381,7 +411,7 @@ export const updateEvent = async ( ): Promise => { try { const client = createClient(serverUrl, username, password); - await client.login(); + await loginClient(client, serverUrl, username); let targetObject: any = null; @@ -504,7 +534,7 @@ export const deleteEvent = async ( ): Promise => { try { const client = createClient(serverUrl, username, password); - await client.login(); + await loginClient(client, serverUrl, username); const parsed = parseCaldavId(eventId); if (parsed) {