Birthday sync-device list + guard group-visible against birthday calendar

- new BirthdaySyncDevice table + /api/birthdays/sync-report & /devices so the
  web can show "birthdays come from these devices" (iOS reports its device on
  each Contacts sync)
- settings: reject setting a birthday calendar as the group-visible calendar
  (it can still be shared directly)
- web: Settings > Calendars > Birthdays shows the device list; exclude birthday
  calendars from the group-visible picker

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-13 18:52:02 +02:00
parent 19258096e2
commit aa12b83302
6 changed files with 141 additions and 3 deletions

View File

@@ -3237,7 +3237,9 @@ function renderGroupVisibleList(selectedId) {
const el = document.getElementById('cfg-group-visible-list');
if (!el) return;
el.dataset.selected = (selectedId == null) ? '' : String(selectedId);
const own = state.localCalendars.filter(c => c.owned !== false && !c.group);
// A birthday calendar may be shared directly, but never stand in as the
// group-visible personal calendar.
const own = state.localCalendars.filter(c => c.owned !== false && !c.group && !c.is_birthday);
const selVal = el.dataset.selected;
const row = (id, name, color) => {
const val = (id == null) ? '' : String(id);
@@ -3631,7 +3633,26 @@ function renderBirthdaySettings() {
const colorHint = document.createElement('div');
colorHint.className = 'form-hint';
colorHint.textContent = t('birthday_color_hint');
container.append(status, notifyWrap, colorHint);
const devicesWrap = document.createElement('div');
devicesWrap.style.marginTop = '12px';
container.append(status, notifyWrap, colorHint, devicesWrap);
// Which devices contribute birthdays (from Contacts sync).
api.get('/birthdays/devices').then(devs => {
if (!Array.isArray(devs) || !devs.length) return;
const title = document.createElement('div');
title.className = 'panel-desc';
title.style.margin = '0 0 4px';
title.textContent = t('birthday_devices_title');
devicesWrap.appendChild(title);
devs.forEach(d => {
const rowEl = document.createElement('div');
rowEl.className = 'form-hint';
rowEl.style.margin = '0 0 2px';
const when = d.last_sync ? new Date(d.last_sync).toLocaleDateString() : '';
rowEl.textContent = `${d.device_name}${t('birthday_devices_count', { n: d.count })}${when ? ' · ' + when : ''}`;
devicesWrap.appendChild(rowEl);
});
}).catch(() => {});
}
function renderHiddenCalendars() {

View File

@@ -297,6 +297,8 @@ const translations = {
birthday_year_ph: 'Jahr',
birthday_settings_title: 'Geburtstage',
birthday_color_hint: 'Farbe und Sichtbarkeit änderst du in der Seitenleiste.',
birthday_devices_title: 'Geburtstage kommen von diesen Geräten:',
birthday_devices_count: '{n} Geburtstage',
error_name_url: 'Bitte Name und URL eingeben',
ical_subscribed: '"{name}" abonniert',
google_synced: 'Kalender synchronisiert',
@@ -639,6 +641,8 @@ const translations = {
birthday_year_ph: 'Year',
birthday_settings_title: 'Birthdays',
birthday_color_hint: 'Change colour and visibility in the sidebar.',
birthday_devices_title: 'Birthdays come from these devices:',
birthday_devices_count: '{n} birthdays',
error_name_url: 'Please enter a name and URL',
ical_subscribed: '"{name}" subscribed',
google_synced: 'Calendar synced',