Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ This repository is a Go service that syncs calendar status and exposes availabil

## Project Overview
- Personal app for syncing calendar-driven status to GitHub.
- Status sync fans out to enabled targets rather than being tied to GitHub alone.
- Optional availability API reads a separate calendar, applies weekday `working_hours.start/end`, and returns the first free block per day for the next 10 days.
- Status sync fans out to configured targets rather than being tied to GitHub alone.
- Optional availability feature reads a separate calendar, applies weekday `suppressions.working_hours.start/end`, and returns the first free block per day for the next 10 days through its API.
- Both features are polling-based and use Pebble for persistence.
- Pebble stores status and snapshots so lookups stay O(1) for the hot path.
- The code is organized so status sync, availability sync, and HTTP serving are separate concerns.
- The config is organized by top-level `status` and `availability` domains, each with nested `sources` and `targets` where applicable.
- The code is organized so status fetch/publish, availability fetch/publish, and HTTP serving are separate concerns.

## Architecture
- `main.go` loads config, opens Pebble, builds the HTTP mux, and starts the sync loops after startup validation.
Expand All @@ -20,13 +21,14 @@ This repository is a Go service that syncs calendar status and exposes availabil
- `internal/config` loads defaults, `config.yaml`, and environment variables.

## Sync Flow
- Status sync fetches the status calendar, stores events, computes the current active event, and syncs enabled targets every 5 minutes.
- Availability sync fetches the availability calendar and stores the raw ICS body plus timezone metadata in Pebble.
- If `availability.exclude_england_bank_holidays` is enabled, the app fetches GOV.UK bank holidays once at startup and stores the parsed holiday dates locally.
- Status sync is enabled only when `status.enabled` is true; it fetches `status.sources.ical.url`, stores events, computes the current active event, and syncs configured targets on `status.sources.ical.interval`.
- Availability sync is enabled only when `availability.enabled` is true; it fetches `availability.sources.ical.url` on `availability.sources.ical.interval`, stores the raw ICS body plus timezone metadata in Pebble, exposes the API, and starts availability targets.
- If a feature-level `enabled` flag is false, both fetching and publishing for that feature must stay disabled regardless of nested configuration.
- If `availability.suppressions.exclude_england_bank_holidays` is enabled, the app fetches GOV.UK bank holidays once at startup and stores the parsed holiday dates locally.
- That startup seed is required for the availability endpoint when holiday exclusion is enabled, so startup should fail if the holiday feed cannot be read or parsed.
- Weekday availability uses `availability.working_hours.start/end` as a suppression window; if `availability.exclude_england_bank_holidays` is enabled, that suppression is lifted on England-and-Wales bank holidays from GOV.UK.
- `/api/availability` is only registered when `availability.is_enabled` is true.
- The availability handler requires an exact `Authorization` header match with `availability.api_key`.
- Weekday availability uses `availability.suppressions.working_hours.start/end` as a suppression window; if `availability.suppressions.exclude_england_bank_holidays` is enabled, that suppression is lifted on England-and-Wales bank holidays from GOV.UK.
- `/api/availability` is only registered when `availability.enabled` is true.
- The availability handler requires an exact `Authorization` header match with `availability.api.key`.

## Build, Test & Lint
- `go build ./...`
Expand All @@ -36,12 +38,13 @@ This repository is a Go service that syncs calendar status and exposes availabil

## Key Conventions
- Keep status and availability calendars separate.
- Status calendar URL and availability calendar URL are separate config values.
- Status calendar URL and availability calendar URL are separate config values: `status.sources.ical.url` and `availability.sources.ical.url`.
- Status and availability fetch intervals are separate config values and default to `5m`.
- Time blocks come from config and are checked in order.
- `availability.working_hours.start` defaults to `09:00` and `availability.working_hours.end` defaults to `17:50`; together they are treated as weekday working time, not as an availability block.
- `availability.suppressions.working_hours.start` defaults to `09:00` and `availability.suppressions.working_hours.end` defaults to `17:50`; together they are treated as weekday working time, not as an availability block.
- Availability data is stored as the fetched raw ICS body plus metadata, not as live network state.
- When enabled, bank holiday data is fetched from `https://www.gov.uk/bank-holidays.json` at startup and cached in Pebble.
- The availability route is disabled when the feature is not configured.
- The availability route is disabled when `availability.enabled` is false.
- Empty env vars are treated as unset.
- Pebble key design includes:
- `status` for the current status record
Expand All @@ -54,10 +57,12 @@ This repository is a Go service that syncs calendar status and exposes availabil
- The holiday snapshot stores the raw GOV.UK JSON body, parsed dates, and fetch timestamp so availability can be computed offline.
- Status is single-tenant.
- Time zone handling should use the feed timezone when available, with UTC fallback.
- If `status.enabled` is true, `status.sources.ical.url` and at least one status target are required; otherwise availability can run without a status calendar.
- If `availability.enabled` is true, `availability.sources.ical.url`, availability blocks, `availability.api.key`, and `availability.targets.cloudflare_pages.deploy_hook` are required.

## Adding a New Status Target
- Add a target under `internal/{platform}` implementing `target.Target`.
- Extend `TargetsConfig` and `envMapping` in `internal/config/config.go`.
- Extend `StatusTargetsConfig` and `envMapping` in `internal/config/config.go`.
- Register the target in `buildTargets()` in `main.go`.
- Add tests and update docs.

Expand All @@ -67,13 +72,13 @@ This repository is a Go service that syncs calendar status and exposes availabil
- Cancelled events are stored but do not count as active.
- Availability computation checks today plus the next 9 days and returns the first free configured block per day.
- On weekdays, blocks that overlap working hours are suppressed unless the day is a bank holiday and holiday exclusion is enabled.
- If the availability config is enabled but incomplete, startup should fail fast.
- If `availability.enabled` is true but availability config is incomplete, startup should fail fast.
- The app is designed for a single user; multi-user support would require key design changes.

## Cloudflare Pages auto-deploy

- Feature: When enabled, the application triggers a Cloudflare Pages deployment at a regular interval.
- Feature: When `availability.enabled` is true, the application triggers a Cloudflare Pages deployment at a regular interval.
- Change Tracking: To save build minutes, deployments are only triggered if the availability calendar has changed since the last successful deployment. This is managed by the `availability.Syncer`, which compares the current computed availability JSON with the `availability_last_deployed` JSON in Pebble. If a change is detected (including time-based changes), the new JSON is stored in `availability_dirty`, signaling the `Deployer` to trigger a build.
- Config keys: `build.is_enabled` (bool), `build.interval` (Go duration string, e.g., "10m"), `build.cf_deploy_hook` (Pages Build Hook URL).
- Scheduling: Deploys are scheduled to always fall offset by one minute after the hour. Example: with `build.interval = 10m` deploys occur at HH:01, HH:11, HH:21, ... This reduces the chance of syncing stale calendar events that commonly start at round minutes (e.g., HH:20, HH:30).
- Security: Do not commit `build.cf_deploy_hook` into source control; provide it via `config.yaml` or the `BUILD_CF_DEPLOY_HOOK` environment variable.
- Config keys: `availability.enabled` (bool), `availability.targets.cloudflare_pages.interval` (Go duration string, e.g., "10m"), `availability.targets.cloudflare_pages.deploy_hook` (Pages Build Hook URL).
- Scheduling: Deploys are scheduled to always fall offset by one minute after the hour. Example: with `availability.targets.cloudflare_pages.interval = 10m` deploys occur at HH:01, HH:11, HH:21, ... This reduces the chance of publishing stale calendar events that commonly start at round minutes (e.g., HH:20, HH:30).
- Security: Do not commit `availability.targets.cloudflare_pages.deploy_hook` into source control; provide it via `config.yaml` or the `AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_DEPLOY_HOOK` environment variable.
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,33 @@ Personal app to sync my calendar status with GitHub and expose availability from
## Quickstart

```bash
export CALENDAR_URL="https://calendar.google.com/calendar/ical/...%40group.calendar.google.com/public/basic.ics"
export GITHUB_TOKEN="ghp_..."
export STATUS_ENABLED=true
export STATUS_SOURCES_ICAL_URL="https://calendar.google.com/calendar/ical/...%40group.calendar.google.com/public/basic.ics"
export STATUS_TARGETS_GITHUB_TOKEN="ghp_..."

podman compose up
```

Status sync starts only when `STATUS_ENABLED=true` and a status target is
configured. The status calendar fetch interval is configured with
`STATUS_SOURCES_ICAL_INTERVAL` / `status.sources.ical.interval`, defaulting to
`5m`.

## Availability

Set `AVAILABILITY_IS_ENABLED=true` to expose `/api/availability` from a separate calendar feed.
Set `AVAILABILITY_ENABLED=true` to fetch availability, expose
`/api/availability`, and trigger Cloudflare Pages deploys when availability
changes.

- `AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_START` defaults to `09:00` and `AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_END` defaults to `17:50`; weekday blocks that overlap that window are suppressed unless the day is a bank holiday.
- Set `AVAILABILITY_SUPPRESSIONS_EXCLUDE_ENGLAND_BANK_HOLIDAYS=true` to lift that weekday suppression on England-and-Wales bank holidays from GOV.UK. Holiday data is fetched at startup and cached in Pebble.
- `AVAILABILITY_SOURCES_ICAL_URL` controls the availability feed, and `AVAILABILITY_API_KEY` controls the exact `Authorization` header required by the endpoint.
- `AVAILABILITY_SOURCES_ICAL_INTERVAL` / `availability.sources.ical.interval` controls the availability calendar fetch interval, defaulting to `5m`.

## Cloudflare Pages

- `AVAILABILITY_WORKING_HOURS_START` defaults to `09:00` and `AVAILABILITY_WORKING_HOURS_END` defaults to `17:50`; weekday blocks that overlap that window are suppressed unless the day is a bank holiday.
- Set `AVAILABILITY_EXCLUDE_ENGLAND_BANK_HOLIDAYS=true` to lift that weekday suppression on England-and-Wales bank holidays from GOV.UK. Holiday data is fetched at startup and cached in Pebble.
- `AVAILABILITY_CALENDAR_URL` and `AVAILABILITY_API_KEY` still control the availability feed and the exact `Authorization` header required by the endpoint.
When availability is enabled, Cloudflare Pages deploys are required and run when
computed availability changes. Configure the build hook with
`AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_DEPLOY_HOOK`; the publish interval is
`AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_INTERVAL` /
`availability.targets.cloudflare_pages.interval`, defaulting to `10m`.
4 changes: 2 additions & 2 deletions chart/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ description: A Helm chart for Kubernetes

type: application

version: 0.2.7
appVersion: "v0.2.7"
version: 0.2.8
appVersion: "v0.2.8"
26 changes: 21 additions & 5 deletions chart/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,35 @@ fullnameOverride: ""
env: []
# - name: EXAMPLE_ENV_VAR
# value: "example-value"
# Status env vars supported by the app:
# - name: STATUS_ENABLED
# value: "true"
# - name: STATUS_SOURCES_ICAL_URL
# value: "https://example.com/status.ics"
# - name: STATUS_SOURCES_ICAL_INTERVAL
# value: "5m"
# - name: STATUS_TARGETS_GITHUB_TOKEN
# value: "ghp_xyz"
# Availability env vars supported by the app:
# - name: AVAILABILITY_IS_ENABLED
# - name: AVAILABILITY_ENABLED
# value: "true"
# - name: AVAILABILITY_CALENDAR_URL
# - name: AVAILABILITY_SOURCES_ICAL_URL
# value: "https://example.com/availability.ics"
# - name: AVAILABILITY_SOURCES_ICAL_INTERVAL
# value: "5m"
# - name: AVAILABILITY_API_KEY
# value: "secret-key"
# - name: AVAILABILITY_WORKING_HOURS_START
# - name: AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_START
# value: "09:00"
# - name: AVAILABILITY_WORKING_HOURS_END
# - name: AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_END
# value: "17:50"
# - name: AVAILABILITY_EXCLUDE_ENGLAND_BANK_HOLIDAYS
# - name: AVAILABILITY_SUPPRESSIONS_EXCLUDE_ENGLAND_BANK_HOLIDAYS
# value: "true"
# Cloudflare Pages target env vars supported by the app:
# - name: AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_INTERVAL
# value: "10m"
# - name: AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_DEPLOY_HOOK
# value: "https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/..."

# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
serviceAccount:
Expand Down
44 changes: 24 additions & 20 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@
# Optional environment variables (override config.yaml values):
# PORT (default: 8080)
# PEBBLE_PATH (default: ./data)
# CALENDAR_URL (iCal URL — required if not in config.yaml)
# GITHUB_TOKEN (GitHub personal access token)
# AVAILABILITY_IS_ENABLED
# AVAILABILITY_CALENDAR_URL
# STATUS_ENABLED
# STATUS_SOURCES_ICAL_URL
# STATUS_SOURCES_ICAL_INTERVAL
# STATUS_TARGETS_GITHUB_TOKEN
# AVAILABILITY_ENABLED
# AVAILABILITY_SOURCES_ICAL_URL
# AVAILABILITY_SOURCES_ICAL_INTERVAL
# AVAILABILITY_API_KEY
# AVAILABILITY_WORKING_HOURS_START
# AVAILABILITY_WORKING_HOURS_END
# AVAILABILITY_EXCLUDE_ENGLAND_BANK_HOLIDAYS
# BUILD_IS_ENABLED
# BUILD_INTERVAL
# BUILD_CF_DEPLOY_HOOK
# AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_START
# AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_END
# AVAILABILITY_SUPPRESSIONS_EXCLUDE_ENGLAND_BANK_HOLIDAYS
# AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_INTERVAL
# AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_DEPLOY_HOOK
#
# Usage:
# podman compose up -d
Expand All @@ -30,14 +32,16 @@ services:
environment:
PORT: ${PORT:-8080}
PEBBLE_PATH: ${PEBBLE_PATH:-/data}
CALENDAR_URL: ${CALENDAR_URL}
GITHUB_TOKEN: ${GITHUB_TOKEN}
AVAILABILITY_IS_ENABLED: ${AVAILABILITY_IS_ENABLED}
AVAILABILITY_CALENDAR_URL: ${AVAILABILITY_CALENDAR_URL}
STATUS_ENABLED: ${STATUS_ENABLED}
STATUS_SOURCES_ICAL_URL: ${STATUS_SOURCES_ICAL_URL}
STATUS_SOURCES_ICAL_INTERVAL: ${STATUS_SOURCES_ICAL_INTERVAL}
STATUS_TARGETS_GITHUB_TOKEN: ${STATUS_TARGETS_GITHUB_TOKEN}
AVAILABILITY_ENABLED: ${AVAILABILITY_ENABLED}
AVAILABILITY_SOURCES_ICAL_URL: ${AVAILABILITY_SOURCES_ICAL_URL}
AVAILABILITY_SOURCES_ICAL_INTERVAL: ${AVAILABILITY_SOURCES_ICAL_INTERVAL}
AVAILABILITY_API_KEY: ${AVAILABILITY_API_KEY}
AVAILABILITY_WORKING_HOURS_START: ${AVAILABILITY_WORKING_HOURS_START}
AVAILABILITY_WORKING_HOURS_END: ${AVAILABILITY_WORKING_HOURS_END}
AVAILABILITY_EXCLUDE_ENGLAND_BANK_HOLIDAYS: ${AVAILABILITY_EXCLUDE_ENGLAND_BANK_HOLIDAYS}
BUILD_IS_ENABLED: ${BUILD_IS_ENABLED}
BUILD_INTERVAL: ${BUILD_INTERVAL}
BUILD_CF_DEPLOY_HOOK: ${BUILD_CF_DEPLOY_HOOK}
AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_START: ${AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_START}
AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_END: ${AVAILABILITY_SUPPRESSIONS_WORKING_HOURS_END}
AVAILABILITY_SUPPRESSIONS_EXCLUDE_ENGLAND_BANK_HOLIDAYS: ${AVAILABILITY_SUPPRESSIONS_EXCLUDE_ENGLAND_BANK_HOLIDAYS}
AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_INTERVAL: ${AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_INTERVAL}
AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_DEPLOY_HOOK: ${AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_DEPLOY_HOOK}
72 changes: 43 additions & 29 deletions config.yaml
Original file line number Diff line number Diff line change
@@ -1,29 +1,51 @@
port: 8080
pebble_path: ./data

# iCal URL for your calendar (required)
calendar_url: https://example.com/calendar.ics

targets:
github:
# GitHub personal access token (optional, enables GitHub status sync)
# Scopes required: user scope for status updates
# token: ghp_xyz
token: ""
# The status feature fetches events from an iCal calendar
# and updates GitHub status accordingly
status:
enabled: true
sources:
ical:
url: https://example.com/status.ics
interval: 5m
targets:
github:
# GitHub personal access token required when status is enabled.
# Scopes required: user scope for status updates.
# token: ghp_xyz
token: ""

# The availability feature fetches events from an iCal calendar,
# infers my availability based on the events & the following configuration,
# expoes the availability via an API used by my static website during build,
# and automatically triggers a redeployment of my website when availability changes.
availability:
# Disabled by default. Set to true to enable the availability API.
is_enabled: false
# Weekday working hours, used to suppress availability unless it's a bank holiday.
working_hours:
start: "09:00"
end: "17:50"
# Set to true to fetch GOV.UK England-and-Wales bank holidays and lift weekday suppression on those dates.
exclude_england_bank_holidays: true
# Separate iCal URL for availability data.
calendar_url: https://example.com/availability.ics
# Exact Authorization header value required by /api/availability.
api_key: ""
enabled: false
sources:
ical:
url: https://example.com/availability.ics
interval: 5m

api:
# Exact Authorization header value required by /api/availability when availability is enabled.
key: ""

targets:
cloudflare_pages:
# Deploy checks are aligned to HH:01, HH:11, HH:21, ... when interval is 10m.
interval: 10m
# Cloudflare Pages build hook URL required when availability is enabled.
# Prefer AVAILABILITY_TARGETS_CLOUDFLARE_PAGES_DEPLOY_HOOK for secrets.
deploy_hook: ""

suppressions:
# Weekday working hours, used to suppress availability unless it's a bank holiday.
working_hours:
start: "09:00"
end: "17:50"
# Set to true to fetch GOV.UK England-and-Wales bank holidays and lift weekday suppression on those dates.
exclude_england_bank_holidays: true
# Ordered availability windows, checked from top to bottom.
blocks:
- name: All day
Expand All @@ -44,11 +66,3 @@ availability:
- name: Evening
start: "17:30"
end: "22:00"

build:
# Disabled by default. Set to true to trigger Cloudflare Pages deploys when availability changes.
is_enabled: false
# Deploy checks are aligned to HH:01, HH:11, HH:21, ... when interval is 10m.
interval: 10m
# Cloudflare Pages build hook URL. Prefer BUILD_CF_DEPLOY_HOOK for secrets.
cf_deploy_hook: ""
Loading