Files
domogeek/CLAUDE.md
T
cyrilixandClaude Sonnet 5 bf357015bc build: fix Docker build and switch releases to the homelab Tekton pipeline
The Dockerfile was broken since the MCP endpoint was added: it built a
single file (cmd/domogeek/domogeek.go) instead of the whole package, so
mcp.go's symbols (calendarDayFor, newMCPHandler) were undefined. Fixes
that by building ./cmd/domogeek, pins the builder image to
golang:1.27-alpine (module now requires go 1.25+), and drops the
hardcoded GOOS/GOARCH=amd64 so multi-arch builds compile natively per
platform instead of always producing an amd64 binary.

Also fixes .Dockerignore (wrong case, never actually read by
docker/buildah) by renaming it to .dockerignore, and anchors its
"domogeek" pattern to /domogeek so it excludes only the compiled binary,
not the whole cmd/domogeek source directory (same bug as the .gitignore
fix in an earlier commit).

Removes build-docker.sh: image releases now go through the existing
homelab Tekton pipeline (gitea-docker-build-multiarch), triggered by a
Gitea webhook on tag push, same pattern as other repos (e.g.
dra-devices). Images move from docker.io/cyrilix/domogeek to
git.cyrilix.bzh/cyrilix/domogeek. Documented in CLAUDE.md and README.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hu8wwahgecuCGfesTKCfEd
2026-09-05 11:09:28 +02:00

8.5 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

domogeek is a small Go HTTP service that exposes calendar/holiday information for home automation (domotic) use. It answers whether "today" is a working day, a French public holiday (ferie), and/or a personal holiday read from a CalDAV calendar. The same information is also exposed as MCP tools over a Streamable HTTP endpoint (/mcp), so LLM agents/MCP clients can query it directly.

Commands

Build and test use Go's standard tooling with vendored dependencies (-mod=vendor). The module requires Go 1.25+ (bumped from 1.18 to use the official MCP SDK). If the go binary on PATH resolves to an older toolchain (e.g. this box also has golang-1.18 installed and it can shadow the newer one), invoke a newer toolchain explicitly, overriding GOROOT if it's pinned to the old one (e.g. by an IDE's _INTELLIJ_FORCE_SET_GOROOT):

GOROOT=/usr/lib/go-1.27 /usr/lib/go-1.27/bin/go build -mod=vendor ./...

Otherwise, plain invocations work the same as before:

go build -mod=vendor ./...                 # build
go vet -mod=vendor ./...                   # vet
go test -mod=vendor ./...                  # run all tests
go test -mod=vendor ./pkg/calendar/ -run TestCalendar_IsWorkingDay -v   # run a single test

Run locally (caldav flags are optional — omitting them disables CalDAV-based holiday checks):

go run -mod=vendor ./cmd/domogeek \
  -port 8080 \
  -caldav-url https://example.com \
  -caldav-path /calendars/user/holidays/ \
  -caldav-username user \
  -caldav-password pass \
  -caldav-summary-pattern Holidays

Running with no flags at all prints usage and exits (see the len(os.Args) <= 1 check in main). Note: despite the "caldav flags are optional" framing, main() currently calls calendar.NewCaldav unconditionally, even with an empty -caldav-url — with no caldav server to validate against, this hits the 1000-retry exponential backoff described below and the process never gets to http.ListenAndServe. This is a pre-existing quirk, not something introduced by the MCP work; if you need a quick local instance (e.g. to exercise /mcp or /calendar without a real CalDAV server), it's easiest to call newMCPHandler()/calendar.New(location) directly from a small throwaway main/test rather than running the built binary.

Docker image (multi-stage, distroless), for local testing:

docker build -t domogeek .   # or: buildah bud -t domogeek .

Continuous builds

Pushing a Git tag matching v[0-9]* (existing convention: vX.Y.Z, e.g. v0.4.0) to the Gitea remote triggers the homelab Tekton pipeline gitea-docker-build-multiarch, which builds and pushes a multi-arch (amd64 + arm64) image plus a signed CycloneDX SBOM:

git.cyrilix.bzh/cyrilix/domogeek:<tag>

The pipeline uses its defaults — Dockerfile at the repo root, build context . — so the Dockerfile must stay at the root. It builds each target platform natively (no GOOS/GOARCH overrides in the go build step), so the Dockerfile must not hardcode a single-arch GOOS/GOARCH — that was a real bug fixed alongside this: the builder image is now pinned to golang:1.27-alpine (matching the go 1.25 module requirement) and the go build step targets the whole package (./cmd/domogeek), not a single file.

There is no Tekton YAML in this repo: the pipeline and its Gitea webhook trigger are defined cluster-side. Watch a run: https://tekton.banquise.cyrilix.bzh

git tag v0.4.0
git push --tags

Note: this replaces the previous docker.io/cyrilix/domogeek:<git describe> releases built locally via the now-removed build-docker.sh/buildah script.

Architecture

  • cmd/domogeek/domogeek.go — the binary entrypoint. Wires up flags, zap logging, the calendar.Calendar, Prometheus metrics/instrumented handlers, and a health-go health check, then serves five HTTP routes:

    • GET /calendar — returns a CalendarDay JSON payload (working_day, ferie, holiday, weekday) for "now" in Europe/Paris.
    • GET /datetime — returns a CurrentDateTime JSON payload (date_time) for "now" in Europe/Paris (DateTimeHandler).
    • POST /mcp — MCP Streamable HTTP endpoint (see cmd/domogeek/mcp.go below).
    • GET /metrics — Prometheus metrics (request counter/summary/histogram wrapping the calendar handler, defined in init()).
    • GET /status — health-go handler with two checks: a static calendar check and a caldav check that calls cal.IsHolidaysFromCaldav.
    • The process blocks on SIGTERM (not SIGINT) via a signal channel; http.ListenAndServe runs in a goroutine and zap.S().Fatals the process on error.
  • cmd/domogeek/mcp.go — MCP server built with github.com/modelcontextprotocol/go-sdk/mcp, mounted at /mcp via mcp.NewStreamableHTTPHandler. Exposes four tools, all reading the same package-level cal/location globals as the /calendar and /datetime handlers:

    • get_current_datetime — same payload as GET /datetime: the current date and time.
    • get_calendar_today — same payload as GET /calendar, for now.
    • get_calendar_for_date — same payload for an arbitrary date (YYYY-MM-DD); an invalid date is returned as a tool error (CallToolResult.IsError), not a protocol-level error, so an MCP client/LLM can see and self-correct.
    • get_holidays — lists French public holidays (Calendar.GetHolidays) for a given year, defaulting to the current year if omitted.
    • calendarDayFor in this file is the single place that builds a CalendarDay from a time.Time; both the HTTP /calendar handler and the MCP tools call it, so there's one code path for the working-day/ferie/holiday/weekday logic.
    • cmd/domogeek/mcp_test.go unit-tests the handler functions directly; cmd/domogeek/mcp_e2e_test.go drives the real Streamable HTTP transport with an MCP client (httptest.Server + mcp.NewClient(...).Connect) to check the wiring end-to-end (tool listing, structured content, and the tool-error-vs-protocol-error distinction). cmd/domogeek/datetime_test.go covers the /datetime HTTP handler directly.
  • pkg/calendar/calendar.go — all domain logic, built around the Calendar struct and functional options (WithCaldav, WithCaldavPath, WithCaldavSummaryPattern):

    • GetEasterDay implements the Gauss/Meeus algorithm to compute Easter Sunday for a given year (in cal.Location); GetHolidays derives the fixed-date French public holidays plus two computed from it: Easter Monday (Easter+1) and Ascension (Easter+39).
    • IsHoliday combines the static French holiday set with IsHolidaysFromCaldav.
    • IsHolidaysFromCaldav queries a CalDAV calendar for events on a given day and treats any event whose Summary contains caldavSummaryPattern (default "Holidays") as a personal holiday. If no Caldav client is configured, it returns false, nil — CalDAV is optional.
    • The Caldav interface (QueryEvents) abstracts github.com/dolanor/caldav-go, letting tests substitute MockCaldav (see calendar_test.go) instead of hitting a real server.
    • NewCaldav validates the CalDAV connection at startup with avast/retry-go, retrying up to 1000 times with exponential backoff (max delay 24h) — this means a bad/unreachable CalDAV server does not fail fast; main() will effectively hang retrying rather than exiting quickly if the caldav server is down (it's only Fatal'd if NewCaldav itself returns a non-nil error, which currently only happens after all retries are exhausted).
  • pkg/metrics/metrics.go — currently a single unused/unconfigured Prometheus counter (empty Namespace/Subsystem/Name/Help). Not wired into cmd/domogeek; treat as in-progress scaffolding rather than an established pattern to follow.

Dependencies

Dependencies are vendored under vendor/ and checked into the repo — always pass -mod=vendor (or rely on GOFLAGS=-mod=vendor if set) for build/test/vet so Go doesn't try to hit the network or the module cache. After changing go.mod/go.sum, run go mod vendor to keep vendor/ in sync (use the newer toolchain per the note above, and -mod=mod instead of -mod=vendor for the go get/go mod tidy step itself).

github.com/modelcontextprotocol/go-sdk (the official Go MCP SDK) is the dependency added for the /mcp endpoint; it pulled in google/jsonschema-go, segmentio/encoding, yosida95/uritemplate, and a few golang.org/x/* packages as transitive deps, and required bumping the go directive in go.mod from 1.18 to 1.25.0.