diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f07619c --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +RESEND_API_KEY= +CHATWOOT_TOKEN= diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml index 96bbb58..11c4567 100644 --- a/.gitea/workflows/docker-publish.yml +++ b/.gitea/workflows/docker-publish.yml @@ -74,3 +74,30 @@ jobs: # cache import timed out (~30s) on every run. The Dockerfile's BuildKit # cache mounts still speed up dependency installs within a build. platforms: linux/amd64 + + # Tell Arcane to pull the freshly-pushed image and recreate the container. + # + # Create the webhook in Arcane: Settings -> Webhooks -> New, targeting the + # compose PROJECT with the "update" action (pulls new images + recreates + # changed services). Arcane shows the token (arc_wh_...) exactly once; the + # full trigger URL is + # https:///api/webhooks/trigger/arc_wh_xxxxxxxx... + # Store that whole URL as the Actions secret ARCANE_WEBHOOK_URL. The + # endpoint is unauthenticated apart from the token in the path (rate-limited + # to 60 req / 10s per IP), takes no body, and only fires on real pushes to + # main — not on PRs, and skipped automatically if the secret is unset. + - name: Trigger Arcane deployment + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + ARCANE_WEBHOOK_URL: ${{ secrets.ARCANE_WEBHOOK_URL }} + run: | + if [ -z "$ARCANE_WEBHOOK_URL" ]; then + echo "ARCANE_WEBHOOK_URL not set; skipping deploy trigger." + exit 0 + fi + code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST "$ARCANE_WEBHOOK_URL") + echo "Arcane webhook responded with HTTP $code" + case "$code" in + 2*) echo "Deployment triggered." ;; + *) echo "::error::Arcane webhook failed (HTTP $code)"; exit 1 ;; + esac diff --git a/.gitignore b/.gitignore index 6e2519a..5d699db 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,9 @@ __v0_jsx-dev-runtime.ts next.user-config.* # Environment variables +.env .env*.local +!.env.example # Common ignores node_modules/ diff --git a/CLAUDE.md b/CLAUDE.md index 0125dcc..f628aed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,11 +84,22 @@ Required for full functionality: ``` RESEND_API_KEY # Resend API key for contact form emails -NEXT_PUBLIC_CHATWOOT_ACCOUNT_TOKEN # Chatwoot widget token +CHATWOOT_TOKEN # Chatwoot website token (served at runtime via GET /api/chatwoot) ``` +Both are read server-side at **runtime**, so they can be supplied via Docker +compose `env_file` — no build args or `NEXT_PUBLIC_` prefix needed. The +Chatwoot token is intentionally not `NEXT_PUBLIC_`: the client widget fetches +it from `/api/chatwoot` (a `force-dynamic` route) instead of having it inlined +at build time. + ## API Routes +### GET /api/chatwoot + +Returns `{ "token": string | null }` — the Chatwoot website token read from +the server environment at request time. Consumed by `components/chatwidget.tsx`. + ### POST /api/send-email Sends contact form submissions to contact@cloudrite.co.nz via Resend. diff --git a/app/api/chatwoot/route.ts b/app/api/chatwoot/route.ts new file mode 100644 index 0000000..4479152 --- /dev/null +++ b/app/api/chatwoot/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from 'next/server' + +// Force runtime evaluation so the token is read from the container's +// environment on each request instead of being frozen at build time. +// This lets Docker compose supply CHATWOOT_TOKEN via env_file. +export const dynamic = 'force-dynamic' + +export async function GET() { + // Chatwoot website tokens are public by design (they ship in the widget), + // so exposing it through this endpoint is safe. + return NextResponse.json({ token: process.env.CHATWOOT_TOKEN ?? null }) +} diff --git a/components/chatwidget.tsx b/components/chatwidget.tsx index a2acadc..b644ec9 100644 --- a/components/chatwidget.tsx +++ b/components/chatwidget.tsx @@ -1,20 +1,64 @@ 'use client' -import Script from 'next/script' +import { useEffect } from 'react' export function ChatWidget() { - return ( - <> -