- Go 97.7%
- Shell 1.8%
- Dockerfile 0.5%
usage_data decodes to map[string]map[string]int ("1" aged 2+, "2"
under 2), not map[string][]int, so every call failed and occupancy
was never known. SpacesUsed also counted under-2s as two spaces;
each class's own #reservationsmax is 10 for every observed class and
one-space-per-child makes occupancy peak at exactly that, while two
pushed classes past the gym's own ceiling. Capacity is now read per
class; FRESSI_CAPACITY is fallback only.
getUpdates retried immediately on error, so one 502 became a 32
req/s storm. Now exponential backoff 1s-30s reset on success,
honouring retry_after. Bot token redacted from logged errors
(url.Error embedded it in the request URL). Timeout now longPoll+20s.
GetUpdates takes a context.
fressi monitoring now starts at boot instead of needing /fressi on
after every restart, and calendarWeeksLocked skips a failed
location/week instead of discarding every result.
Verified live: reservationsmax 10, peak occupancy 10, arrival 18:45
rejected while 19:15 offered.
|
||
|---|---|---|
| .forgejo/workflows | ||
| app | ||
| manifests | ||
| scripts | ||
| .gitignore | ||
| Dockerfile | ||
| README.md | ||
telegram-agent
Full-agent Telegram bot: restart workloads and trigger reruns from a chat message. Runs as a pod in K3s, no public exposure.
How it reaches the outside world (and vice versa)
- Telegram: long polling (
getUpdates), not a webhook — the pod only makes outbound HTTPS toapi.telegram.org. No Service, no MetalLB IP, no Caddy route. Nothing on the internet can reach this pod directly. - Kubernetes API: in-cluster, via the auto-mounted ServiceAccount token — no kubeconfig to manage.
- Forgejo: outbound HTTPS to
git.yukselcloud.com's API, using a fine-grained token (Actions:write, scoped tolab/homelabonly).
Safety model
- Chat-ID allowlist: every incoming update is checked against
TELEGRAM_AUTHORIZED_CHAT_ID; anything else is silently ignored. - Confirm before acting:
/restartand/rerunstage the action and wait up to 60s for a plainyesreply from the same chat before doing anything./statusis read-only and doesn't need confirmation. - Namespace allowlist: hardcoded in
app/config.go, kept in sync with which namespaces have atelegram-agent-actionsRoleBinding applied. Changing what the bot can touch means a code change through the normal GitOps review, not a live config edit. - Least-privilege RBAC: a purpose-built ClusterRole
(
clusterrole-actions.yaml) with only the verbs/restartand/rerunactually need — not the built-ineditrole.
Commands
/status <namespace>
/restart <namespace> <deployment|statefulset|daemonset> <name>
/rerun k8s <namespace> <cronjob>
/rerun ci <workflow-file.yaml> [ref, default main]
/fressi <status|check>
Setup
-
Get a bot token from @BotFather, and your numeric chat ID from @userinfobot.
-
Mint a Forgejo fine-grained token scoped to
lab/homelab,Actions: Read and Writeonly. -
Add
workflow_dispatch:to theon:block of any.forgejo/workflows/*file you want/rerun ciable to trigger — the existingpush-only workflows can't be dispatched via API as-is. -
Apply the manifests (or let the GitOps workflow do it after first push):
kubectl apply -f manifests/namespace.yaml kubectl apply -f manifests/serviceaccount.yaml kubectl apply -f manifests/clusterrolebinding-view.yaml kubectl apply -f manifests/clusterrole-actions.yaml kubectl apply -f manifests/rolebinding-media-actions.yaml kubectl apply -f manifests/rolebinding-monitoring-actions.yaml -
Create the real Secret (never commit it — see the comment in
manifests/secret-bot-config.yaml.example):kubectl -n telegram-agent create secret generic telegram-agent-config \ --from-literal=TELEGRAM_BOT_TOKEN='...' \ --from-literal=TELEGRAM_AUTHORIZED_CHAT_ID='...' \ --from-literal=FORGEJO_TOKEN='...' \ --from-literal=FORGEJO_BASE_URL='https://git.yukselcloud.com' \ --from-literal=FORGEJO_OWNER='lab' \ --from-literal=FORGEJO_REPO='homelab' -
kubectl apply -f manifests/deployment.yaml, or push and let.forgejo/workflows/deploy-telegram-agent.yamldo it — check the comment at the top of that workflow first: it assumes the runner can build/push a Docker image directly, which hasn't been verified against how this cluster's runner is actually set up.
Extending it with push notifications later
homelab-event-collector already plans to notify via ntfy once its ingress
layer (webhook + K8s watcher) is built. Once that's routing events through
Redis/Redpanda, adding Telegram as a second sink is small: a goroutine here
subscribing to the same channel and calling SendMessage. Not built yet
because the upstream pipeline isn't either — no point guessing its shape
before it exists.
Adding a new managed namespace
- Copy
manifests/rolebinding-media-actions.yaml, change the namespace, apply it. - Add the namespace to
AllowedNamespacesinapp/config.go, redeploy.
Both steps are required — RBAC without the app-level allowlist just means the bot tries and gets a 403; the allowlist without RBAC means a confusing in-cluster permission error instead of a clean "not allowed" reply.