···6 "net/http"
7 "os"
8 "os/signal"
9+ "strings"
10 "syscall"
11 "time"
12···49 listHandler := handlers.NewListHandler(authHandler.Client())
50 settingsHandler := handlers.NewSettingsHandler(authHandler.Client())
51 pushHandler := handlers.NewPushHandler(notificationRepo)
52+ calendarHandler := handlers.NewCalendarHandler(authHandler.Client())
53+ icalHandler := handlers.NewICalHandler(authHandler.Client())
5455 // Initialize Stripe client and supporter handler (only if Stripe keys are configured)
56 var supporterHandler *handlers.SupporterHandler
···6768 // Initialize push notification sender (only if VAPID keys are configured)
69 var pushSender *push.Sender
70+ var taskJobRunner *jobs.Runner
71+ var calendarJobRunner *jobs.Runner
72 if cfg.VAPIDPublicKey != "" && cfg.VAPIDPrivateKey != "" {
73 pushSender = push.NewSender(cfg.VAPIDPublicKey, cfg.VAPIDPrivateKey, cfg.VAPIDSubscriber)
74 pushHandler.SetSender(pushSender)
75 log.Println("Push notification sender initialized")
7677+ // Initialize background job runner for task notifications (check every 5 minutes)
78+ taskJobRunner = jobs.NewRunner(5 * time.Minute)
0079 notificationJob := jobs.NewNotificationCheckJob(notificationRepo, authHandler.Client(), pushSender)
80+ taskJobRunner.AddJob(notificationJob)
81+ taskJobRunner.Start()
82+ log.Println("Task notification job runner started (5 minute interval)")
8384+ // Initialize background job runner for calendar notifications (check every 30 minutes)
85+ calendarJobRunner = jobs.NewRunner(30 * time.Minute)
86+ calendarNotificationJob := jobs.NewCalendarNotificationJob(notificationRepo, authHandler.Client(), pushSender, settingsHandler)
87+ calendarJobRunner.AddJob(calendarNotificationJob)
88+ calendarJobRunner.Start()
89+ log.Println("Calendar notification job runner started (30 minute interval)")
90 } else {
91 log.Println("VAPID keys not configured - push notifications disabled")
92 log.Println("Run 'go run ./cmd/vapid' to generate VAPID keys")
···144 mux.HandleFunc("/list/", listHandler.HandlePublicListView)
145 logRoute("GET /list/*")
146147+ // Public iCal feed routes
148+ mux.HandleFunc("/calendar/feed/", icalHandler.GenerateCalendarFeed)
149+ logRoute("GET /calendar/feed/{did}/events.ics")
150+ mux.HandleFunc("/tasks/feed/", icalHandler.GenerateTasksFeed)
151+ logRoute("GET /tasks/feed/{did}/tasks.ics")
152+153 // Protected routes
154 mux.Handle("/app", authMiddleware.RequireAuth(http.HandlerFunc(handleDashboard)))
155 logRoute("GET /app [protected]")
···175 logRoute("POST /app/push/test [protected]")
176 mux.Handle("/app/push/check", authMiddleware.RequireAuth(http.HandlerFunc(pushHandler.HandleCheckTasks)))
177 logRoute("POST /app/push/check [protected]")
178+179+ // Calendar routes
180+ mux.Handle("/app/calendar/events", authMiddleware.RequireAuth(http.HandlerFunc(calendarHandler.ListEvents)))
181+ logRoute("GET /app/calendar/events [protected]")
182+ mux.Handle("/app/calendar/upcoming", authMiddleware.RequireAuth(http.HandlerFunc(calendarHandler.ListUpcomingEvents)))
183+ logRoute("GET /app/calendar/upcoming [protected]")
184+ // Note: These must be after the specific routes above to avoid matching them
185+ mux.Handle("/app/calendar/events/", authMiddleware.RequireAuth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
186+ // Route to either GetEvent or GetEventRSVPs based on URL
187+ if strings.HasSuffix(r.URL.Path, "/rsvps") {
188+ calendarHandler.GetEventRSVPs(w, r)
189+ } else {
190+ calendarHandler.GetEvent(w, r)
191+ }
192+ })))
193+ logRoute("GET /app/calendar/events/:rkey [protected]")
194+ logRoute("GET /app/calendar/events/:rkey/rsvps [protected]")
195196 // Supporter routes (only if Stripe is configured)
197 if supporterHandler != nil {
···243 log.Println("Shutting down gracefully...")
244245 // Stop background jobs
246+ if taskJobRunner != nil {
247+ taskJobRunner.Stop()
248+ }
249+ if calendarJobRunner != nil {
250+ calendarJobRunner.Stop()
251 }
252253 log.Println("Shutdown complete")
+264
docs/features.md
···12- [User Interface Preferences](#user-interface-preferences)
13- [Notifications](#notifications)
14- [Lists](#lists)
015- [Progressive Web App](#progressive-web-app)
1617---
···5024. Share with anyone
503504**Shared lists are public** - anyone with the link can view tasks in that list (but not edit them).
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000505506---
507
···12- [User Interface Preferences](#user-interface-preferences)
13- [Notifications](#notifications)
14- [Lists](#lists)
15+- [Calendar Events](#calendar-events)
16- [Progressive Web App](#progressive-web-app)
1718---
···5034. Share with anyone
504505**Shared lists are public** - anyone with the link can view tasks in that list (but not edit them).
506+507+---
508+509+## Calendar Events
510+511+AT Todo integrates with the AT Protocol calendar ecosystem, allowing you to view calendar events created by other calendar applications like [Smokesignal](https://smokesignal.events).
512+513+### What are Calendar Events?
514+515+Calendar events are social events stored in the AT Protocol using the `community.lexicon.calendar.event` lexicon. Unlike tasks (which are personal todo items), calendar events are typically:
516+517+- **Public or shared**: Visible to others in the AT Protocol network
518+- **Time-specific**: Have defined start/end times
519+- **Social**: Support RSVPs and attendance tracking
520+- **External**: Created by dedicated calendar apps like Smokesignal
521+522+### Viewing Calendar Events
523+524+**Access calendar events:**
525+1. Navigate to the **📅 Events** tab in your dashboard
526+2. Choose between two views:
527+ - **Upcoming Events**: Shows events in the next 7 days
528+ - **All Events**: Shows all calendar events
529+530+**Event display includes:**
531+- 📅 Event name and description
532+- 🕐 Start and end times (in your local timezone)
533+- 📍 Location information (for in-person events)
534+- 💻 Attendance mode (Virtual, In Person, or Hybrid)
535+- 🔗 Links to related resources
536+- 💨 Direct link to view on Smokesignal
537+538+### Event Details
539+540+**View detailed information:**
541+1. Click "View Details" on any event card
542+2. A modal opens showing:
543+ - Full event description
544+ - Complete date/time information
545+ - Event status (Planned, Scheduled, Rescheduled, Cancelled)
546+ - Location details with addresses
547+ - Attendance mode with visual indicators
548+ - Related URLs and resources
549+ - Your personal RSVP status (if you've RSVP'd)
550+ - Link to view all RSVPs on Smokesignal
551+552+**Visual indicators:**
553+- 💻 **Virtual**: Online-only events
554+- 📍 **In Person**: Physical location events
555+- 🔄 **Hybrid**: Both online and in-person options
556+557+### Event Sources
558+559+AT Todo displays events from two sources:
560+561+1. **Your own events**: Calendar events in your AT Protocol repository
562+2. **RSVP'd events**: Events you've RSVP'd to via calendar apps
563+564+All events are read-only in AT Todo. To create or manage events, use a dedicated calendar app like [Smokesignal](https://smokesignal.events).
565+566+### Calendar Notifications
567+568+Stay informed about upcoming events with automatic notifications.
569+570+**Enabling calendar notifications:**
571+1. Open Settings
572+2. Scroll to "Calendar Notification Settings"
573+3. Toggle "Enable calendar event notifications"
574+4. Set your preferred lead time (default: 1 hour before event)
575+5. Ensure push notifications are enabled
576+577+**Notification timing:**
578+- Choose how far in advance to be notified (e.g., "1h", "30m", "2h")
579+- Notifications sent when events fall within your lead time window
580+- Default: 1 hour before event start time
581+- Respects quiet hours settings
582+583+**What you'll receive:**
584+```
585+Upcoming Event: Community Meetup
586+💻 Virtual event starts in 1 hour
587+```
588+589+**Smart notification features:**
590+- ✅ Shows event mode (Virtual/In-Person/Hybrid)
591+- ✅ Calculates time until event starts
592+- ✅ Links to Smokesignal for full details
593+- ✅ Sent to all registered devices
594+- ✅ Won't spam (24-hour cooldown per event)
595+596+**Notification frequency:**
597+Calendar events are checked every 30 minutes for upcoming events (separate from task notifications which run every 5 minutes).
598+599+### RSVPs and Attendance
600+601+**Viewing your RSVP:**
602+- Open event details to see your RSVP status
603+- Status indicators:
604+ - ✓ **Going** (green)
605+ - ⓘ **Interested** (blue)
606+ - ✗ **Not Going** (red)
607+608+**Managing RSVPs:**
609+RSVPs are managed through calendar applications like Smokesignal. AT Todo displays your RSVP status but doesn't provide RSVP functionality.
610+611+**To RSVP to an event:**
612+1. Click the 💨 Smokesignal link in the event details
613+2. RSVP on Smokesignal
614+3. Your RSVP status will appear in AT Todo automatically
615+616+### Event Status Badges
617+618+Events display status badges to indicate their current state:
619+620+- **Scheduled** (green): Confirmed and finalized
621+- **Planned** (blue): Created but not yet finalized
622+- **Rescheduled** (orange): Date/time has been changed
623+- **Cancelled** (red): Event has been cancelled
624+- **Postponed** (red): Event delayed with no new date
625+626+Cancelled and postponed events still appear in your calendar but are clearly marked.
627+628+### Integration with Smokesignal
629+630+AT Todo integrates seamlessly with [Smokesignal](https://smokesignal.events), the premier AT Protocol calendar application.
631+632+**Smokesignal features:**
633+- Create and manage calendar events
634+- RSVP to events
635+- View all RSVPs and attendees
636+- Share event links
637+- Event discovery and search
638+639+**Quick access:**
640+- Click the 💨 emoji next to any event name
641+- Opens the event directly on Smokesignal
642+- View full attendee list
643+- Manage your RSVP
644+- Share event with others
645+646+### Google Calendar & iCal Subscription
647+648+Subscribe to your AT Protocol calendar events **and tasks** in Google Calendar, Apple Calendar, Outlook, or any calendar app that supports iCal feeds.
649+650+**Getting your iCal feed URLs:**
651+652+AT Todo provides two separate feeds:
653+654+**Calendar Events Feed:**
655+```
656+https://attodo.app/calendar/feed/{your-did}/events.ics
657+```
658+659+**Tasks Feed (tasks with due dates):**
660+```
661+https://attodo.app/tasks/feed/{your-did}/tasks.ics
662+```
663+664+To find your DID:
665+1. Open AT Todo and navigate to the 📅 Events tab
666+2. Open your browser's developer console (F12)
667+3. Your DID appears in the console when loading events, or
668+4. Check your AT Protocol profile on Bluesky
669+670+**Subscribing in Google Calendar:**
671+672+You can subscribe to both feeds separately:
673+674+**For Events:**
675+1. Open [Google Calendar](https://calendar.google.com)
676+2. Click the **+** next to "Other calendars"
677+3. Select **"From URL"**
678+4. Paste your events feed URL: `https://attodo.app/calendar/feed/{your-did}/events.ics`
679+5. Click **"Add calendar"**
680+681+**For Tasks:**
682+1. Click the **+** next to "Other calendars" again
683+2. Select **"From URL"**
684+3. Paste your tasks feed URL: `https://attodo.app/tasks/feed/{your-did}/tasks.ics`
685+4. Click **"Add calendar"**
686+687+Your AT Protocol events and tasks will now appear in Google Calendar and sync automatically!
688+689+**Subscribing in Apple Calendar:**
690+691+Subscribe to both feeds for complete coverage:
692+693+1. Open Calendar app
694+2. Go to **File** → **New Calendar Subscription**
695+3. Paste your events feed URL, click **Subscribe**
696+4. Choose auto-refresh frequency (recommended: every hour)
697+5. Repeat for tasks feed URL
698+699+**Subscribing in Outlook:**
700+701+Subscribe to both feeds separately:
702+703+1. Open Outlook
704+2. Go to **File** → **Account Settings** → **Internet Calendars**
705+3. Click **New**
706+4. Paste your events feed URL, click **Add**
707+5. Repeat for tasks feed URL
708+709+**What syncs from Events Feed:**
710+- ✅ Event names and descriptions
711+- ✅ Start and end times (in your timezone)
712+- ✅ Location information
713+- ✅ Event status (confirmed, tentative, cancelled)
714+- ✅ Links to Smokesignal
715+- ✅ Event mode (virtual/in-person/hybrid) as categories
716+717+**What syncs from Tasks Feed:**
718+- ✅ Task titles and descriptions
719+- ✅ Due dates (in your timezone)
720+- ✅ Completion status
721+- ✅ Tags as categories
722+- ✅ Only tasks with due dates (no due date = not included)
723+724+**Auto-refresh:**
725+- Calendar apps check for updates periodically
726+- Google Calendar: Every few hours
727+- Apple Calendar: Configurable (hourly recommended)
728+- Outlook: Configurable
729+730+**Privacy note:**
731+Calendar events and tasks in AT Protocol are public by design. Anyone with your iCal feed URLs can view your events and tasks. This is the same as viewing them on Smokesignal or other AT Protocol apps.
732+733+**Tips:**
734+- Subscribe to both feeds to see your complete schedule in one place
735+- Tasks appear as "todos" in most calendar apps
736+- Completed tasks remain in the feed with completion status
737+- Use separate calendar colors to distinguish events from tasks
738+739+### Event Timezone Handling
740+741+All event times are automatically converted to your local timezone:
742+- **Displayed**: In your browser's timezone
743+- **Stored**: In UTC in AT Protocol
744+- **Notifications**: Respect your local time
745+- **Created date**: Shows when the event was created (in local time)
746+747+### Read-Only Access
748+749+**Important notes:**
750+- 📖 Calendar events in AT Todo are **read-only**
751+- ✏️ To create or edit events, use a calendar app like Smokesignal
752+- 🔄 AT Todo automatically syncs events from your AT Protocol repository
753+- 📅 Perfect for viewing your event schedule alongside your tasks
754+755+### Calendar Best Practices
756+757+**Organizing your calendar:**
758+1. **Use Smokesignal** for event creation and management
759+2. **View in AT Todo** to see events alongside tasks
760+3. **Enable notifications** to stay informed
761+4. **RSVP on Smokesignal** to indicate attendance
762+5. **Share event links** from Smokesignal with others
763+764+**Integrating with tasks:**
765+- Create tasks for event preparation (e.g., "Prepare presentation for Monday meetup")
766+- Use tags to link tasks to events (e.g., #meetup, #conference)
767+- Set task due dates relative to event times
768+- Enable both task and calendar notifications
769770---
771