# macro-book calendar — maintenance

How to operate the calendar: add events, change reminders, fix dates, regenerate the `.ics`, and connect to Google Calendar.

## Current setup (as of 2026-06-16)

- **Source of truth:** `07-calendar/events.yaml`
- **Generator:** `scripts/calendar_sync.py` → `07-calendar/macro-book.ics` (204K, 396 events, 470 alarms)
- **HTTP server:** systemd user service `calendar-http.service` (auto-restart, journald logging, enabled on login via linger)
- **Public exposure:** Tailscale Funnel on port 443, TLS via Tailscale, proxies to localhost:8765
- **Refresh:** cron at `0 6 * * *` → `scripts/refresh_calendar.sh` regenerates the `.ics`. The server reads the file on each request — no service restart needed.
- **Subscription URL (PUBLIC):** `https://raspberrypi.tailbb3ae1.ts.net/07-calendar/macro-book.ics`
- **Subscription URL (tailnet-only, for your other Tailscale devices):** `http://100.102.237.9:8765/07-calendar/macro-book.ics`

## TL;DR

```bash
# 1. Edit events
$EDITOR ~/Obsidian-Macro/07-calendar/events.yaml

# 2. Regenerate the .ics (optional — cron does this daily at 06:00)
~/Obsidian-Macro/scripts/refresh_calendar.sh

# 3. Add to Google Calendar (one-time, in your browser)
#    Open Google Calendar (signed in to the gmail account) → Settings (gear) →
#    Add calendar → Subscribe to URL → paste:
#    http://100.102.237.9:8765/07-calendar/macro-book.ics
```

## How the system fits together

```
07-calendar/events.yaml          ← you edit this
        ↓
scripts/calendar_sync.py         ← reads YAML, expands recurrences, computes UTC, attaches VALARMs
        ↓
07-calendar/macro-book.ics       ← static file
        ↓
python3 -m http.server :8765     ← systemd user service, serving ~/Obsidian-Macro/
        ↓
Tailscale (100.102.237.9)        ← encrypted transport
        ↓
Google Calendar (your account)   ← subscribes to the URL, polls every ~12h, auto-syncs
```

## Operating the service

```bash
# Status
systemctl --user status calendar-http

# Restart (e.g. after editing the service file)
systemctl --user restart calendar-http

# View logs
journalctl --user -u calendar-http -f

# Disable auto-start (if you want to run it manually)
systemctl --user disable calendar-http
```

The service binds to `0.0.0.0:8765` so the `.ics` is reachable from any interface (local network AND Tailscale). It is not authenticated — anyone on your tailnet or local network can fetch the `.ics`. The data is public-by-design (you want Google to fetch it), so this is fine. If you want to lock it down to Tailscale only, change `--bind 0.0.0.0` to `--bind 100.102.237.9` in the service unit.

## Adding the calendar to Google Calendar (one-time)

The Funnel URL is **public**, so Google Cal servers can fetch it from anywhere.

1. Open <https://calendar.google.com> in a browser, signed in to your macro-book gmail account
2. First, **remove the broken subscription** (the old `http://100.102.237.9:8765/...` URL that wasn't reachable from Google):
   - Left sidebar → click the three-dot menu next to the `macro-book` calendar → **Settings** → scroll to bottom → **Delete** (or just **Unsubscribe**)
3. Then add the new one:
   - Left sidebar → **+** next to "Other calendars" → **Subscribe to URL**
   - Paste: `https://raspberrypi.tailbb3ae1.ts.net/07-calendar/macro-book.ics`
   - Click **Subscribe**
4. Optionally rename to `macro-book`, pick a colour, click **Subscribe** again

Google will fetch the `.ics` immediately to populate the calendar, then re-fetch every ~12 hours to pick up updates. If you regenerate the `.ics` manually and want to see changes immediately, in Google Calendar's web UI: click the calendar's three-dot menu → **Settings** → near the bottom, the URL is shown — Google will refresh on its own schedule.

## Editing events

Open `07-calendar/events.yaml`. Each event is a list item with these fields:

```yaml
- id: unique-id-here           # required, used for UID generation
  name: "Human-readable name"  # required
  tier: 1 | 2 | structural     # required, drives reminder windows
  date: 2026-07-29             # required for one-off events (no recurrence)
  time: "14:00"                # required, in the source timezone
  tz_source: America/New_York  # required, IANA timezone name
  source: "https://..."        # optional, shown in DESCRIPTION
  notes: "free text"           # optional, shown in DESCRIPTION
  categories: [rates, us]      # optional, written as CATEGORIES
  status: projected            # optional, marks dates that are inferred not confirmed
```

For recurring events, use `recurrence` instead of `date`:

```yaml
- id: cot-friday
  name: "CFTC COT Report"
  tier: structural
  recurrence: weekly-friday     # or 'weekday'
  time: "15:30"
  tz_source: America/New_York
  start: 2026-06-19
  end: 2027-06-30
```

Supported recurrences:
- `weekly-friday` — every Friday between start and end
- `weekday` — every Mon-Fri between start and end (no holidays excluded; add manual entries for known closures if needed)

## Changing reminder windows

Edit the `reminder_defaults` block at the top of `events.yaml`:

```yaml
reminder_defaults:
  tier1: ["P7D", "P1D", "PT1H"]   # 7 days, 1 day, 1 hour before
  tier2: ["P1D"]                   # 1 day before
  structural: ["PT12H"]            # 12 hours before
```

Or override per-event by adding a `reminders` list to the event (not yet supported — file an issue if you need it).

## When new schedules are published

- **BLS** publishes 2027 schedule in Q4 2026 — add dates for cpi-2027-* and nfp-2027-*
- **FOMC** tentative 2028 schedule is announced ~Aug 2027 — add next year
- **ECB** publishes 2028 schedule ~end of 2027
- **BoJ** publishes 2027 schedule mid-2026
- **SNB** is quarterly, add as published
- **ISM** is first/third business day of month — current dates go through Dec 2026, add 2027 when ISM publishes

## Verification

```bash
# Count events in the .ics
grep -c "BEGIN:VEVENT" ~/Obsidian-Macro/07-calendar/macro-book.ics
# Should print ~396

# Check service is up
systemctl --user is-active calendar-http

# Test fetch (should return 200, ~204K bytes)
# Public URL (Google Cal and any internet service):
curl -sS -o /dev/null -w "HTTP %{http_code} %{size_download} bytes\n" \
  https://raspberrypi.tailbb3ae1.ts.net/07-calendar/macro-book.ics
# Tailnet-only URL (Tailscale devices):
curl -sS -o /dev/null -w "HTTP %{http_code} %{size_download} bytes\n" \
  http://100.102.237.9:8765/07-calendar/macro-book.ics

# Force a regeneration
~/Obsidian-Macro/scripts/refresh_calendar.sh
```

## Cron

`crontab -l` shows:

```
0 6 * * * /home/rpi/Obsidian-Macro/scripts/refresh_calendar.sh >> /home/rpi/Obsidian-Macro/07-calendar/refresh.log 2>&1
```

Daily at 06:00 Europe/Zurich (server-local). Adjust if you need different timing. The `.ics` is overwritten in place; the http.server picks up the new file on its next request (no restart needed).

## Telegram reminders (optional, not in v1)

If you want Telegram pushes on top of native calendar notifications, the path is:

1. Add a Telegram bot for the user (separate from Hermes — this would be a personal one, not the agent's)
2. Write `scripts/telegram_reminders.py` that reads `events.yaml` and sends T-24h + T-1h messages
3. Run from a separate cron: `*/30 * * * *` checks every 30 min and sends for any event whose reminder window has just opened

Skipped in v1 because Google Calendar native notifications on phone cover the requirement.

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| Service won't start | python3 missing or port already in use | `systemctl --user status calendar-http` for details |
| Service is up but Tailscale is not | tailscaled.service down | `sudo systemctl start tailscaled` |
| `curl http://100.102.237.9:8765/...` times out from another device | That device isn't on the tailnet, or firewall blocks the port | Test from a known tailnet device; check Tailscale ACLs in admin console |
| `curl` works from Pi but Google Cal shows "URL not found" | Google needs to be able to reach the URL — for the public Funnel URL, check that `tailscale funnel status` shows the listener is still up | Verify with `curl https://raspberrypi.tailbb3ae1.ts.net/07-calendar/macro-book.ics` from any non-Pi network |
| Google Cal shows old events after regenerating | Google re-fetches every ~12h, not immediately | Wait, or trigger a manual refresh in Google Cal web UI (calendar settings → URL shown) |
| Events show with wrong time | The `.ics` is UTC-encoded; Google Cal converts to your local TZ. If times are off by hours, your Google Cal account timezone is wrong | Check Google Cal settings → Time zone |
| Cron doesn't run | Crontab syntax, or system clock off | `crontab -l` to verify entry; check `journalctl --user` for errors |

## Notes on Funnel

Tailscale Funnel exposes the local port over Cloudflare's edge. The hostname `raspberrypi.tailbb3ae1.ts.net` is a Tailscale-managed certificate. Funnel is on by default once enabled in the admin console and started on the node; it persists across reboots.

Useful commands:
```bash
tailscale funnel status          # see current config
tailscale funnel reset           # remove all funnel listeners
sudo tailscale funnel --bg 8765  # re-add the listener (after reset or reboot)
journalctl --user -u calendar-http -f  # service logs
```

If you ever change the calendar-http service port, update the funnel target accordingly:
```bash
sudo tailscale funnel --bg <new-port>
```
