Chatwoot, Arcane deployment
Docker / build (push) Successful in 1m35s

This commit is contained in:
2026-07-18 00:40:53 +12:00
parent 1da82b9d30
commit 19dcce5d59
10 changed files with 137 additions and 18 deletions
+2
View File
@@ -0,0 +1,2 @@
RESEND_API_KEY=
CHATWOOT_TOKEN=
+27
View File
@@ -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://<arcane-host>/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
+2
View File
@@ -8,7 +8,9 @@ __v0_jsx-dev-runtime.ts
next.user-config.*
# Environment variables
.env
.env*.local
!.env.example
# Common ignores
node_modules/
+12 -1
View File
@@ -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.
+12
View File
@@ -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 })
}
+59 -15
View File
@@ -1,20 +1,64 @@
'use client'
import Script from 'next/script'
import { useEffect } from 'react'
export function ChatWidget() {
return (
<>
<Script
defer
src="http://odoo.cloudrite.co.nz/im_livechat/loader/2"
strategy="afterInteractive"
/>
<Script
defer
src="http://odoo.cloudrite.co.nz/im_livechat/assets_embed.js"
strategy="afterInteractive"
/>
</>
)
useEffect(() => {
let cancelled = false
// Fetch the token at runtime from the server so it can come from the
// container environment (compose env_file) instead of being inlined
// into the client bundle at build time.
fetch('/api/chatwoot')
.then((res) => (res.ok ? res.json() : null))
.then((config: { token: string | null } | null) => {
if (cancelled) return
const chatwootToken = config?.token
if (!chatwootToken) {
console.warn('CHATWOOT_TOKEN not configured')
return
}
// Match the site's dark theme. Chatwoot only supports 'light' and
// 'auto' (follows prefers-color-scheme), so 'auto' is the darkest
// available setting. Must be set before the SDK runs.
;(window as any).chatwootSettings = {
...(window as any).chatwootSettings,
darkMode: 'auto'
}
// Load Chatwoot widget script
const script = document.createElement('script')
script.src = 'https://app.chatwoot.com/packs/js/sdk.js'
script.async = true
script.onload = () => {
// Initialize Chatwoot after script loads
if ((window as any).chatwootSDK) {
;(window as any).chatwootSDK.run({
websiteToken: chatwootToken,
baseUrl: 'https://app.chatwoot.com'
})
}
}
script.onerror = () => {
console.error('Failed to load Chatwoot widget')
}
document.body.appendChild(script)
})
.catch(() => {
console.error('Failed to fetch Chatwoot config')
})
return () => {
cancelled = true
// Cleanup is handled by Chatwoot itself
// Don't remove the script as it manages its own lifecycle
}
}, [])
return null
}
+1 -1
View File
@@ -147,7 +147,7 @@ export function Hero() {
asChild
variant="outline"
size="lg"
className="w-full py-6 text-lg border-border hover:bg-card hover:border-primary/50 transition-all duration-300"
className="w-full py-6 text-lg border-border hover:bg-card hover:border-primary/50 hover:text-primary-background transition-all duration-300"
>
<Link href="#services">Our Services</Link>
</Button>
+20
View File
@@ -0,0 +1,20 @@
services:
cloudrite:
image: git.cloudrite.co.nz/asowneryt/cloudrite:main
container_name: cloudrite
restart: unless-stopped
ports:
- "3000:3000"
env_file:
- path: .env
required: false
healthcheck:
test:
- CMD
- node
- -e
- "fetch('http://127.0.0.1:3000/').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
File diff suppressed because one or more lines are too long