Compare commits

...

2 Commits

Author SHA1 Message Date
Scarriffle
b8a578cf53 refactor(web): use server per-user colour for shared calendars
Replace the per-device localStorage colour override with the server-backed
per-user colour: the colour dot on a shared calendar now PUTs /calendars/{id}/color
(stores the recipient's own colour server-side, synced across devices) and the
list/events already carry share.color from the server, so the localStorage
re-apply layer is removed. Owners and recipients share one code path. v74.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:22:33 +02:00
Scarriffle
572172c424 fix(web): stay logged in when app initialisation fails after reload
boot() ran token validation and launchApp() inside the SAME try/catch, so ANY
error during app init (a failed calendar/event fetch, a render error) cleared
the token and bounced a validly-authenticated user to the login screen — the
"logged out on every F5" bug. Now /auth/me validates the token alone; launchApp()
runs outside that catch, so a data/render error can no longer log the user out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 21:22:33 +02:00
3 changed files with 18 additions and 38 deletions

View File

@@ -23,14 +23,23 @@ async function boot() {
// Check if already logged in // Check if already logged in
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
if (token) { if (token) {
let authed = false;
try { try {
await api.get('/auth/me'); // validate token await api.get('/auth/me'); // validate the TOKEN only
await launchApp(); authed = true;
return;
} catch (_) { } catch (_) {
// The token is genuinely invalid/expired — clear it and show login.
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('user'); localStorage.removeItem('user');
} }
if (authed) {
// Token is valid → stay logged in. launchApp() runs OUTSIDE the auth
// try/catch on purpose: a later data/render error inside app init must
// never bounce a validly-authenticated user back to the login screen
// (that was the "logged out on every reload" bug).
await launchApp();
return;
}
} }
showScreen('login'); showScreen('login');

View File

@@ -305,27 +305,6 @@ function setGroupColorOverride(groupId, key, hex) {
try { localStorage.setItem('groupMemberColors', JSON.stringify(all)); } catch (e) { /* ignore */ } try { localStorage.setItem('groupMemberColors', JSON.stringify(all)); } catch (e) { /* ignore */ }
} }
// Per-device colour override for calendars shared WITH me. The calendar belongs
// to someone else (server colour is theirs), so the recipient's chosen colour is
// stored locally and re-applied on every fetch — keyed by local calendar id.
function loadSharedCalColors() {
try { return JSON.parse(localStorage.getItem('sharedCalColors') || '{}'); } catch (e) { return {}; }
}
function sharedCalColorOverride(id) { return loadSharedCalColors()[String(id)] || null; }
function setSharedCalColorOverride(id, hex) {
const all = loadSharedCalColors();
if (hex) all[String(id)] = hex; else delete all[String(id)];
try { localStorage.setItem('sharedCalColors', JSON.stringify(all)); } catch (e) { /* ignore */ }
}
function applySharedColorOverrides(events) {
const overrides = loadSharedCalColors();
if (!Object.keys(overrides).length) return;
events.forEach(ev => {
if (ev.source !== 'local') return;
const cid = String(ev.calendar_id).replace('local-', '');
if (overrides[cid]) ev.calendarColor = overrides[cid];
});
}
async function fetchAndRender(force = false, silent = false) { async function fetchAndRender(force = false, silent = false) {
const { start, end } = getViewRange(); const { start, end } = getViewRange();
@@ -405,7 +384,6 @@ async function fetchAndRender(force = false, silent = false) {
showToast(`${label} (${err.name}): ${err.message}`, true); showToast(`${label} (${err.name}): ${err.message}`, true);
} }
} }
applySharedColorOverrides(events);
eventCache.start = fetchStart; eventCache.start = fetchStart;
eventCache.end = fetchEnd; eventCache.end = fetchEnd;
eventCache.events = events; eventCache.events = events;
@@ -756,7 +734,7 @@ function renderCalendarList() {
// (e.g. Guido's "Persönlich" appears as "Guido"); the original calendar // (e.g. Guido's "Persönlich" appears as "Guido"); the original calendar
// name stays in the sub-label so it's still identifiable. // name stays in the sub-label so it's still identifiable.
entries.push({ key: `local:${cal.id}`, source: 'local', dataId: `data-cal-id="${cal.id}"`, entries.push({ key: `local:${cal.id}`, source: 'local', dataId: `data-cal-id="${cal.id}"`,
name: cal.shared_by || cal.name, color: sharedCalColorOverride(cal.id) || cal.color, enabled: cal.enabled, readOnly, name: cal.shared_by || cal.name, color: cal.color, enabled: cal.enabled, readOnly,
sourceLabel: `${t('shared_with_me')} · ${cal.name}${readOnly ? ' · ' + t('perm_read') : ''}`, remove: null }); sourceLabel: `${t('shared_with_me')} · ${cal.name}${readOnly ? ' · ' + t('perm_read') : ''}`, remove: null });
}); });
// Group calendars (owned by the creator or reached via membership) — shown so // Group calendars (owned by the creator or reached via membership) — shown so
@@ -928,19 +906,12 @@ function renderCalendarList() {
} else if (source === 'local') { } else if (source === 'local') {
const calId = parseInt(dot.dataset.calId); const calId = parseInt(dot.dataset.calId);
const cal = state.localCalendars.find(c => c.id === calId); const cal = state.localCalendars.find(c => c.id === calId);
if (cal && cal.owned === false) {
// Shared calendar — belongs to someone else. Apply a per-device local
// colour (stored client-side) instead of a server PUT that would 403.
const picked = await openColorPicker(dot, sharedCalColorOverride(calId) || cal.color || '#34a853');
if (picked) {
setSharedCalColorOverride(calId, picked);
applyCalendarColor('local', calId, picked); // recolour cached events + re-render
}
return;
}
const picked = await openColorPicker(dot, cal?.color || '#34a853'); const picked = await openColorPicker(dot, cal?.color || '#34a853');
if (picked) { if (picked) {
await api.put(`/local/calendars/${calId}`, { color: picked }); // Colour-only endpoint: the owner sets the calendar's colour, a share
// recipient sets their OWN per-user colour (server-side, synced across
// devices) — without write access or the ability to rename.
await api.put(`/local/calendars/${calId}/color`, { color: picked });
if (cal) cal.color = picked; if (cal) cal.color = picked;
applyCalendarColor('local', calId, picked); applyCalendarColor('local', calId, picked);
} }

View File

@@ -1,2 +1,2 @@
// Increment APP_VERSION with every code change // Increment APP_VERSION with every code change
export const APP_VERSION = 'v73'; export const APP_VERSION = 'v74';