diff --git a/frontend/css/app.css b/frontend/css/app.css
index c0cbb0a..b1b3629 100644
--- a/frontend/css/app.css
+++ b/frontend/css/app.css
@@ -1841,3 +1841,21 @@ a { color: var(--primary); text-decoration: none; }
.group-member-item:hover { background: var(--bg-surface); }
.group-member-item input[type="checkbox"] { flex: 0 0 auto; margin: 0; }
.group-member-name { flex: 1 1 auto; color: var(--text-1); }
+
+/* Calendar radio list (group-visible selection in settings). */
+.cal-radio-list {
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ overflow: hidden;
+}
+.cal-radio-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 14px;
+ cursor: pointer;
+ border-bottom: 1px solid var(--border);
+}
+.cal-radio-item:last-child { border-bottom: none; }
+.cal-radio-item:hover { background: var(--bg-surface); }
+.cal-radio-item .cal-item-dot { border-radius: 50%; flex: 0 0 auto; }
diff --git a/frontend/index.html b/frontend/index.html
index a1f3bf8..54a1068 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -757,6 +757,13 @@
+
diff --git a/frontend/js/api.js b/frontend/js/api.js
index cc12b4a..6c2c1b6 100644
--- a/frontend/js/api.js
+++ b/frontend/js/api.js
@@ -50,8 +50,13 @@ async function uploadRequest(path, formData) {
return null;
}
if (!res.ok) {
- const err = await res.json().catch(() => ({ detail: t('unknown_error') }));
- throw new Error(err.detail || `HTTP ${res.status}`);
+ // Upload errors may be non-JSON (e.g. an nginx 413/502 HTML page); fall back
+ // to the HTTP status so the message is diagnostic, not "unknown error".
+ const err = await res.json().catch(() => null);
+ const detail = (err && err.detail)
+ ? err.detail
+ : (res.status === 413 ? t('upload_too_large') : `HTTP ${res.status} ${res.statusText || ''}`.trim());
+ throw new Error(detail);
}
if (res.status === 204) return null;
return res.json();
diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js
index fe7739a..1a9c608 100644
--- a/frontend/js/calendar.js
+++ b/frontend/js/calendar.js
@@ -576,28 +576,38 @@ function renderCalendarList() {
}).join('');
}
- // ── Local calendars (own + shared with me) ─────────────
- if (state.localCalendars.length) {
+ // ── Local calendars: own ones, then a separate "shared with me" group ──
+ const ownLocal = state.localCalendars.filter(c => c.owned !== false);
+ const sharedLocal = state.localCalendars.filter(c => c.owned === false && !c.group);
+
+ const renderLocalItem = (cal, withRemove) => {
+ const removeBtn = withRemove
+ ? `
`
+ : '';
+ return `
+
+
+
${escHtml(cal.name)}
+ ${removeBtn}
+
`;
+ };
+
+ if (ownLocal.length) {
html += `
${t('cal_local')}
`;
- html += state.localCalendars.map(cal => {
- const owned = cal.owned !== false;
- // Shared calendars get an owner badge and no delete button (owner-only).
- const sharedBadge = !owned
- ? `
${escHtml(cal.shared_by || '')}`
- : '';
- const removeBtn = owned
- ? `
`
- : '';
- return `
+ html += ownLocal.map(c => renderLocalItem(c, true)).join('');
+ }
+ if (sharedLocal.length) {
+ html += `
${t('shared_with_me')}
`;
+ html += sharedLocal.map(cal =>
+ `
`;
- }).join('');
+
${escHtml(cal.shared_by || '')}
+
`
+ ).join('');
}
// ── iCal subscriptions ─────────────────────────────────
@@ -677,8 +687,12 @@ function renderCalendarList() {
cacheCalId = calId; // numeric integer in cached events
} else if (source === 'local') {
const calId = parseInt(cb.dataset.calId);
- await api.put(`/local/calendars/${calId}`, { enabled: cb.checked });
const cal = state.localCalendars.find(c => c.id === calId);
+ // `enabled` is the owner's property — only the owner may PUT it.
+ // For shared/group calendars just toggle visibility client-side.
+ if (cal && cal.owned !== false) {
+ await api.put(`/local/calendars/${calId}`, { enabled: cb.checked });
+ }
if (cal) cal.enabled = cb.checked;
cacheCalId = `local-${calId}`;
} else if (source === 'ical') {
@@ -2341,6 +2355,25 @@ function bindGroupUI() {
};
}
+// Radio list of the user's OWN local calendars to pick the one visible to
+// group members (plus a "none" option). Selection is read on settings save.
+function renderGroupVisibleList(selectedId) {
+ const el = document.getElementById('cfg-group-visible-list');
+ if (!el) return;
+ const own = state.localCalendars.filter(c => c.owned !== false && !c.group);
+ const opt = (id, name, color) => {
+ const checked = (id === null && (selectedId == null)) || id === selectedId;
+ const dot = color ? `
` : '';
+ return `
`;
+ };
+ el.innerHTML =
+ opt(null, t('group_visible_none'), null) +
+ own.map(c => opt(c.id, c.name, c.color)).join('');
+}
+
// ── Settings Modal ────────────────────────────────────────
function openSettingsModal() {
const s = state.settings;
@@ -2374,6 +2407,7 @@ function openSettingsModal() {
document.getElementById('cfg-dim-past').checked = !!s.dim_past_events;
document.getElementById('cfg-language').value = getLang();
document.getElementById('cfg-private-visibility').value = s.private_event_visibility || 'busy';
+ renderGroupVisibleList(s.group_visible_calendar_id);
// Set active contrast/hour-height buttons
[
@@ -2854,6 +2888,8 @@ function bindSettingsModal() {
language: document.getElementById('cfg-language').value,
private_event_visibility: document.getElementById('cfg-private-visibility').value,
};
+ const gvSel = document.querySelector('input[name="cfg-group-visible"]:checked');
+ settings.group_visible_calendar_id = gvSel && gvSel.value ? parseInt(gvSel.value) : null;
try {
await api.put('/settings/', settings);
state.settings = { ...state.settings, ...settings };
diff --git a/frontend/js/i18n.js b/frontend/js/i18n.js
index 61a498d..5823dee 100644
--- a/frontend/js/i18n.js
+++ b/frontend/js/i18n.js
@@ -121,6 +121,12 @@ const translations = {
group_created: 'Gruppe erstellt',
group_view_label: 'Gruppenansicht: {name}',
group_exit: 'Gruppenansicht verlassen',
+ upload_too_large: 'Datei zu groß (Server-Limit). Bitte Upload-Limit erhöhen.',
+ shared_with_me: 'Mit dir geteilt',
+ settings_calendars: 'Kalender',
+ settings_group_visible: 'Für Gruppen sichtbarer Kalender',
+ settings_group_visible_desc: 'Wähle, welcher deiner Kalender für deine Gruppenmitglieder sichtbar ist',
+ group_visible_none: 'Keiner',
settings_hour_height: 'Stundenhöhe (Wochen- & Tagesansicht)',
settings_hour_height_desc: 'Wie viel Platz eine Stunde in der Zeitrasteransicht einnimmt',
hour_compact: 'Kompakt', hour_normal: 'Normal',
@@ -373,6 +379,12 @@ const translations = {
group_created: 'Group created',
group_view_label: 'Group view: {name}',
group_exit: 'Exit group view',
+ upload_too_large: 'File too large (server limit). Please raise the upload limit.',
+ shared_with_me: 'Shared with me',
+ settings_calendars: 'Calendars',
+ settings_group_visible: 'Calendar visible to groups',
+ settings_group_visible_desc: 'Choose which of your calendars your group members can see',
+ group_visible_none: 'None',
settings_hour_height: 'Hour height (week & day view)',
settings_hour_height_desc: 'How much space one hour takes in the time grid',
hour_compact: 'Compact', hour_normal: 'Normal',
diff --git a/frontend/js/version.js b/frontend/js/version.js
index e6a7b0b..c0e691f 100644
--- a/frontend/js/version.js
+++ b/frontend/js/version.js
@@ -1,2 +1,2 @@
// Increment APP_VERSION with every code change
-export const APP_VERSION = 'v25';
+export const APP_VERSION = 'v26';