feat(calendar): en læst Google-kalender, med sit eget endpoint og sin egen sweep

Samme registrering som Gmail, et andet scope og en anden redirect-URI. Google
udsteder en refresh-token pr. godkendelse og holder op til hundrede i live pr.
konto, hver med sine egne scopes — så en forbundet kalender rører ikke en
Gmail, der virker, og at afbryde den ene tager ikke den anden med.

Callbacken har sin egen sti. Én callback, der ud fra gemt tilstand afgjorde,
hvilket af de to flows der kom tilbage, ville lægge valget af *hvilket
token-lager der skrives* bag et opslag — og går det galt, går det galt ved at
overskrive en virkende Gmail-forbindelse med kalender-tokens.

Begivenhederne caches i et vindue på 90 dage tilbage og et år frem. Et
månedsgitter bladrer, og et kald til Google pr. piletast er en kalender,
der er langsommere end den kalender, den viser. Vinduet har en kant, og
endpointet siger hvor den er: et tomt svar uden forklaring er en kalender,
der lydløst påstår, at en dag er fri.
This commit is contained in:
Kristian Wind
2026-08-20 07:01:14 +02:00
parent 7828c1e6ed
commit 987d4edfce
16 changed files with 1476 additions and 22 deletions
+38
View File
@@ -61,6 +61,7 @@ export const en = {
'nav.upcoming': 'Upcoming',
'nav.delegated': 'Waiting on others',
'nav.inbox': 'Inbox',
'nav.calendar': 'Calendar',
'nav.settings': 'Settings',
'nav.signOut': 'Sign out',
'nav.main': 'Main menu',
@@ -582,6 +583,29 @@ export const en = {
'int.server': 'Server',
'int.caldavAuth':
'The username is your email. The password is an API token — not your real password.',
'int.calendar': 'Google Calendar',
'int.calendarHint':
'Read, never written. The events appear in Calendar beside your own tasks with due dates — they cannot be moved or completed from here, because they belong to Google.',
'int.connectCalendar': 'Connect Google Calendar',
'int.calendarConnectedAs': 'Connected as',
'int.whichCalendars': 'Which calendars are shown',
'int.noCalendars': 'The account has no calendars to show.',
'int.calendarLastSync': 'Last fetched {when}.',
'int.calendarNeverSynced': 'Not fetched yet.',
'int.calendarFetchedOne': '{n} event fetched.',
'int.calendarFetchedMany': '{n} events fetched.',
'int.calendarNothingNew': 'No events in the window.',
'int.calendarConnected': 'Google Calendar is connected.',
'int.calendarState': 'Google’s reply did not match the attempt that started here. Try again.',
'int.calendarExpired': 'The attempt expired on the way. Try again.',
'int.calendarInvalid': 'Google’s reply could not be read. Try again.',
'int.calendarFailed': 'Google refused to exchange the code.',
'int.calendarNoRefresh':
'Google sent no refresh token. Remove verdande from your Google account and connect again.',
'int.calendarDenied': 'Access was refused at Google.',
'int.calendarOrgInternal':
'The Google app is registered as Internal and only accepts accounts inside the organisation. A private account cannot consent to it, whatever the screen says.',
'int.calendarOther': 'Google replied: {what}',
'int.gmailFetchedOne': '{n} task fetched.',
'int.gmailFetchedMany': '{n} tasks fetched.',
'int.gmailNothingNew': 'No new messages.',
@@ -595,6 +619,16 @@ export const en = {
'int.gmailDenied': 'Access was refused at Google.',
'int.gmailOther': 'Gmail replied: {what}',
// --- the calendar, with Google’s events laid over ------------------------------------
'cal.title': 'Calendar',
'cal.notConnected': 'No calendar is connected yet. That is done under Settings → Integrations.',
'cal.settings': 'Settings',
'cal.allDay': 'All day',
'cal.openInGoogle': 'Open in Google Calendar',
'cal.beyondWindow':
'Events are kept from {from} to {to}. Outside that the grid shows only your own tasks.',
'cal.eventCannotMove': '{name} — an event in {calendar}. It cannot be moved from here.',
// --- a project ----------------------------------------------------------------------
'project.name': 'The project’s name',
'project.clickToRename': 'Click to rename',
@@ -736,6 +770,10 @@ export const en = {
'This instance cannot restart itself: {missing} are not set. A container cannot replace its own image — the panel does the work, and this is only the way to ask.',
'error.gmailSaid': 'Gmail said: {what}',
'error.calendarRefused': 'Google refused the call. Try connecting the calendar again.',
'error.calendarSaid': 'Google replied: {what}',
'int.disconnectCalendarQuestion':
'Disconnect Google Calendar? The tokens are forgotten and the fetched copy of the events is deleted. Your own tasks are untouched.',
'error.mailboxRefused': 'The mailbox could not be read.',
'error.mailboxSaid': 'The mailbox answered: {what}',